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
@@ -4,8 +4,12 @@
4
4
  *
5
5
  * @param {string} hookName - Name of the hook for debugging purposes
6
6
  * @returns {Object} Base hook interface with common functionality
7
- */const useBaseHook=hookName=>{// Environment detection — live check at call time, never stale from SSR
8
- const isNative=(0,_react.useCallback)(()=>{if(typeof window==="undefined")return false;return!!(window.WebBridge&&(window.NativeBridge||window.webkit?.messageHandlers?.NativeBridge));},[]);const isWeb=(0,_react.useCallback)(()=>{return!isNative();},[isNative]);// Common state management
7
+ */const useBaseHook=(hookName,{hasWebFallback=false,webFallback}={})=>{// Environment detection — live check at call time, never stale from SSR
8
+ const isNative=(0,_react.useCallback)(()=>{if(typeof window==="undefined")return false;return!!(window.WebBridge&&_NativeBridge.default.isAvailable());},[]);const isWeb=(0,_react.useCallback)(()=>{return!isNative();},[isNative]);// Web fallback state — only meaningful when isWeb() and hook declared hasWebFallback:true
9
+ // State tracks imperative setWebFallback() calls. The prop is the synchronous source of truth
10
+ // when provided — prop wins so there is no async gap between prop change and disabled state.
11
+ const[webFallbackState,setWebFallback]=(0,_react.useState)(true);// When prop is explicitly provided, use it directly (synchronous). When undefined, fall back to state.
12
+ const webFallbackResolved=webFallback!==undefined?!!webFallback:webFallbackState;const webFallbackActive=isWeb()&&hasWebFallback&&webFallbackResolved;const webFallbackDisabled=isWeb()&&hasWebFallback&&!webFallbackResolved;// Common state management
9
13
  const[data,setData]=(0,_react.useState)(null);const[loading,setLoading]=(0,_react.useState)(false);const[error,setError]=(0,_react.useState)(null);const[progress,setProgress]=(0,_react.useState)({state:"idle",// 'idle' | 'opening' | 'processing' | 'routing' | 'complete' | 'error'
10
14
  percentage:null,// 0-100 or null
11
15
  message:null,// Human readable message or null
@@ -17,22 +21,23 @@ bytesTotal:null// Total bytes
17
21
  const updateProgress=(0,_react.useCallback)(updates=>{setProgress(prev=>({...prev,...updates}));},[]);const resetProgress=(0,_react.useCallback)(()=>{setProgress({state:"idle",percentage:null,message:null,phase:null,transport:null,bytesLoaded:null,bytesTotal:null});},[]);const startProgress=(0,_react.useCallback)((phase=null,message=null)=>{updateProgress({state:"active",phase,message,percentage:null});},[updateProgress]);const completeProgress=(0,_react.useCallback)(()=>{updateProgress({state:"complete",percentage:100});},[updateProgress]);const errorProgress=(0,_react.useCallback)(()=>{updateProgress({state:"error"});},[updateProgress]);// Error handling utilities
18
22
  const handleNativeError=(0,_react.useCallback)(nativeError=>{const standardError=(0,_errors.translateError)(nativeError);setError(standardError);setLoading(false);errorProgress();// Development logging
19
23
  if((0,_errors.isDevelopment)()){console.group(`🚨 ${hookName} Error`);console.log("Standard Error:",standardError);console.log("Native Error:",nativeError);console.groupEnd();}return standardError;},[hookName,errorProgress]);const clearError=(0,_react.useCallback)(()=>{setError(null);if(progress.state==="error"){resetProgress();}},[progress.state,resetProgress]);// Data management utilities
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
- 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
24
+ const setDataAndComplete=(0,_react.useCallback)(newData=>{setData(newData);setLoading(false);completeProgress();setError(null);if((0,_errors.isDevelopment)()){console.log("✅ Native hook success:",hookName,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
25
+ 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("Native hook call failed silently:",hookName,err);}}},[hookName,isNative]);// Operation wrapper that handles common patterns
22
26
  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,isNative,startProgress,handleNativeError]);// Environment flags (computed values, not functions)
27
+ operationCallback();}catch(err){handleNativeError(err);console.error("❌ Native hook operation failed:",hookName,operationName,err);}},[hookName,isWeb,isNative,startProgress,handleNativeError]);// Environment flags (computed values, not functions)
24
28
  const environmentFlags={isWeb:isWeb(),isNative:isNative()};// Return standardized interface
25
29
  return{// Data state
26
30
  data,// Loading states
27
31
  loading,progress,// Error handling
28
32
  error,// Environment detection
29
- ...environmentFlags,// Actions
33
+ ...environmentFlags,// Web fallback control
34
+ webFallbackActive,webFallbackDisabled,setWebFallback,// Actions
30
35
  clear,clearError,// Internal utilities for specific hooks
31
36
  setData,setLoading,setError,setProgress,updateProgress,resetProgress,startProgress,completeProgress,errorProgress,setDataAndComplete,handleNativeError,callNative,executeOperation};};/**
32
37
  * Hook for development debugging and testing
33
38
  * @param {string} hookName
34
39
  * @returns {Object} Environment information and utilities
35
- */exports.useBaseHook=useBaseHook;const useEnvironmentInfo=hookName=>{const isNativeAvailable=()=>{if(typeof window==="undefined")return false;return!!(window.WebBridge&&_NativeBridge.default.isAvailable());};const getEnvironmentDetails=()=>{const details={hasWindow:typeof window!=="undefined",hasWebBridge:typeof window!=="undefined"&&!!window.WebBridge,nativeBridgeAvailable:false,userAgent:typeof navigator!=="undefined"?navigator.userAgent:"SSR",timestamp:new Date().toISOString()};if(details.hasWebBridge){details.nativeBridgeAvailable=_NativeBridge.default.isAvailable();}return details;};if((0,_errors.isDevelopment)()){console.log(`🔍 ${hookName} Environment:`,getEnvironmentDetails());}return{isNative:isNativeAvailable(),isWeb:!isNativeAvailable(),environmentDetails:getEnvironmentDetails()};};/**
40
+ */exports.useBaseHook=useBaseHook;const useEnvironmentInfo=hookName=>{const isNativeAvailable=()=>{if(typeof window==="undefined")return false;return!!(window.WebBridge&&_NativeBridge.default.isAvailable());};const getEnvironmentDetails=()=>{const details={hasWindow:typeof window!=="undefined",hasWebBridge:typeof window!=="undefined"&&!!window.WebBridge,nativeBridgeAvailable:false,userAgent:typeof navigator!=="undefined"?navigator.userAgent:"SSR",timestamp:new Date().toISOString()};if(details.hasWebBridge){details.nativeBridgeAvailable=_NativeBridge.default.isAvailable();}return details;};if((0,_errors.isDevelopment)()){console.log("🔍 Native hook environment:",hookName,getEnvironmentDetails());}return{isNative:isNativeAvailable(),isWeb:!isNativeAvailable(),environmentDetails:getEnvironmentDetails()};};/**
36
41
  * Progress state constants for consistency across hooks
37
42
  */exports.useEnvironmentInfo=useEnvironmentInfo;const PROGRESS_STATES=exports.PROGRESS_STATES={// Common states
38
43
  IDLE:"idle",STARTING:"starting",COMPLETE:"complete",ERROR:"error",// File Picker states
@@ -12,20 +12,44 @@
12
12
  * Validate command before execution
13
13
  */_validateCommand(command){if(!command){throw new Error("Command is required");}if(!(0,_NativeInterfaces.isValidCommand)(command)){throw new Error(`Invalid command: ${command}. Available commands: ${Object.values(_NativeInterfaces.NATIVE_COMMANDS).join(", ")}`);}if(!this.isAvailable()){throw new Error("Native bridge not available. Ensure you are running in a native WebView environment.");}}/**
14
14
  * Execute command on Android platform
15
- */_executeAndroidCommand(command,data){try{if(typeof window.NativeBridge[command]==="function"){console.log(`🌉 Calling Android method '${command}'`,data?{data}:"(no data)");// Always pass data parameter to match Kotlin method signatures
16
- window.NativeBridge[command](data);return true;}else{console.error(`Android bridge method '${command}' not found`);return false;}}catch(error){console.error(`Error executing Android command '${command}':`,error);return false;}}/**
15
+ */_executeAndroidCommand(command,data){try{if(typeof window.NativeBridge[command]==="function"){console.log("🌉 Calling Android method",command,data?{data}:"(no data)");// Always pass data parameter to match Kotlin method signatures
16
+ window.NativeBridge[command](data);return true;}else{console.error(`Android bridge method '${command}' not found`);return false;}}catch(error){console.error("Error executing Android command:",command,error);return false;}}/**
17
17
  * Execute command on iOS platform
18
18
  */_executeIOSCommand(command,data){try{// For iOS, we always send a message object
19
19
  const message={command:command,data:data// This can be null/undefined, iOS bridge should handle it
20
- };console.log(`🌉 Calling iOS method '${command}'`,data?{data}:"(no data)");window.webkit.messageHandlers.NativeBridge.postMessage(message);return true;}catch(error){console.error(`Error executing iOS command '${command}':`,error);throw error;}}/**
20
+ };console.log("🌉 Calling iOS method",command,data?{data}:"(no data)");window.webkit.messageHandlers.NativeBridge.postMessage(message);return true;}catch(error){console.error("Error executing iOS command:",command,error);throw error;}}/**
21
21
  * Main method to call native commands
22
22
  * @param {string} command - The command to execute (from NATIVE_COMMANDS)
23
23
  * @param {any} data - Optional data to pass to the native side
24
24
  * @returns {boolean} - Success status
25
25
  */call(command,data=null){try{// Validate environment and command
26
- this._validateCommand(command);console.log(`🌉 Executing native command: ${command}`,data?{data}:"");// Execute on appropriate platform
27
- if(this.isAndroid){return this._executeAndroidCommand(command,data);}else if(this.isIOS){return this._executeIOSCommand(command,data);}return false;}catch(error){console.error(`🌉 Failed to execute command '${command}':`,error);// In development, we might want to show user-friendly errors
26
+ this._validateCommand(command);console.log("🌉 Executing native command:",command,data?{data}:"");// Execute on appropriate platform
27
+ if(this.isAndroid){return this._executeAndroidCommand(command,data);}else if(this.isIOS){return this._executeIOSCommand(command,data);}return false;}catch(error){console.error("🌉 Failed to execute command:",command,error);// In development, we might want to show user-friendly errors
28
28
  if(process.env.NODE_ENV==="development"){console.warn(`🌉 Development mode: Native command '${command}' failed. This is expected when running in browser.`);}throw error;}}/**
29
+ * Video stream methods
30
+ */videoStream={/**
31
+ * Start native camera video stream
32
+ * @param {Object} options - Stream options
33
+ * facing: 'back' | 'front'
34
+ * format: 'qr' | 'barcode' | 'all' — format filter (default: 'all')
35
+ * zoom: { auto: true, initial: 1.0 } — auto-zoom via ML Kit, initial is multiplier (1.0=1x)
36
+ * zoom: { auto: false, initial: 2.0 } — manual, starts at 2x
37
+ */start:(options={})=>this.call(_NativeInterfaces.NATIVE_COMMANDS.START_VIDEO_STREAM,options),/**
38
+ * Stop native camera video stream
39
+ */stop:()=>this.call(_NativeInterfaces.NATIVE_COMMANDS.STOP_VIDEO_STREAM),/**
40
+ * Set zoom level multiplier (e.g. 1.0 = 1x, 2.0 = 2x)
41
+ * @param {number} zoomPct - multiplier >= 1.0
42
+ */setZoom:zoomPct=>{if(zoomPct==null||typeof zoomPct!=="number"||isNaN(zoomPct)){console.warn("[NativeBridge] setZoom: invalid value, ignoring:",zoomPct);return false;}return this.call(_NativeInterfaces.NATIVE_COMMANDS.SET_VIDEO_STREAM_ZOOM,zoomPct.toString());},/**
43
+ * Enable or disable torch (back camera only)
44
+ * @param {boolean} on
45
+ */setTorch:on=>this.call(_NativeInterfaces.NATIVE_COMMANDS.SET_VIDEO_STREAM_TORCH,JSON.stringify({on:!!on})),/**
46
+ * Set target FPS range — triggers session restart
47
+ * @param {number|null} min - minimum fps (e.g. 15), null to clear
48
+ * @param {number|null} max - maximum fps (e.g. 30), null to clear
49
+ */setFps:(min,max)=>this.call(_NativeInterfaces.NATIVE_COMMANDS.SET_VIDEO_STREAM_FPS,JSON.stringify({min:min??null,max:max??null})),/**
50
+ * Flip between front and back camera — restarts the native session internally
51
+ * Torch resets to off after flip; ON_TORCH_CHANGED(false) fires automatically.
52
+ */flip:()=>this.call(_NativeInterfaces.NATIVE_COMMANDS.FLIP_VIDEO_STREAM)};/**
29
53
  * Camera-specific methods for easier usage
30
54
  */camera={/**
31
55
  * Open camera for photo capture
@@ -99,7 +123,18 @@ if(process.env.NODE_ENV==="development"){console.warn(`🌉 Development mode: Na
99
123
  */getScreenSecure:()=>this.call(_NativeInterfaces.NATIVE_COMMANDS.GET_SCREEN_SECURE),/**
100
124
  * Clear all WebView data (cache, history, cookies, WebStorage)
101
125
  * Returns result through ON_WEB_DATA_CLEARED / ON_WEB_DATA_CLEAR_ERROR callback
102
- */clearWebData:()=>this.call(_NativeInterfaces.NATIVE_COMMANDS.CLEAR_WEB_DATA)};/**
126
+ */clearWebData:()=>this.call(_NativeInterfaces.NATIVE_COMMANDS.CLEAR_WEB_DATA)};transition={/**
127
+ * Begin a native transition — native takes a snapshot/overlay of the current screen.
128
+ * @param {Object} options
129
+ * type: 'slide' | 'fade'
130
+ * direction: 'left' | 'right' | 'up' | 'down' (slide only)
131
+ * duration: number (ms, default 300)
132
+ * timeout: number (ms, safety timer — native force-fades overlay if commitTransition is never called)
133
+ */start:(options={})=>this.call(_NativeInterfaces.NATIVE_COMMANDS.START_TRANSITION,JSON.stringify(options)),/**
134
+ * Commit the transition — native animates the snapshot overlay out, revealing the new page.
135
+ */commit:()=>this.call(_NativeInterfaces.NATIVE_COMMANDS.COMMIT_TRANSITION),/**
136
+ * Cancel the transition — native removes the overlay immediately without animating.
137
+ */cancel:()=>this.call(_NativeInterfaces.NATIVE_COMMANDS.CANCEL_TRANSITION)};/**
103
138
  * Get environment info
104
139
  */getEnvironmentInfo(){return{isAndroid:this.isAndroid,isIOS:this.isIOS,isNativeEnvironment:this.isNativeEnvironment,platform:this.isAndroid?"android":this.isIOS?"ios":"web"};}/**
105
140
  * Check if native bridge is available — live check, never stale
@@ -0,0 +1,52 @@
1
+ "use strict";const fs=require("fs");const path=require("path");function createAssetsPhase(ctx){const{configPath,publicPath,pwd,progress}=ctx;async function copyBuildAssets(androidConfig,buildOptimisation=false){if(!buildOptimisation)return;progress.log("Copying build assets to Android project...","info");try{const sourcePath=`${process.cwd()}/build/`;const destPath=`${pwd}/androidProject/app/src/main/assets/build/`;ctx.runCommand(`mkdir -p ${destPath}`);ctx.runCommand(`rm -rf ${destPath}/*`);const excludePatterns=["route-manifest.json.gz","route-manifest.json.br"];if(buildOptimisation){progress.log("Running with build optimization...","info");const excludeParams=excludePatterns.map(pattern=>`--exclude="${pattern}"`).join(" ");const rsyncCommand=`rsync -av ${excludeParams} ${sourcePath} ${destPath}`;progress.log("Executing rsync command with exclusions...","info");ctx.runCommand(rsyncCommand);for(const pattern of excludePatterns){const checkCommand=`find ${destPath} -name "${pattern}" | wc -l`;const count=parseInt(ctx.runCommand(checkCommand).trim(),10);if(count>0){progress.log(`Warning: Found ${count} instances of excluded file ${pattern}`,"warning");ctx.runCommand(`find ${destPath} -name "${pattern}" -delete`);}}progress.log("Build assets copied with optimization (excluded route-manifest JSON files)","success");}else{progress.log("Running without build optimization...","info");const exclusions=excludePatterns.map(pattern=>`-not -name "${pattern}"`).join(" ");const copyCommand=`find ${sourcePath} -type f ${exclusions} -exec cp --parents {} ${destPath} \\;`;progress.log(`Executing copy command with exclusions...`,"info");ctx.runCommand(copyCommand);progress.log("Build assets copied successfully!","success");}}catch(error){throw new Error("Error copying build assets: "+error.message);}}async function copySplashscreenAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const configFile=fs.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG={}}=config;if(!WEBVIEW_CONFIG.splashScreen)return;const androidPublicPath=`${process.cwd()}/public/android`;const imageFormats=["png","jpg","jpeg","gif","bmp","webp"];if(fs.existsSync(androidPublicPath)){const drawableDir=`${destPath}/drawable`;if(!fs.existsSync(drawableDir)){fs.mkdirSync(drawableDir,{recursive:true});}for(const format of imageFormats){const existingPath=`${destPath}/drawable/splashscreen.${format}`;if(fs.existsSync(existingPath)){fs.unlinkSync(existingPath);}}for(const format of imageFormats){const sourcePath=`${androidPublicPath}/splashscreen.${format}`;if(fs.existsSync(sourcePath)){fs.copyFileSync(sourcePath,`${destPath}/drawable/splashscreen.${format}`);break;}}}const backgroundColor=WEBVIEW_CONFIG.splashScreen.backgroundColor||"#ffffff";const themeFiles=[`${destPath}/values/themes.xml`,`${destPath}/values-night/themes.xml`];for(const themesFile of themeFiles){try{let content=fs.readFileSync(themesFile,"utf8");content=content.replace(/<item name="android:windowBackground">.*?<\/item>/,`<item name="android:windowBackground">${backgroundColor}</item>`);content=content.replace(/<item name="android:windowSplashScreenBackground" tools:targetApi="31">.*?<\/item>/,`<item name="android:windowSplashScreenBackground" tools:targetApi="31">${backgroundColor}</item>`);fs.writeFileSync(themesFile,content);}catch(e){if(e.code!=="ENOENT")throw e;}}}catch(error){progress.log(`Error processing splash screen: ${error.message}`,"warning");}}async function copyOfflinePage(){try{const sourcePath=`${process.cwd()}/public/offline.html`;const destDir=`${pwd}/androidProject/app/src/main/assets/offline`;const destPath=`${destDir}/offline.html`;if(!fs.existsSync(sourcePath)){progress.log("offline.html not found in public/; skipping offline asset copy","warning");return;}ctx.runCommand(`mkdir -p ${destDir}`);fs.copyFileSync(sourcePath,destPath);progress.log("offline.html copied to Android assets","success");}catch(error){progress.log(`Warning: Error copying offline.html: ${error.message}`,"warning");}}async function copyIconAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;const androidIconDir=path.join(publicPath,"android","appIcons");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - publicPath is process.cwd()/public, a trusted internal path.
2
+ const fallbackIconPath=path.join(__dirname,"../assets","catalyst.png");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - __dirname is a fixed module path, not user input.
3
+ const fallbackExists=fs.existsSync(fallbackIconPath);const extensions=["png","jpg","jpeg"];if(!fs.existsSync(publicPath)){progress.log(`Warning: Public directory not found at ${publicPath}`,"warning");return;}const densities=[{key:"mdpi",dir:"mipmap-mdpi"},{key:"hdpi",dir:"mipmap-hdpi"},{key:"xhdpi",dir:"mipmap-xhdpi"},{key:"xxhdpi",dir:"mipmap-xxhdpi"},{key:"xxxhdpi",dir:"mipmap-xxxhdpi"}];const hasIconDirectory=fs.existsSync(androidIconDir)&&fs.lstatSync(androidIconDir).isDirectory();const cleanDensityDir=dir=>{if(!fs.existsSync(dir))return;for(const file of fs.readdirSync(dir)){if(file.startsWith("icon.")){fs.unlinkSync(path.join(dir,file));// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - dir is derived from pwd/destPath (internal), file comes from fs.readdirSync on that same dir.
4
+ }}};const findIconForDensity=densityKey=>{if(!hasIconDirectory)return null;for(const ext of extensions){const candidate=path.join(androidIconDir,`icon-${densityKey}.${ext}`);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - androidIconDir is derived from publicPath (internal); densityKey and ext are fixed internal strings.
5
+ if(fs.existsSync(candidate))return{path:candidate,ext};}return null;};let hasCustomIcons=false;let usedFallback=false;for(const density of densities){const targetDir=path.join(destPath,density.dir);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destPath is derived from pwd (internal); density.dir is a fixed string from the densities array.
6
+ const source=findIconForDensity(density.key);cleanDensityDir(targetDir);if(source){fs.mkdirSync(targetDir,{recursive:true});const destination=path.join(targetDir,`icon.${source.ext}`);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - targetDir is derived from pwd (internal); source.ext is from a fixed extensions array.
7
+ fs.copyFileSync(source.path,destination);hasCustomIcons=true;}}if(!hasCustomIcons&&fallbackExists){const targetDir=path.join(destPath,"mipmap-xxxhdpi");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destPath is derived from pwd (internal); "mipmap-xxxhdpi" is a fixed literal.
8
+ fs.mkdirSync(targetDir,{recursive:true});cleanDensityDir(targetDir);const destination=path.join(targetDir,"icon.png");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - targetDir is derived from pwd (internal); "icon.png" is a fixed literal.
9
+ fs.copyFileSync(fallbackIconPath,destination);usedFallback=true;}const setManifestIcons=(iconValue,roundIconValue)=>{let current;try{current=fs.readFileSync(manifestPath,"utf8");}catch(e){if(e.code==="ENOENT")return;throw e;}const updated=current.replace(/android:icon="[^"]*"/g,`android:icon="${iconValue}"`).replace(/android:roundIcon="[^"]*"/g,`android:roundIcon="${roundIconValue}"`);if(updated!==current)fs.writeFileSync(manifestPath,updated);};if(!hasCustomIcons&&!usedFallback){progress.log("No custom Android icons found; using default template icons.","info");setManifestIcons("@mipmap/ic_launcher","@mipmap/ic_launcher_round");return;}setManifestIcons("@mipmap/icon","@mipmap/icon");if(hasCustomIcons){progress.log("Applied Android launcher icons from public/android/appIcons.","success");}if(usedFallback){progress.log("Used bundled Catalyst fallback icon for launcher.","info");}}catch(error){progress.log(`Warning: Error copying app icon assets: ${error.message}`,"warning");}}async function configureAppName(androidConfig){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const stringsFile=`${destPath}/values/strings.xml`;let stringsContent=fs.readFileSync(stringsFile,"utf8");if(androidConfig.appName){stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">${androidConfig.appName}</string>`);fs.writeFileSync(stringsFile,stringsContent);progress.log(`App display name configured: ${androidConfig.appName}`,"success");}else{stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">Catalyst Application</string>`);fs.writeFileSync(stringsFile,stringsContent);progress.log("App display name reverted to default: Catalyst Application","info");}}catch(error){progress.log(`Warning: Error configuring app name: ${error.message}`,"warning");}}// ── Notification helpers ──────────────────────────────────────────────────
10
+ async function handleGoogleServicesJson(){try{const rootGoogleServicesPath=`${process.cwd()}/google-services.json`;const androidGoogleServicesPath=`${pwd}/androidProject/app/google-services.json`;if(fs.existsSync(rootGoogleServicesPath)){progress.log("Found google-services.json in root directory","info");const appDir=`${pwd}/androidProject/app`;if(!fs.existsSync(appDir)){fs.mkdirSync(appDir,{recursive:true});}fs.copyFileSync(rootGoogleServicesPath,androidGoogleServicesPath);progress.log("Copied google-services.json to androidProject/app/","success");return true;}else if(fs.existsSync(androidGoogleServicesPath)){progress.log("google-services.json already exists in androidProject/app/","info");return true;}else{progress.log("google-services.json not found - Firebase push notifications will not work","warning");progress.log("Place google-services.json in project root or src/native/androidProject/app/","info");return false;}}catch(error){progress.log(`Warning: Error handling google-services.json: ${error.message}`,"warning");return false;}}async function processNotifications(WEBVIEW_CONFIG){const hasNotificationConfig=!!WEBVIEW_CONFIG.notifications?.enabled;try{await writeGeneratedBuildProperties(hasNotificationConfig);await cleanupNotificationPermissions();await cleanupNotificationResources();await cleanupNotificationMetadata();await cleanupNotificationAssets();if(!hasNotificationConfig){progress.log("Notifications disabled - cleaned up notification configurations","info");return;}const hasGoogleServices=await handleGoogleServicesJson();if(!hasGoogleServices){progress.log("Continuing without Firebase - only local notifications will work","warning");}await addNotificationPermissions();await generateNotificationResources(WEBVIEW_CONFIG.notifications);await addNotificationMetadata();await processNotificationAssets();progress.log("Notification configuration completed successfully!","success");}catch(error){progress.log(`Warning: Error processing notifications: ${error.message}`,"warning");}}async function writeGeneratedBuildProperties(notificationsEnabled){try{const propertiesPath=`${pwd}/androidProject/app/catalyst-build.properties`;const propertiesContent=["# Generated by Catalyst. Used by Android Studio builds when -PconfigPath is not provided.",`notifications.enabled=${notificationsEnabled?"true":"false"}`,""].join("\n");fs.writeFileSync(propertiesPath,propertiesContent);progress.log("Updated generated Android build properties","success");}catch(error){throw new Error(`Failed to write generated Android build properties: ${error.message}`);}}async function addNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const permissionsXml=` <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
11
+ <uses-permission android:name="android.permission.VIBRATE" />
12
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
13
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />`;manifestContent=manifestContent.replace(/(<uses-permission[^>]*>[\s\S]*?)(\s*<uses-feature)/,`$1\n${permissionsXml}$2`);fs.writeFileSync(manifestPath,manifestContent);progress.log("Added notification permissions to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification permissions: ${error.message}`);}}async function generateNotificationResources(notificationConfig){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=fs.readFileSync(colorsPath,"utf8");const notificationColorXml=` <color name="notification_color">${notificationConfig.color||"#007AFF"}</color>`;if(!colorsContent.includes('name="notification_color"')){colorsContent=colorsContent.replace(/(<\/resources>)/,`${notificationColorXml}\n$1`);fs.writeFileSync(colorsPath,colorsContent);progress.log("Added notification color to colors.xml","success");}}catch(error){throw new Error(`Failed to generate notification resources: ${error.message}`);}}async function addNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const metadataXml=`
14
+ <!-- Default notification configuration -->
15
+ <meta-data
16
+ android:name="default_notification_channel_id"
17
+ android:value="default_notifications" />
18
+ <meta-data
19
+ android:name="default_notification_icon"
20
+ android:resource="@drawable/ic_notification" />
21
+ <meta-data
22
+ android:name="default_notification_color"
23
+ android:resource="@color/notification_color" />
24
+
25
+ <!-- Firebase default notification configuration -->
26
+ <meta-data
27
+ android:name="com.google.firebase.messaging.default_notification_channel_id"
28
+ android:value="default" />
29
+ <meta-data
30
+ android:name="com.google.firebase.messaging.default_notification_icon"
31
+ android:resource="@drawable/ic_notification" />
32
+ <meta-data
33
+ android:name="com.google.firebase.messaging.default_notification_sound"
34
+ android:resource="@raw/notification_sound_default" />
35
+
36
+ <!-- Push Notification Service -->
37
+ <service
38
+ android:name="io.yourname.androidproject.utils.PushNotificationUtils"
39
+ android:exported="false">
40
+ <intent-filter>
41
+ <action android:name="com.google.firebase.MESSAGING_EVENT" />
42
+ </intent-filter>
43
+ </service>`;manifestContent=manifestContent.replace(/(\s*<\/application>)/,`${metadataXml}\n$1`);fs.writeFileSync(manifestPath,manifestContent);progress.log("Added notification metadata and push notification service to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification metadata: ${error.message}`);}}async function processNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp"];const audioFormats=["mp3","wav","ogg"];let assetsProcessed=0;const notificationIcons=[{sourceName:"notification-icon",resourceName:"ic_notification"},{sourceName:"notification-large",resourceName:"ic_notification_large"}];const notificationSounds=[{sourceName:"notification-sound-default",resourceName:"notification_sound_default"},{sourceName:"notification-sound-urgent",resourceName:"notification_sound_urgent"}];for(const icon of notificationIcons){for(const format of imageFormats){const existingIconPath=`${destPath}/drawable/${icon.resourceName}.${format}`;if(fs.existsSync(existingIconPath)){fs.unlinkSync(existingIconPath);progress.log(`Removed existing ${icon.resourceName}.${format}`,"info");}}}for(const sound of notificationSounds){for(const format of audioFormats){const existingSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;if(fs.existsSync(existingSoundPath)){fs.unlinkSync(existingSoundPath);progress.log(`Removed existing ${sound.resourceName}.${format}`,"info");}}}let iconFound=false;for(const icon of notificationIcons){for(const format of imageFormats){const iconImagePath=`${publicPath}/${icon.sourceName}.${format}`;if(fs.existsSync(iconImagePath)){const destImagePath=`${destPath}/drawable/${icon.resourceName}.${format}`;fs.copyFileSync(iconImagePath,destImagePath);progress.log(`Notification icon copied: ${icon.sourceName}.${format} -> ${icon.resourceName}.${format}`,"success");assetsProcessed++;if(icon.sourceName==="notification-icon"){iconFound=true;}break;}}}if(!iconFound){generateNotificationIconDrawable("ic_notification",destPath);progress.log("Generated default notification icon","info");}const rawDir=`${destPath}/raw`;if(!fs.existsSync(rawDir)){fs.mkdirSync(rawDir,{recursive:true});}for(const sound of notificationSounds){for(const format of audioFormats){const soundImagePath=`${publicPath}/${sound.sourceName}.${format}`;if(fs.existsSync(soundImagePath)){const destSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;fs.copyFileSync(soundImagePath,destSoundPath);progress.log(`Notification sound copied: ${sound.sourceName}.${format} -> ${sound.resourceName}.${format}`,"success");assetsProcessed++;break;}}}if(assetsProcessed>0){progress.log(`Processed ${assetsProcessed} notification assets from public/`,"success");}else{progress.log("No notification assets found in public/ - using defaults","info");}}catch(error){throw new Error(`Failed to process notification assets: ${error.message}`);}}function generateNotificationIconDrawable(iconName,destPath){const iconXml=`<vector xmlns:android="http://schemas.android.com/apk/res/android"
44
+ android:width="24dp"
45
+ android:height="24dp"
46
+ android:viewportWidth="24"
47
+ android:viewportHeight="24"
48
+ android:tint="?attr/colorOnPrimary">
49
+ <path
50
+ android:fillColor="@android:color/white"
51
+ android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
52
+ </vector>`;fs.writeFileSync(`${destPath}/drawable/${iconName}.xml`,iconXml);}async function cleanupNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const notificationPermissions=["android.permission.POST_NOTIFICATIONS","android.permission.VIBRATE","android.permission.WAKE_LOCK","android.permission.RECEIVE_BOOT_COMPLETED"];manifestContent=manifestContent.split("\n").filter(line=>{return!notificationPermissions.some(permission=>line.includes(`<uses-permission android:name="${permission}"`));}).join("\n");fs.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification permissions: ${error.message}`,"warning");}}async function cleanupNotificationResources(){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=fs.readFileSync(colorsPath,"utf8");const existingColorLine=colorsContent.split("\n").find(line=>line.includes('name="notification_color"'));if(existingColorLine){colorsContent=colorsContent.replace(existingColorLine,"");fs.writeFileSync(colorsPath,colorsContent);}}catch(error){progress.log(`Warning: Error cleaning notification resources: ${error.message}`,"warning");}}async function cleanupNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const metadataNames=["default_notification_channel_id","default_notification_icon","default_notification_color","com.google.firebase.messaging.default_notification_channel_id","com.google.firebase.messaging.default_notification_icon","com.google.firebase.messaging.default_notification_sound"];for(const metadataName of metadataNames){const metadataRegex=new RegExp(`\\s*<meta-data\\s+[\\s\\S]*?android:name="${metadataName}"[\\s\\S]*?\\/>`,"gi");manifestContent=manifestContent.replace(metadataRegex,"");}const serviceRegex=/\s*<!--\s*Push Notification Service\s*-->\s*<service[^>]*android:name="[^"]*PushNotificationUtils"[\s\S]*?<\/service>/gi;manifestContent=manifestContent.replace(serviceRegex,"");manifestContent=manifestContent.replace(/\s*<!--\s*Default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Firebase default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Push Notification Service\s*-->/gi,"");fs.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification metadata: ${error.message}`,"warning");}}async function cleanupNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp","xml"];const audioFormats=["mp3","wav","ogg"];const notificationIcons=["ic_notification","ic_notification_large"];const notificationSounds=["notification_sound_default","notification_sound_urgent"];for(const icon of notificationIcons){for(const format of imageFormats){const iconPath=`${destPath}/drawable/${icon}.${format}`;if(fs.existsSync(iconPath)){fs.unlinkSync(iconPath);}}}const rawDir=`${destPath}/raw`;if(fs.existsSync(rawDir)){for(const sound of notificationSounds){for(const format of audioFormats){const soundPath=`${rawDir}/${sound}.${format}`;if(fs.existsSync(soundPath)){fs.unlinkSync(soundPath);}}}}}catch(error){progress.log(`Warning: Error cleaning notification assets: ${error.message}`,"warning");}}return{copyBuildAssets,copySplashscreenAssets,copyOfflinePage,copyIconAssets,configureAppName,processNotifications};}module.exports=createAssetsPhase;
@@ -0,0 +1,19 @@
1
+ "use strict";const fs=require("fs");const path=require("path");const{buildAndroidAAB}=require("../renameAndroidProject.js");const DEFAULT_DEPLOYMENT_PATH="./deployment";const DEFAULT_OLD_PROJECT_NAME="androidProject";const DEFAULT_OVERWRITE_EXISTING=true;function createBuildPhase(ctx){const{configPath,pwd,progress,ANDROID_PACKAGE}=ctx;async function detectPhysicalDevice(ADB_PATH){try{progress.log("Detecting physical devices...","info");// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
2
+ const devices=ctx.runCommand(`${ADB_PATH} devices -l`);const lines=devices.split("\n").filter(line=>line.trim()&&!line.includes("List of devices"));for(const line of lines){const parts=line.trim().split(/\s+/);if(parts.length>=2&&parts[1]==="device"){const deviceId=parts[0];if(!deviceId.startsWith("emulator-")){try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
3
+ const qemuCheck=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell getprop ro.kernel.qemu`);if(!qemuCheck.trim()){progress.log(`Physical device detected: ${deviceId}`,"success");let deviceModel="Unknown Device";try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
4
+ const model=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell getprop ro.product.model`);deviceModel=model.trim()||deviceModel;}catch(e){// Ignore model detection errors
5
+ }return{id:deviceId,model:deviceModel};}}catch(error){continue;}}}}progress.log("No physical devices detected","info");return null;}catch(error){progress.log("Error detecting physical devices: "+error.message,"error");return null;}}async function testPhysicalDeviceInstallation(ADB_PATH,deviceId){try{progress.log(`Testing installation capability on device ${deviceId}...`,"info");// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
6
+ const connectionTest=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell echo "test"`);if(!connectionTest.includes("test")){progress.log("Device connection test failed","error");return false;}try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
7
+ const developerOptions=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell settings get global development_settings_enabled`);if(developerOptions.trim()!=="1"){progress.log("Developer options not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify developer options status","warning");}try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
8
+ const usbDebugging=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell settings get global adb_enabled`);if(usbDebugging.trim()!=="1"){progress.log("USB debugging not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify USB debugging status","warning");}try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
9
+ ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell pm list packages -3 | head -1`);}catch(error){progress.log("Cannot access package manager on device","error");return false;}progress.log(`Device ${deviceId} is ready for installation`,"success");return true;}catch(error){progress.log(`Installation capability test failed: ${error.message}`,"error");return false;}}async function checkEmulator(ADB_PATH){try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
10
+ const devices=ctx.runCommand(`${ADB_PATH} devices`);return devices.includes("emulator");}catch(error){progress.log("Error checking emulator status: "+error.message,"error");return false;}}async function startEmulator(EMULATOR_PATH,androidConfig){progress.log(`Starting emulator: ${androidConfig.emulatorName}...`,"info");// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - EMULATOR_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
11
+ return ctx.runInteractiveCommand(EMULATOR_PATH,["-avd",androidConfig.emulatorName,"-read-only"],{}).then(()=>{progress.log("Emulator started successfully","success");}).catch(error=>{progress.log("Error starting emulator: "+error.message,"error");throw error;});}async function handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig){progress.log("Setting up emulator...","info");const emulatorRunning=await checkEmulator(ADB_PATH);if(!emulatorRunning){progress.log("No emulator running, attempting to start one...","info");await startEmulator(EMULATOR_PATH,androidConfig);await new Promise(resolve=>setTimeout(resolve,5000));}else{progress.log("Emulator already running","success");}return{type:"emulator",name:androidConfig.emulatorName};}async function buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice=null){progress.log("Building and installing app...","info");try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - pwd and configPath are internal resolved paths, not user input.
12
+ let buildCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath} -PbuildOptimisation=${buildOptimisation} && ./gradlew clean installDebug -PconfigPath=${configPath} --quiet --console=rich`;if(targetDevice&&targetDevice.type==="physical"){buildCommand=buildCommand.replace("installDebug",`installDebug -Pandroid.injected.target.device=${targetDevice.id}`);}await ctx.runInteractiveCommand("sh",["-c",buildCommand],{"BUILD SUCCESSFUL":""});progress.log("App build and installation completed successfully!","success");}catch(error){throw new Error("Error building/installing app: "+error.message);}}async function launchApp(ADB_PATH,buildType="debug",targetDevice=null){if(!targetDevice){progress.log("No target device specified, skipping launch","warning");return;}if(targetDevice.type==="physical"){progress.log(`App installed on physical device: ${targetDevice.model}`,"success");progress.log("Manual launch required - check your device to open the app","info");return;}try{progress.log("Launching app on emulator...","info");const packageName=`${ANDROID_PACKAGE}${buildType==="debug"?".debug":""}`;// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
13
+ const launchCommand=`${ADB_PATH} shell monkey -p ${packageName} 1`;await ctx.runInteractiveCommand("sh",["-c",launchCommand],{});progress.log("App launched successfully on emulator!","success");}catch(error){progress.log(`Warning: Could not auto-launch app: ${error.message}`,"warning");progress.log("App was installed successfully, but auto-launch failed","info");}}async function createAABConfig(androidConfig){const DEFAULT_PROJECT_PATH=`${pwd}/androidProject`;const aabConfig={projectPath:androidConfig.projectPath||DEFAULT_PROJECT_PATH,deploymentPath:DEFAULT_DEPLOYMENT_PATH,oldProjectName:DEFAULT_OLD_PROJECT_NAME,overwriteExisting:androidConfig.overwriteExisting!==undefined?androidConfig.overwriteExisting:DEFAULT_OVERWRITE_EXISTING,newProjectName:androidConfig.newProjectName||androidConfig.appName||androidConfig.packageName?.split(".").pop()||"catalystapp",packageName:androidConfig.packageName||null,createSignedAAB:true,outputPath:androidConfig.outputPath||`${process.cwd()}/build-output`};if(androidConfig.keystoreConfig){aabConfig.keystoreConfig=androidConfig.keystoreConfig;}else if(androidConfig.keystore){aabConfig.keystoreConfig={keyAlias:androidConfig.keystore.alias||"release",storePassword:androidConfig.keystore.storePassword,keyPassword:androidConfig.keystore.keyPassword,validityYears:25,organizationInfo:{companyName:androidConfig.keystore.organizationName||"YourCompany",city:androidConfig.keystore.city||"YourCity",state:androidConfig.keystore.state||"YourState",countryCode:androidConfig.keystore.countryCode||"US"}};}progress.log(`AAB Configuration:`,"info");progress.log(` Project Path: ${aabConfig.projectPath} ${aabConfig.projectPath===DEFAULT_PROJECT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Deployment Path: ${aabConfig.deploymentPath} ${aabConfig.deploymentPath===DEFAULT_DEPLOYMENT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Old Project Name: ${aabConfig.oldProjectName} ${aabConfig.oldProjectName===DEFAULT_OLD_PROJECT_NAME?"(default)":"(configured)"}`,"info");progress.log(` New Project Name: ${aabConfig.newProjectName}`,"info");progress.log(` Overwrite Existing: ${aabConfig.overwriteExisting} ${aabConfig.overwriteExisting===DEFAULT_OVERWRITE_EXISTING?"(default)":"(configured)"}`,"info");progress.log(` Output Path: ${aabConfig.outputPath}`,"info");return aabConfig;}async function moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,appName){try{if(!BUILD_OUTPUT_PATH){progress.log("BUILD_OUTPUT_PATH not set, skipping APK move","warning");return null;}const currentDate=new Date().toLocaleDateString("en-GB").replace(/\//g,"-");const currentTime=new Date().toLocaleTimeString("en-US",{hour12:true,hour:"2-digit",minute:"2-digit",second:"2-digit"}).replace(/:/g,"-");let destinationApkFileName="";if(appName){destinationApkFileName=buildType==="release"?`${appName}-${currentTime}.release.apk`:`${appName}-${currentTime}.debug.apk`;}else{destinationApkFileName=buildType==="release"?`app-${currentTime}.release.apk`:`app-${currentTime}.debug.apk`;}const sourceApkFileName=buildType==="release"?`app.apk`:`app-debug.apk`;const sourceApkPath=path.join(pwd,// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pwd is an internal resolved path.
14
+ "androidProject","app","build","outputs","apk",buildType,// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - buildType is "debug" or "release" from config.
15
+ sourceApkFileName);const destinationDir=path.join(process.cwd(),BUILD_OUTPUT_PATH,// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - BUILD_OUTPUT_PATH is trusted application config.
16
+ "native","android",currentDate,buildType// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - buildType is "debug" or "release" from config.
17
+ );const destinationApkPath=path.join(destinationDir,destinationApkFileName);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destinationDir and destinationApkFileName are derived from internal config values, not user input.
18
+ if(!fs.existsSync(sourceApkPath)){progress.log(`APK not found at source path: ${sourceApkPath}`,"warning");return null;}if(!fs.existsSync(destinationDir)){fs.mkdirSync(destinationDir,{recursive:true});}fs.copyFileSync(sourceApkPath,destinationApkPath);return destinationApkPath;}catch(error){progress.log(`Error moving APK: ${error.message}`,"error");return null;}}async function buildSignedAAB(androidConfig){progress.log("Building signed AAB for release...","info");try{progress.log("Generating webview configuration for release...","info");try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - pwd and configPath are internal resolved paths, not user input.
19
+ const generateConfigCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath}`;await ctx.runInteractiveCommand("sh",["-c",generateConfigCommand],{"BUILD SUCCESSFUL":""});progress.log("Webview config generated successfully","success");}catch(configError){progress.log(`Warning: Webview config generation failed: ${configError.message}`,"warning");throw new Error("Webview config generation is required for release builds");}const aabConfig=await createAABConfig(androidConfig);await buildAndroidAAB(aabConfig);progress.log("Signed AAB build completed successfully!","success");}catch(error){throw new Error("Error building signed AAB: "+error.message);}}return{detectPhysicalDevice,testPhysicalDeviceInstallation,checkEmulator,handleEmulatorSetup,startEmulator,buildApp,launchApp,createAABConfig,moveApkToOutputPath,buildSignedAAB};}module.exports=createBuildPhase;
@@ -0,0 +1 @@
1
+ "use strict";const fs=require("fs");function createConfigPhase(ctx){const{configPath,progress}=ctx;async function initializeConfig(){const configFile=fs.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");}const buildType=WEBVIEW_CONFIG.android.buildType||"debug";progress.log(`Build Type: ${buildType}`,"info");if(buildType==="release"){progress.log("Release build detected - AAB will be generated","info");}return{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH};}function validateAndroidTools(androidConfig){const ANDROID_SDK=androidConfig.sdkPath;const ADB_PATH=`${ANDROID_SDK}/platform-tools/adb`;const EMULATOR_PATH=`${ANDROID_SDK}/emulator/emulator`;progress.log("Validating Android tools...","info");if(!ANDROID_SDK){throw new Error("Android SDK path is not configured");}if(!fs.existsSync(ANDROID_SDK)){throw new Error(`Android SDK path does not exist: ${ANDROID_SDK}`);}if(!fs.existsSync(ADB_PATH)){throw new Error(`ADB not found at: ${ADB_PATH}`);}try{ctx.runCommand(`${ADB_PATH} version`);progress.log("ADB validation successful","success");}catch(error){throw new Error(`ADB is not working properly: ${error.message}`);}const buildType=androidConfig.buildType||"debug";if(buildType!=="release"){if(!fs.existsSync(EMULATOR_PATH)){throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);}try{ctx.runCommand(`${EMULATOR_PATH} -version`);progress.log("Emulator validation successful","success");}catch(error){throw new Error(`Emulator is not working properly: ${error.message}`);}try{const avdOutput=ctx.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}`);}}else{progress.log("Skipping emulator validation for release build","info");}progress.log("Android tools validation completed successfully!","success");return{ANDROID_SDK,ADB_PATH,EMULATOR_PATH};}return{initializeConfig,validateAndroidTools};}module.exports=createConfigPhase;
@@ -0,0 +1 @@
1
+ "use strict";const path=require("path");const{runCommand,runInteractiveCommand}=require("../utils.js");const TerminalProgress=require("../TerminalProgress.js").default;const{composeAndroidPlugins}=require("../pluginComposerAndroid.js");const{resolvePluginConfig,resolveInternalPluginsRoot}=require("../internalPluginUtils.js");const createConfigPhase=require("./config.js");const createAssetsPhase=require("./assets.js");const createBuildPhase=require("./build.js");const catalystCorePath=path.dirname(require.resolve("catalyst-core/package.json"));const pwd=path.join(catalystCorePath,"dist/native");const ANDROID_PACKAGE="io.yourname.androidproject";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};function createAndroidBuild(config){const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=config;const configPath=`${process.cwd()}/config/config.json`;const publicPath=`${process.cwd()}/public`;const progress=new TerminalProgress(steps,"Catalyst Android Build",progressConfig);const ctx={WEBVIEW_CONFIG,BUILD_OUTPUT_PATH,configPath,publicPath,pwd,progress,ANDROID_PACKAGE,runCommand,runInteractiveCommand};const{initializeConfig,validateAndroidTools}=createConfigPhase(ctx);const{copyBuildAssets,copySplashscreenAssets,copyOfflinePage,copyIconAssets,configureAppName,processNotifications}=createAssetsPhase(ctx);const{detectPhysicalDevice,testPhysicalDeviceInstallation,handleEmulatorSetup,buildApp,launchApp,moveApkToOutputPath,buildSignedAAB}=createBuildPhase(ctx);async function buildAndroidApp(){let androidConfig=null;let targetDevice=null;try{progress.start("config");const{WEBVIEW_CONFIG:wvConfig,BUILD_OUTPUT_PATH:bop}=await initializeConfig();androidConfig=wvConfig.android;const buildType=androidConfig.buildType||"debug";const buildOptimisation=!!androidConfig.buildOptimisation||false;progress.complete("config");progress.start("tools");const{ADB_PATH,EMULATOR_PATH}=validateAndroidTools(androidConfig);progress.complete("tools");if(buildType!=="release"){progress.start("emulator");const physicalDevice=await detectPhysicalDevice(ADB_PATH);if(physicalDevice){progress.log(`Found physical device: ${physicalDevice.model} (${physicalDevice.id})`,"success");const canInstallOnPhysical=await testPhysicalDeviceInstallation(ADB_PATH,physicalDevice.id);if(canInstallOnPhysical){targetDevice={type:"physical",id:physicalDevice.id,model:physicalDevice.model};progress.log(`Using physical device: ${physicalDevice.model}`,"success");}else{progress.log("Physical device installation test failed, falling back to emulator","warning");targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}}else{targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}progress.complete("emulator");}else{progress.log("Skipping device setup for release build","info");}progress.start("copyAssets");await copyBuildAssets(androidConfig,buildOptimisation);await copySplashscreenAssets();await copyOfflinePage();await copyIconAssets();await configureAppName(androidConfig);const pluginConfig=resolvePluginConfig(wvConfig);composeAndroidPlugins({corePluginsRoot:resolveInternalPluginsRoot(catalystCorePath),androidProjectPath:`${pwd}/androidProject`,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await processNotifications(wvConfig);progress.log(`Build optimization: ${buildOptimisation?"Enabled":"Disabled"}`,"info");progress.complete("copyAssets");let movedApkPath=null;if(buildType==="release"){progress.start("aab");await buildSignedAAB(androidConfig);progress.complete("aab");movedApkPath=await moveApkToOutputPath(buildType,bop,androidConfig.appName);}else{progress.start("build");await buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice);await launchApp(ADB_PATH,buildType,targetDevice);progress.complete("build");movedApkPath=await moveApkToOutputPath(buildType,bop,androidConfig.appName);}const summaryItems=["Build completed successfully:",{text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`SDK Path: ${androidConfig.sdkPath}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Build Optimization: ${buildOptimisation?"Enabled":"Disabled"}`,indent:1,prefix:"├─ ",color:"gray"}];if(buildType==="release"){if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}summaryItems.push({text:`Output: Signed AAB generated in build-output/`,indent:1,prefix:"└─ ",color:"green"});}else{if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}if(targetDevice&&targetDevice.type==="physical"){summaryItems.push({text:`Target Device: ${targetDevice.model} (Physical)`,indent:1,prefix:"└─ ",color:"green"});}else{summaryItems.push({text:`Target Device: ${androidConfig.emulatorName} (Emulator)`,indent:1,prefix:"└─ ",color:"gray"});}}progress.printTreeContent("Build Summary",summaryItems);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);const troubleshootingItems=["Build failed. Please try the following steps:",{text:"Check if Android SDK is properly configured",indent:1,prefix:"├─ ",color:"yellow"},{text:"Verify build assets exist in the source directory",indent:1,prefix:"├─ ",color:"yellow"}];const buildType=androidConfig?.buildType||"debug";if(buildType!=="release"){if(error.message.includes("physical device")||error.message.includes("installation test")){troubleshootingItems.push("\nPhysical Device Issues:",{text:"Enable Developer Options on your device",indent:1,prefix:"├─ ",color:"yellow"},{text:"Enable USB Debugging in Developer Options",indent:1,prefix:"├─ ",color:"yellow"},{text:'Accept the "Allow USB Debugging" prompt on your device',indent:1,prefix:"├─ ",color:"yellow"},{text:"Try disconnecting and reconnecting your device",indent:1,prefix:"├─ ",color:"yellow"},{text:'Check if "adb devices" shows your device as authorized',indent:1,prefix:"├─ ",color:"yellow"});}troubleshootingItems.push({text:"Verify that the emulator exists and is working",indent:1,prefix:"├─ ",color:"yellow"},{text:"If using physical device, ensure it stays connected",indent:1,prefix:"├─ ",color:"yellow"});}else{troubleshootingItems.push({text:"Verify keystore configuration for release builds",indent:1,prefix:"├─ ",color:"yellow"},{text:"Check that keystore passwords are properly set",indent:1,prefix:"├─ ",color:"yellow"});}troubleshootingItems.push({text:'Run "npm run setupEmulator:android" to reconfigure Android settings',indent:1,prefix:"└─ ",color:"yellow"},"\nVerify Configuration:");if(androidConfig){troubleshootingItems.push({text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Android SDK Path: ${androidConfig.sdkPath||"Not configured"}`,indent:1,prefix:"├─ ",color:"gray"});if(buildType!=="release"){if(targetDevice){troubleshootingItems.push({text:`Target Device: ${targetDevice.type==="physical"?`${targetDevice.model} (Physical)`:`${androidConfig.emulatorName} (Emulator)`}`,indent:1,prefix:"└─ ",color:"gray"});}else{troubleshootingItems.push({text:`Selected Emulator: ${androidConfig.emulatorName||"Not configured"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:`Output Path: ${androidConfig.outputPath||"build-output/"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:"Configuration could not be loaded",indent:1,prefix:"├─ ",color:"red"},{text:"Check if config/config.json exists and has valid Android configuration",indent:1,prefix:"└─ ",color:"red"});}progress.printTreeContent("Troubleshooting Guide",troubleshootingItems);}throw error;}}async function buildAndroidForTesting(){try{const{WEBVIEW_CONFIG:wvConfig}=await initializeConfig();const androidConfig=wvConfig.android;const buildOptimisation=!!androidConfig.buildOptimisation||false;await copyBuildAssets(androidConfig,buildOptimisation);await copySplashscreenAssets();await copyOfflinePage();await copyIconAssets();await configureAppName(androidConfig);const pluginConfig=resolvePluginConfig(wvConfig);composeAndroidPlugins({corePluginsRoot:resolveInternalPluginsRoot(catalystCorePath),androidProjectPath:`${pwd}/androidProject`,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await processNotifications(wvConfig);progress.log("✅ buildAndroidForTesting complete — project ready for gradlew test","success");return{success:true};}catch(error){progress.log("buildAndroidForTesting failed: "+error.message,"error");throw error;}}return{buildAndroidApp,buildAndroidForTesting,progress,WEBVIEW_CONFIG};}module.exports={createAndroidBuild,pwd};
@@ -1,150 +1,2 @@
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
2
- function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}const configPath=`${process.env.PWD}/config/config.json`;const publicPath=`${process.env.PWD}/public`;const catalystCorePath=_path.default.dirname(require.resolve("catalyst-core/package.json"));const pwd=_path.default.join(catalystCorePath,"dist/native");const ANDROID_PACKAGE="io.yourname.androidproject";// Default values for AAB building
3
- const DEFAULT_PROJECT_PATH=`${pwd}/androidProject`;const DEFAULT_DEPLOYMENT_PATH="./deployment";const DEFAULT_OLD_PROJECT_NAME="androidProject";// Use actual project name in catalyst
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
5
- const buildType=WEBVIEW_CONFIG.android.buildType||"debug";progress.log(`Build Type: ${buildType}`,"info");if(buildType==="release"){progress.log("Release build detected - AAB will be generated","info");}return{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH};}function validateAndroidTools(androidConfig){const ANDROID_SDK=androidConfig.sdkPath;const ADB_PATH=`${ANDROID_SDK}/platform-tools/adb`;const EMULATOR_PATH=`${ANDROID_SDK}/emulator/emulator`;progress.log("Validating Android tools...","info");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 validation successful","success");}catch(error){throw new Error(`ADB is not working properly: ${error.message}`);}// Skip emulator validation for release builds
6
- const buildType=androidConfig.buildType||"debug";if(buildType!=="release"){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 validation successful","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}`);}}else{progress.log("Skipping emulator validation for release build","info");}progress.log("Android tools validation completed successfully!","success");return{ANDROID_SDK,ADB_PATH,EMULATOR_PATH};}async function detectPhysicalDevice(ADB_PATH){try{progress.log("Detecting physical devices...","info");const devices=(0,_utils.runCommand)(`${ADB_PATH} devices -l`);const lines=devices.split("\n").filter(line=>line.trim()&&!line.includes("List of devices"));for(const line of lines){const parts=line.trim().split(/\s+/);if(parts.length>=2&&parts[1]==="device"){const deviceId=parts[0];// Check if it's a physical device (not emulator)
7
- if(!deviceId.startsWith("emulator-")){try{// Double-check it's not an emulator using device properties
8
- const qemuCheck=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell getprop ro.kernel.qemu`);if(!qemuCheck.trim()){// Empty means physical device
9
- progress.log(`Physical device detected: ${deviceId}`,"success");// Get device model/name for better logging
10
- let deviceModel="Unknown Device";try{const model=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell getprop ro.product.model`);deviceModel=model.trim()||deviceModel;}catch(e){// Ignore model detection errors
11
- }return{id:deviceId,model:deviceModel};}}catch(error){// If we can't check properties, skip this device
12
- continue;}}}}progress.log("No physical devices detected","info");return null;}catch(error){progress.log("Error detecting physical devices: "+error.message,"error");return null;}}async function testPhysicalDeviceInstallation(ADB_PATH,deviceId){try{progress.log(`Testing installation capability on device ${deviceId}...`,"info");// Test 1: Basic ADB connection
13
- const connectionTest=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell echo "test"`);if(!connectionTest.includes("test")){progress.log("Device connection test failed","error");return false;}// Test 2: Check if developer options are enabled
14
- try{const developerOptions=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell settings get global development_settings_enabled`);if(developerOptions.trim()!=="1"){progress.log("Developer options not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify developer options status","warning");}// Test 3: Check USB debugging status
15
- try{const usbDebugging=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell settings get global adb_enabled`);if(usbDebugging.trim()!=="1"){progress.log("USB debugging not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify USB debugging status","warning");}// Test 4: Check if we can access package manager
16
- try{;(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell pm list packages -3 | head -1`);}catch(error){progress.log("Cannot access package manager on device","error");return false;}progress.log(`Device ${deviceId} is ready for installation`,"success");return true;}catch(error){progress.log(`Installation capability test failed: ${error.message}`,"error");return false;}}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 status: "+error.message,"error");return false;}}async function handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig){progress.log("Setting up emulator...","info");const emulatorRunning=await checkEmulator(ADB_PATH);if(!emulatorRunning){progress.log("No emulator running, attempting to start one...","info");await startEmulator(EMULATOR_PATH,androidConfig);await new Promise(resolve=>setTimeout(resolve,5000));}else{progress.log("Emulator already running","success");}return{type:"emulator",name:androidConfig.emulatorName};}async function startEmulator(EMULATOR_PATH,androidConfig){progress.log(`Starting emulator: ${androidConfig.emulatorName}...`,"info");return new Promise((resolve,reject)=>{;(0,_child_process.exec)(`${EMULATOR_PATH} -avd ${androidConfig.emulatorName} -read-only > /dev/null &`,error=>{if(error){progress.log("Error starting emulator: "+error.message,"error");reject(error);}else{progress.log("Emulator started successfully","success");resolve();}});});}async function copyBuildAssets(androidConfig,buildOptimisation=false){if(!buildOptimisation)return;progress.log("Copying build assets to Android project...","info");try{// Define source and destination paths
17
- const sourcePath=`${process.env.PWD}/build/`;const destPath=`${pwd}/androidProject/app/src/main/assets/build/`// Create destination directory if it doesn't exist
18
- ;(0,_utils.runCommand)(`mkdir -p ${destPath}`)// Clear existing destination to avoid conflicts
19
- ;(0,_utils.runCommand)(`rm -rf ${destPath}/*`);// Files to exclude from copying
20
- const excludePatterns=["route-manifest.json.gz","route-manifest.json.br"];if(buildOptimisation){progress.log("Running with build optimization...","info");const excludeParams=excludePatterns.map(pattern=>`--exclude="${pattern}"`).join(" ");const rsyncCommand=`rsync -av ${excludeParams} ${sourcePath} ${destPath}`;progress.log("Executing rsync command with exclusions...","info");(0,_utils.runCommand)(rsyncCommand);// Verify excluded files don't exist in destination
21
- for(const pattern of excludePatterns){const checkCommand=`find ${destPath} -name "${pattern}" | wc -l`;const count=parseInt((0,_utils.runCommand)(checkCommand).trim(),10);if(count>0){progress.log(`Warning: Found ${count} instances of excluded file ${pattern}`,"warning");(0,_utils.runCommand)(`find ${destPath} -name "${pattern}" -delete`);}}progress.log("Build assets copied with optimization (excluded route-manifest JSON files)","success");}else{progress.log("Running without build optimization...","info");const exclusions=excludePatterns.map(pattern=>`-not -name "${pattern}"`).join(" ");const copyCommand=`find ${sourcePath} -type f ${exclusions} -exec cp --parents {} ${destPath} \\;`;progress.log(`Executing copy command with exclusions...`,"info");(0,_utils.runCommand)(copyCommand);progress.log("Build assets copied successfully!","success");}}catch(error){throw new Error("Error copying build assets: "+error.message);}}async function buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice=null){progress.log("Building and installing app...","info");try{// Build command without monkey launch
22
- let buildCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath} -PbuildOptimisation=${buildOptimisation} && ./gradlew clean installDebug -PconfigPath=${configPath} --quiet --console=rich`;// Add device-specific install target if physical device
23
- if(targetDevice&&targetDevice.type==="physical"){buildCommand=buildCommand.replace("installDebug",`installDebug -Pandroid.injected.target.device=${targetDevice.id}`);}await(0,_utils.runInteractiveCommand)("sh",["-c",buildCommand],{"BUILD SUCCESSFUL":""});progress.log("App build and installation completed successfully!","success");}catch(error){throw new Error("Error building/installing app: "+error.message);}}async function launchApp(ADB_PATH,buildType="debug",targetDevice=null){if(!targetDevice){progress.log("No target device specified, skipping launch","warning");return;}if(targetDevice.type==="physical"){progress.log(`App installed on physical device: ${targetDevice.model}`,"success");progress.log("Manual launch required - check your device to open the app","info");return;}// Launch on emulator
24
- try{progress.log("Launching app on emulator...","info");const packageName=`${ANDROID_PACKAGE}${buildType==="debug"?".debug":""}`;const launchCommand=`${ADB_PATH} shell monkey -p ${packageName} 1`;await(0,_utils.runInteractiveCommand)("sh",["-c",launchCommand],{});progress.log("App launched successfully on emulator!","success");}catch(error){progress.log(`Warning: Could not auto-launch app: ${error.message}`,"warning");progress.log("App was installed successfully, but auto-launch failed","info");}}async function copySplashscreenAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG={}}=config;if(!WEBVIEW_CONFIG.splashScreen)return;const androidPublicPath=`${process.env.PWD}/public/android`;const imageFormats=["png","jpg","jpeg","gif","bmp","webp"];// Copy splash screen image if exists
25
- if(_fs.default.existsSync(androidPublicPath)){const drawableDir=`${destPath}/drawable`;if(!_fs.default.existsSync(drawableDir)){_fs.default.mkdirSync(drawableDir,{recursive:true});}for(const format of imageFormats){const existingPath=`${destPath}/drawable/splashscreen.${format}`;if(_fs.default.existsSync(existingPath)){_fs.default.unlinkSync(existingPath);}}for(const format of imageFormats){const sourcePath=`${androidPublicPath}/splashscreen.${format}`;if(_fs.default.existsSync(sourcePath)){_fs.default.copyFileSync(sourcePath,`${destPath}/drawable/splashscreen.${format}`);break;}}}// Update theme background color
26
- const backgroundColor=WEBVIEW_CONFIG.splashScreen.backgroundColor||"#ffffff";const themeFiles=[`${destPath}/values/themes.xml`,`${destPath}/values-night/themes.xml`];for(const themesFile of themeFiles){if(_fs.default.existsSync(themesFile)){let content=_fs.default.readFileSync(themesFile,"utf8");// Replace windowBackground - matches any color value
27
- content=content.replace(/<item name="android:windowBackground">.*?<\/item>/,`<item name="android:windowBackground">${backgroundColor}</item>`);// Replace splash screen background - matches any color value
28
- content=content.replace(/<item name="android:windowSplashScreenBackground" tools:targetApi="31">.*?<\/item>/,`<item name="android:windowSplashScreenBackground" tools:targetApi="31">${backgroundColor}</item>`);_fs.default.writeFileSync(themesFile,content);}}}catch(error){progress.log(`Error processing splash screen: ${error.message}`,"warning");}}async function copyOfflinePage(){try{const sourcePath=`${process.env.PWD}/public/offline.html`;const destDir=`${pwd}/androidProject/app/src/main/assets/offline`;const destPath=`${destDir}/offline.html`;if(!_fs.default.existsSync(sourcePath)){progress.log("offline.html not found in public/; skipping offline asset copy","warning");return;}// Ensure destination directory exists
29
- ;(0,_utils.runCommand)(`mkdir -p ${destDir}`);_fs.default.copyFileSync(sourcePath,destPath);progress.log("offline.html copied to Android assets","success");}catch(error){progress.log(`Warning: Error copying offline.html: ${error.message}`,"warning");}}async function copyIconAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;const androidIconDir=_path.default.join(publicPath,"android","appIcons");const fallbackIconPath=_path.default.join(__dirname,"assets","catalyst.png");const fallbackExists=_fs.default.existsSync(fallbackIconPath);const extensions=["png","jpg","jpeg"];if(!_fs.default.existsSync(publicPath)){progress.log(`Warning: Public directory not found at ${publicPath}`,"warning");return;}const densities=[{key:"mdpi",dir:"mipmap-mdpi"},{key:"hdpi",dir:"mipmap-hdpi"},{key:"xhdpi",dir:"mipmap-xhdpi"},{key:"xxhdpi",dir:"mipmap-xxhdpi"},{key:"xxxhdpi",dir:"mipmap-xxxhdpi"}];const hasIconDirectory=_fs.default.existsSync(androidIconDir)&&_fs.default.lstatSync(androidIconDir).isDirectory();const cleanDensityDir=dir=>{if(!_fs.default.existsSync(dir)){return;}for(const file of _fs.default.readdirSync(dir)){if(file.startsWith("icon.")){_fs.default.unlinkSync(_path.default.join(dir,file));}}};const findIconForDensity=densityKey=>{if(!hasIconDirectory){return null;}for(const ext of extensions){const candidate=_path.default.join(androidIconDir,`icon-${densityKey}.${ext}`);if(_fs.default.existsSync(candidate)){return{path:candidate,ext};}}return null;};let hasCustomIcons=false;let usedFallback=false;for(const density of densities){const targetDir=_path.default.join(destPath,density.dir);const source=findIconForDensity(density.key);cleanDensityDir(targetDir);if(source){_fs.default.mkdirSync(targetDir,{recursive:true});const destination=_path.default.join(targetDir,`icon.${source.ext}`);_fs.default.copyFileSync(source.path,destination);hasCustomIcons=true;}}if(!hasCustomIcons&&fallbackExists){const targetDir=_path.default.join(destPath,"mipmap-xxxhdpi");_fs.default.mkdirSync(targetDir,{recursive:true});cleanDensityDir(targetDir);const destination=_path.default.join(targetDir,"icon.png");_fs.default.copyFileSync(fallbackIconPath,destination);usedFallback=true;}const setManifestIcons=(iconValue,roundIconValue)=>{if(!_fs.default.existsSync(manifestPath))return;const current=_fs.default.readFileSync(manifestPath,"utf8");const updated=current.replace(/android:icon="[^"]*"/g,`android:icon="${iconValue}"`).replace(/android:roundIcon="[^"]*"/g,`android:roundIcon="${roundIconValue}"`);if(updated!==current){_fs.default.writeFileSync(manifestPath,updated);}};if(!hasCustomIcons&&!usedFallback){progress.log("No custom Android icons found; using default template icons.","info");setManifestIcons("@mipmap/ic_launcher","@mipmap/ic_launcher_round");return;}setManifestIcons("@mipmap/icon","@mipmap/icon");if(hasCustomIcons){progress.log("Applied Android launcher icons from public/android/appIcons.","success");}if(usedFallback){progress.log("Used bundled Catalyst fallback icon for launcher.","info");}}catch(error){progress.log(`Warning: Error copying app icon assets: ${error.message}`,"warning");}}async function configureAppName(androidConfig){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const stringsFile=`${destPath}/values/strings.xml`;let stringsContent=_fs.default.readFileSync(stringsFile,"utf8");// Check if androidConfig.appName exists
30
- if(androidConfig.appName){stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">${androidConfig.appName}</string>`);_fs.default.writeFileSync(stringsFile,stringsContent);progress.log(`App display name configured: ${androidConfig.appName}`,"success");}else{// No appName configured, revert to default
31
- stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">Catalyst Application</string>`);_fs.default.writeFileSync(stringsFile,stringsContent);progress.log("App display name reverted to default: Catalyst Application","info");}}catch(error){progress.log(`Warning: Error configuring app name: ${error.message}`,"warning");}}async function handleGoogleServicesJson(){try{const rootGoogleServicesPath=`${process.env.PWD}/google-services.json`;const androidGoogleServicesPath=`${pwd}/androidProject/app/google-services.json`;// Check if google-services.json exists in the root directory
32
- if(_fs.default.existsSync(rootGoogleServicesPath)){progress.log("Found google-services.json in root directory","info");// Create the app directory if it doesn't exist
33
- const appDir=`${pwd}/androidProject/app`;if(!_fs.default.existsSync(appDir)){_fs.default.mkdirSync(appDir,{recursive:true});}// Copy the file to the Android project
34
- _fs.default.copyFileSync(rootGoogleServicesPath,androidGoogleServicesPath);progress.log("Copied google-services.json to androidProject/app/","success");return true;}else if(_fs.default.existsSync(androidGoogleServicesPath)){progress.log("google-services.json already exists in androidProject/app/","info");return true;}else{progress.log("google-services.json not found - Firebase push notifications will not work","warning");progress.log("Place google-services.json in project root or src/native/androidProject/app/","info");return false;}}catch(error){progress.log(`Warning: Error handling google-services.json: ${error.message}`,"warning");return false;}}async function processNotifications(WEBVIEW_CONFIG){const hasNotificationConfig=!!WEBVIEW_CONFIG.notifications?.enabled;try{// Always clean up notification configurations first (regardless of config)
35
- await cleanupNotificationPermissions();await cleanupNotificationResources();await cleanupNotificationMetadata();await cleanupNotificationAssets();if(!hasNotificationConfig){progress.log("Notifications disabled - cleaned up notification configurations","info");return;}// Handle google-services.json file for Firebase
36
- const hasGoogleServices=await handleGoogleServicesJson();if(!hasGoogleServices){progress.log("Continuing without Firebase - only local notifications will work","warning");}// Only add configurations if notifications are enabled
37
- await addNotificationPermissions();await generateNotificationResources(WEBVIEW_CONFIG.notifications);await addNotificationMetadata();await processNotificationAssets();progress.log("Notification configuration completed successfully!","success");}catch(error){progress.log(`Warning: Error processing notifications: ${error.message}`,"warning");}}async function addNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");const permissionsXml=` <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
38
- <uses-permission android:name="android.permission.VIBRATE" />
39
- <uses-permission android:name="android.permission.WAKE_LOCK" />
40
- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />`;manifestContent=manifestContent.replace(/(<uses-permission[^>]*>[\s\S]*?)(\s*<uses-feature)/,`$1\n${permissionsXml}$2`);_fs.default.writeFileSync(manifestPath,manifestContent);progress.log("Added notification permissions to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification permissions: ${error.message}`);}}async function generateNotificationResources(notificationConfig){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=_fs.default.readFileSync(colorsPath,"utf8");const notificationColorXml=` <color name="notification_color">${notificationConfig.color||"#007AFF"}</color>`;if(!colorsContent.includes('name="notification_color"')){colorsContent=colorsContent.replace(/(<\/resources>)/,`${notificationColorXml}\n$1`);_fs.default.writeFileSync(colorsPath,colorsContent);progress.log("Added notification color to colors.xml","success");}}catch(error){throw new Error(`Failed to generate notification resources: ${error.message}`);}}async function addNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");const metadataXml=`
41
- <!-- Default notification configuration -->
42
- <meta-data
43
- android:name="default_notification_channel_id"
44
- android:value="default_notifications" />
45
- <meta-data
46
- android:name="default_notification_icon"
47
- android:resource="@drawable/ic_notification" />
48
- <meta-data
49
- android:name="default_notification_color"
50
- android:resource="@color/notification_color" />
51
-
52
- <!-- Firebase default notification configuration -->
53
- <meta-data
54
- android:name="com.google.firebase.messaging.default_notification_channel_id"
55
- android:value="default" />
56
- <meta-data
57
- android:name="com.google.firebase.messaging.default_notification_icon"
58
- android:resource="@drawable/ic_notification" />
59
- <meta-data
60
- android:name="com.google.firebase.messaging.default_notification_sound"
61
- android:resource="@raw/notification_sound_default" />
62
-
63
- <!-- Push Notification Service -->
64
- <service
65
- android:name="io.yourname.androidproject.utils.PushNotificationUtils"
66
- android:exported="false">
67
- <intent-filter>
68
- <action android:name="com.google.firebase.MESSAGING_EVENT" />
69
- </intent-filter>
70
- </service>`;manifestContent=manifestContent.replace(/(\s*<\/application>)/,`${metadataXml}\n$1`);_fs.default.writeFileSync(manifestPath,manifestContent);progress.log("Added notification metadata and push notification service to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification metadata: ${error.message}`);}}async function processNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp"];const audioFormats=["mp3","wav","ogg"];let assetsProcessed=0;// Define notification icons to process
71
- const notificationIcons=[{sourceName:"notification-icon",resourceName:"ic_notification"},{sourceName:"notification-large",resourceName:"ic_notification_large"}];// Define notification sounds to process
72
- const notificationSounds=[{sourceName:"notification-sound-default",resourceName:"notification_sound_default"},{sourceName:"notification-sound-urgent",resourceName:"notification_sound_urgent"}];// Remove existing notification icons to avoid conflicts
73
- for(const icon of notificationIcons){for(const format of imageFormats){const existingIconPath=`${destPath}/drawable/${icon.resourceName}.${format}`;if(_fs.default.existsSync(existingIconPath)){_fs.default.unlinkSync(existingIconPath);progress.log(`Removed existing ${icon.resourceName}.${format}`,"info");}}}// Remove existing notification sounds to avoid conflicts
74
- for(const sound of notificationSounds){for(const format of audioFormats){const existingSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;if(_fs.default.existsSync(existingSoundPath)){_fs.default.unlinkSync(existingSoundPath);progress.log(`Removed existing ${sound.resourceName}.${format}`,"info");}}}// Process notification icons
75
- let iconFound=false;for(const icon of notificationIcons){for(const format of imageFormats){const iconImagePath=`${publicPath}/${icon.sourceName}.${format}`;if(_fs.default.existsSync(iconImagePath)){const destImagePath=`${destPath}/drawable/${icon.resourceName}.${format}`;_fs.default.copyFileSync(iconImagePath,destImagePath);progress.log(`Notification icon copied: ${icon.sourceName}.${format} -> ${icon.resourceName}.${format}`,"success");assetsProcessed++;// Track if we found the main notification icon
76
- if(icon.sourceName==="notification-icon"){iconFound=true;}break;}}}// Generate default notification icon if not found
77
- if(!iconFound){generateNotificationIconDrawable("ic_notification",destPath);progress.log("Generated default notification icon","info");}// Create raw directory if it doesn't exist
78
- const rawDir=`${destPath}/raw`;if(!_fs.default.existsSync(rawDir)){_fs.default.mkdirSync(rawDir,{recursive:true});}// Process notification sounds
79
- for(const sound of notificationSounds){for(const format of audioFormats){const soundImagePath=`${publicPath}/${sound.sourceName}.${format}`;if(_fs.default.existsSync(soundImagePath)){const destSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;_fs.default.copyFileSync(soundImagePath,destSoundPath);progress.log(`Notification sound copied: ${sound.sourceName}.${format} -> ${sound.resourceName}.${format}`,"success");assetsProcessed++;break;}}}if(assetsProcessed>0){progress.log(`Processed ${assetsProcessed} notification assets from public/`,"success");}else{progress.log("No notification assets found in public/ - using defaults","info");}}catch(error){throw new Error(`Failed to process notification assets: ${error.message}`);}}function generateNotificationIconDrawable(iconName,destPath){// Create vector drawable for notification icon as fallback
80
- const iconXml=`<vector xmlns:android="http://schemas.android.com/apk/res/android"
81
- android:width="24dp"
82
- android:height="24dp"
83
- android:viewportWidth="24"
84
- android:viewportHeight="24"
85
- android:tint="?attr/colorOnPrimary">
86
- <path
87
- android:fillColor="@android:color/white"
88
- android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
89
- </vector>`;_fs.default.writeFileSync(`${destPath}/drawable/${iconName}.xml`,iconXml);}// Cleanup functions to remove notification configurations
90
- async function cleanupNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");// Remove notification permissions
91
- const notificationPermissions=["android.permission.POST_NOTIFICATIONS","android.permission.VIBRATE","android.permission.WAKE_LOCK","android.permission.RECEIVE_BOOT_COMPLETED"];notificationPermissions.forEach(permission=>{const permissionRegex=new RegExp(`\\s*<uses-permission android:name="${permission}"[^>]*/>`,"g");manifestContent=manifestContent.replace(permissionRegex,"");});_fs.default.writeFileSync(manifestPath,manifestContent);}catch(error){// Don't throw error for cleanup operations, just log
92
- progress.log(`Warning: Error cleaning notification permissions: ${error.message}`,"warning");}}async function cleanupNotificationResources(){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=_fs.default.readFileSync(colorsPath,"utf8");// Remove notification color
93
- const existingColorLine=colorsContent.split("\n").find(line=>line.includes('name="notification_color"'));if(existingColorLine){colorsContent=colorsContent.replace(existingColorLine,"");_fs.default.writeFileSync(colorsPath,colorsContent);}}catch(error){progress.log(`Warning: Error cleaning notification resources: ${error.message}`,"warning");}}async function cleanupNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");// Remove notification metadata entries
94
- const metadataNames=["default_notification_channel_id","default_notification_icon","default_notification_color","com.google.firebase.messaging.default_notification_channel_id","com.google.firebase.messaging.default_notification_icon","com.google.firebase.messaging.default_notification_sound"];metadataNames.forEach(metadataName=>{// Remove the meta-data block including comments
95
- const metadataRegex=new RegExp(`\\s*<!--[^>]*notification[^>]*-->\\s*<meta-data[^>]*android:name="${metadataName}"[\\s\\S]*?/>|\\s*<meta-data[^>]*android:name="${metadataName}"[\\s\\S]*?/>`,"gi");manifestContent=manifestContent.replace(metadataRegex,"");});// Remove Push Notification Service
96
- const serviceRegex=/\s*<!--\s*Push Notification Service\s*-->\s*<service[^>]*android:name="[^"]*PushNotificationUtils"[\s\S]*?<\/service>/gi;manifestContent=manifestContent.replace(serviceRegex,"");// Clean up any standalone notification comments
97
- manifestContent=manifestContent.replace(/\s*<!--\s*Default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Firebase default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Push Notification Service\s*-->/gi,"");_fs.default.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification metadata: ${error.message}`,"warning");}}async function cleanupNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp","xml"];const audioFormats=["mp3","wav","ogg"];// Define notification icons to clean up
98
- const notificationIcons=["ic_notification","ic_notification_large"];// Define notification sounds to clean up
99
- const notificationSounds=["notification_sound_default","notification_sound_urgent"];// Remove existing notification icons
100
- for(const icon of notificationIcons){for(const format of imageFormats){const iconPath=`${destPath}/drawable/${icon}.${format}`;if(_fs.default.existsSync(iconPath)){_fs.default.unlinkSync(iconPath);}}}// Remove existing notification sounds
101
- const rawDir=`${destPath}/raw`;if(_fs.default.existsSync(rawDir)){for(const sound of notificationSounds){for(const format of audioFormats){const soundPath=`${rawDir}/${sound}.${format}`;if(_fs.default.existsSync(soundPath)){_fs.default.unlinkSync(soundPath);}}}}}catch(error){progress.log(`Warning: Error cleaning notification assets: ${error.message}`,"warning");}}// Legacy function for backward compatibility
102
- // async function installApp(ADB_PATH, androidConfig, buildOptimisation, buildType = "debug") {
103
- // await buildApp(ADB_PATH, androidConfig, buildOptimisation, buildType)
104
- // await launchApp(ADB_PATH, buildType, { type: "emulator" })
105
- // }
106
- async function createAABConfig(androidConfig){// Create AAB configuration based on WebView config with defaults applied
107
- const aabConfig={// Apply defaults here in buildAppAndroid.js
108
- projectPath:androidConfig.projectPath||DEFAULT_PROJECT_PATH,deploymentPath:DEFAULT_DEPLOYMENT_PATH,oldProjectName:DEFAULT_OLD_PROJECT_NAME,overwriteExisting:androidConfig.overwriteExisting!==undefined?androidConfig.overwriteExisting:DEFAULT_OVERWRITE_EXISTING,// Required field - use configured value or derive from other config
109
- newProjectName:androidConfig.newProjectName||androidConfig.appName||androidConfig.packageName?.split(".").pop()||"catalystapp",// IMPORTANT: Pass packageName for renaming
110
- packageName:androidConfig.packageName||null,// AAB specific settings
111
- createSignedAAB:true,outputPath:androidConfig.outputPath||`${process.env.PWD}/build-output`};// Add keystore configuration if available
112
- if(androidConfig.keystoreConfig){aabConfig.keystoreConfig=androidConfig.keystoreConfig;}else if(androidConfig.keystore){// Map old keystore format to new format
113
- aabConfig.keystoreConfig={keyAlias:androidConfig.keystore.alias||"release",storePassword:androidConfig.keystore.storePassword,keyPassword:androidConfig.keystore.keyPassword,validityYears:25,organizationInfo:{companyName:androidConfig.keystore.organizationName||"YourCompany",city:androidConfig.keystore.city||"YourCity",state:androidConfig.keystore.state||"YourState",countryCode:androidConfig.keystore.countryCode||"US"}};}// Log the configuration being used
114
- progress.log(`AAB Configuration:`,"info");progress.log(` Project Path: ${aabConfig.projectPath} ${aabConfig.projectPath===DEFAULT_PROJECT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Deployment Path: ${aabConfig.deploymentPath} ${aabConfig.deploymentPath===DEFAULT_DEPLOYMENT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Old Project Name: ${aabConfig.oldProjectName} ${aabConfig.oldProjectName===DEFAULT_OLD_PROJECT_NAME?"(default)":"(configured)"}`,"info");progress.log(` New Project Name: ${aabConfig.newProjectName}`,"info");progress.log(` Overwrite Existing: ${aabConfig.overwriteExisting} ${aabConfig.overwriteExisting===DEFAULT_OVERWRITE_EXISTING?"(default)":"(configured)"}`,"info");progress.log(` Output Path: ${aabConfig.outputPath}`,"info");return aabConfig;}async function moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,appName){try{// Check if required environment variables are set
115
- if(!process.env.PWD||!BUILD_OUTPUT_PATH){progress.log("Environment variables PWD or BUILD_OUTPUT_PATH not set, skipping APK move","warning");return null;}const currentDate=new Date().toLocaleDateString("en-GB").replace(/\//g,"-");// DD-MM-YYYY format
116
- const currentTime=new Date().toLocaleTimeString("en-US",{hour12:true,hour:"2-digit",minute:"2-digit",second:"2-digit"}).replace(/:/g,":");// HH-MM-SS AM/PM format
117
- let destinationApkFileName="";// Construct source and destination paths
118
- if(appName){destinationApkFileName=buildType==="release"?`${appName}-${currentTime}.release.apk`:`${appName}-${currentTime}.debug.apk`;}else{destinationApkFileName=buildType==="release"?`app-${currentTime}.release.apk`:`app-${currentTime}.debug.apk`;}const sourceApkFileName=buildType==="release"?`app.apk`:`app-debug.apk`;const sourceApkPath=_path.default.join(pwd,"androidProject","app","build","outputs","apk",buildType,sourceApkFileName);const destinationDir=_path.default.join(process.env.PWD,BUILD_OUTPUT_PATH,"native","android",currentDate,buildType);const destinationApkPath=_path.default.join(destinationDir,destinationApkFileName);// Check if source APK exists
119
- if(!_fs.default.existsSync(sourceApkPath)){progress.log(`APK not found at source path: ${sourceApkPath}`,"warning");return null;}// Create destination directory if it doesn't exist
120
- if(!_fs.default.existsSync(destinationDir)){_fs.default.mkdirSync(destinationDir,{recursive:true});}// Copy APK to destination
121
- _fs.default.copyFileSync(sourceApkPath,destinationApkPath);return destinationApkPath;}catch(error){progress.log(`Error moving APK: ${error.message}`,"error");return null;}}async function buildSignedAAB(androidConfig){progress.log("Building signed AAB for release...","info");try{// Generate webview config BEFORE creating AAB
122
- progress.log("Generating webview configuration for release...","info");try{const generateConfigCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath}`;await(0,_utils.runInteractiveCommand)("sh",["-c",generateConfigCommand],{"BUILD SUCCESSFUL":""});progress.log("Webview config generated successfully","success");}catch(configError){progress.log(`Warning: Webview config generation failed: ${configError.message}`,"warning");throw new Error("Webview config generation is required for release builds");}// Create AAB configuration directly without temporary file
123
- const aabConfig=await createAABConfig(androidConfig);// Call the AAB builder directly with config object
124
- await(0,_renameAndroidProject.buildAndroidAAB)(aabConfig);progress.log("Signed AAB build completed successfully!","success");}catch(error){throw new Error("Error building signed AAB: "+error.message);}}async function buildAndroidApp(){// Initialize androidConfig outside try block to ensure it's available in catch
125
- let androidConfig=null;let targetDevice=null;try{// Initialize configuration
126
- progress.start("config");const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=await initializeConfig();androidConfig=WEBVIEW_CONFIG.android;const buildType=androidConfig.buildType||"debug";const buildOptimisation=!!androidConfig.buildOptimisation||false;progress.complete("config");// Validate tools and get paths
127
- progress.start("tools");const{ADB_PATH,EMULATOR_PATH}=await validateAndroidTools(androidConfig);progress.complete("tools");// Device detection and setup (skip for release builds)
128
- if(buildType!=="release"){progress.start("emulator");// Step 1: Check for physical device first
129
- const physicalDevice=await detectPhysicalDevice(ADB_PATH);if(physicalDevice){progress.log(`Found physical device: ${physicalDevice.model} (${physicalDevice.id})`,"success");// Test if we can install on physical device
130
- const canInstallOnPhysical=await testPhysicalDeviceInstallation(ADB_PATH,physicalDevice.id);if(canInstallOnPhysical){// Use physical device - skip emulator entirely
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
- 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
- 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
135
- let movedApkPath=null;if(buildType==="release"){// Build signed AAB for release
136
- progress.start("aab");await buildSignedAAB(androidConfig);progress.complete("aab");// Move APK to output directory
137
- movedApkPath=await moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,androidConfig.appName);}else{// Install debug app for development
138
- progress.start("build");await buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice);await launchApp(ADB_PATH,buildType,targetDevice);progress.complete("build");// Move APK to output directory
139
- movedApkPath=await moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,androidConfig.appName);}// Print build summary
140
- const summaryItems=["Build completed successfully:",{text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`SDK Path: ${androidConfig.sdkPath}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Build Optimization: ${buildOptimisation?"Enabled":"Disabled"}`,indent:1,prefix:"├─ ",color:"gray"}];if(buildType==="release"){// Add APK path for release builds
141
- // Show moved APK location if move was successful
142
- if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}summaryItems.push({text:`Output: Signed AAB generated in build-output/`,indent:1,prefix:"└─ ",color:"green"});}else{// Add APK path for debug builds
143
- // Show moved APK location if move was successful
144
- if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}if(targetDevice&&targetDevice.type==="physical"){summaryItems.push({text:`Target Device: ${targetDevice.model} (Physical)`,indent:1,prefix:"└─ ",color:"green"});}else{summaryItems.push({text:`Target Device: ${androidConfig.emulatorName} (Emulator)`,indent:1,prefix:"└─ ",color:"gray"});}}progress.printTreeContent("Build Summary",summaryItems);process.exit(0);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);const troubleshootingItems=["Build failed. Please try the following steps:",{text:"Check if Android SDK is properly configured",indent:1,prefix:"├─ ",color:"yellow"},{text:"Verify build assets exist in the source directory",indent:1,prefix:"├─ ",color:"yellow"}];// Use androidConfig safely with null checks
145
- const buildType=androidConfig?.buildType||"debug";// Device-specific troubleshooting
146
- if(buildType!=="release"){// Check if error is related to device issues
147
- if(error.message.includes("physical device")||error.message.includes("installation test")){troubleshootingItems.push("\nPhysical Device Issues:",{text:"Enable Developer Options on your device",indent:1,prefix:"├─ ",color:"yellow"},{text:"Enable USB Debugging in Developer Options",indent:1,prefix:"├─ ",color:"yellow"},{text:'Accept the "Allow USB Debugging" prompt on your device',indent:1,prefix:"├─ ",color:"yellow"},{text:"Try disconnecting and reconnecting your device",indent:1,prefix:"├─ ",color:"yellow"},{text:'Check if "adb devices" shows your device as authorized',indent:1,prefix:"├─ ",color:"yellow"});}if(buildType==="release"){troubleshootingItems.push({text:"Verify keystore configuration for release builds",indent:1,prefix:"├─ ",color:"yellow"},{text:"Check that keystore passwords are properly set",indent:1,prefix:"├─ ",color:"yellow"});}else{troubleshootingItems.push({text:"Verify that the emulator exists and is working",indent:1,prefix:"├─ ",color:"yellow"},{text:"If using physical device, ensure it stays connected",indent:1,prefix:"├─ ",color:"yellow"});}}else{troubleshootingItems.push({text:"Verify keystore configuration for release builds",indent:1,prefix:"├─ ",color:"yellow"},{text:"Check that keystore passwords are properly set",indent:1,prefix:"├─ ",color:"yellow"});}troubleshootingItems.push({text:'Run "npm run setupEmulator:android" to reconfigure Android settings',indent:1,prefix:"└─ ",color:"yellow"},"\nVerify Configuration:");// Add configuration details only if androidConfig is available
148
- if(androidConfig){troubleshootingItems.push({text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Android SDK Path: ${androidConfig.sdkPath||"Not configured"}`,indent:1,prefix:"├─ ",color:"gray"});if(buildType!=="release"){// Show device information if available
149
- if(targetDevice){troubleshootingItems.push({text:`Target Device: ${targetDevice.type==="physical"?`${targetDevice.model} (Physical)`:`${androidConfig.emulatorName} (Emulator)`}`,indent:1,prefix:"└─ ",color:"gray"});}else{troubleshootingItems.push({text:`Selected Emulator: ${androidConfig.emulatorName||"Not configured"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:`Output Path: ${androidConfig.outputPath||"build-output/"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:"Configuration could not be loaded",indent:1,prefix:"├─ ",color:"red"},{text:"Check if config/config.json exists and has valid Android configuration",indent:1,prefix:"└─ ",color:"red"});}progress.printTreeContent("Troubleshooting Guide",troubleshootingItems);}process.exit(1);}}// Execute the main build process
150
- buildAndroidApp();
1
+ "use strict";const{createAndroidBuild,pwd}=require("./buildAndroid/index.js");const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=require(`${process.cwd()}/config/config.json`);async function main(){const build=createAndroidBuild({WEBVIEW_CONFIG,BUILD_OUTPUT_PATH});const{buildAndroidApp}=build;try{await buildAndroidApp();}catch(error){process.exit(1);}process.exit(0);}if(require.main===module){main();}// Legacy re-exports for any tooling that imports from this file directly
2
+ module.exports={createAndroidBuild,pwd};