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.
- package/README.md +1 -1
- package/bin/catalyst.js +8 -1
- package/changelog.md +6 -0
- package/dist/native/androidProject/app/build.gradle.kts +34 -8
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +270 -78
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +34 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +149 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeCameraManager.kt +337 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/OfflineCacheService.kt +374 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/TransitionManager.kt +202 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +156 -39
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +123 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +244 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/HoldController.kt +70 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/TorchController.kt +57 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamState.kt +41 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamStateMachine.kt +40 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ViewfinderMapper.kt +75 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ZoomController.kt +156 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +16 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +42 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +7 -3
- package/dist/native/androidProject/app/src/main/res/drawable/debug_viewfinder_border.xml +8 -0
- package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +37 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
- package/dist/native/androidSetup.js +1 -0
- package/dist/native/bridge/WebBridge.js +5 -5
- package/dist/native/bridge/constants/NativeInterfaces.js +9 -5
- package/dist/native/bridge/hooks/index.js +1 -0
- package/dist/native/bridge/hooks/useCamera.js +9 -0
- package/dist/native/bridge/hooks/useDataProtection.js +2 -0
- package/dist/native/bridge/hooks/useDeviceInfo.js +2 -0
- package/dist/native/bridge/hooks/useFilePicker.js +2 -0
- package/dist/native/bridge/hooks/useGoogleSignIn.js +1 -0
- package/dist/native/bridge/hooks/useHapticFeedback.js +1 -0
- package/dist/native/bridge/hooks/useIntent.js +1 -0
- package/dist/native/bridge/hooks/useNativeTransition.js +43 -0
- package/dist/native/bridge/hooks/useNetworkStatus.js +3 -0
- package/dist/native/bridge/hooks/useNotification.js +2 -0
- package/dist/native/bridge/hooks/useSafeArea.js +2 -0
- package/dist/native/bridge/hooks/useVideoStream.js +6 -0
- package/dist/native/bridge/hooks/utils.js +1 -0
- package/dist/native/bridge/hooks.js +1 -137
- package/dist/native/bridge/useBaseHook.js +12 -7
- package/dist/native/bridge/utils/NativeBridge.js +41 -6
- package/dist/native/buildAndroid/assets.js +52 -0
- package/dist/native/buildAndroid/build.js +19 -0
- package/dist/native/buildAndroid/config.js +1 -0
- package/dist/native/buildAndroid/index.js +1 -0
- package/dist/native/buildAppAndroid.js +2 -150
- package/dist/native/buildAppIos.js +2 -312
- package/dist/native/buildIos/assets.js +8 -0
- package/dist/native/buildIos/build.js +40 -0
- package/dist/native/buildIos/config.js +71 -0
- package/dist/native/buildIos/index.js +6 -0
- package/dist/native/buildIos/plugins.js +45 -0
- package/dist/native/internalPluginUtils.js +7 -0
- package/dist/native/iosnativeWebView/Package.resolved +36 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/BarcodeDetector.swift +116 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraPreviewView.swift +58 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +308 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +63 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +194 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/TorchController.swift +60 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamState.swift +44 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamStateMachine.swift +60 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +130 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +11 -1
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +2 -1
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +39 -4
- package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineCacheService.swift +683 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineURLSchemeHandler.swift +94 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +19 -6
- package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +227 -4
- package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +53 -8
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +88 -4
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +30 -11
- package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
- package/dist/native/plugin-bridge/PluginBridge.js +1 -0
- package/dist/native/pluginComposerAndroid.js +28 -0
- package/dist/native/pluginComposerIos.js +16 -0
- package/dist/native/renameAndroidProject.js +5 -5
- package/dist/native/setupEmulatorIos.js +6 -4
- package/dist/native/setupServer.js +2 -3
- package/dist/native/utils.js +4 -1
- package/dist/offline/catalyst-sw.js +1 -0
- package/dist/offline/registerServiceWorker.js +1 -0
- package/dist/otel.js +70 -2
- package/dist/router/ClientRouter.js +1 -1
- package/dist/scripts/build.js +1 -1
- package/dist/scripts/cleanCache.js +1 -0
- package/dist/scripts/devBuild.js +1 -1
- package/dist/scripts/devServe.js +1 -1
- package/dist/scripts/generateOfflineManifest.js +1 -0
- package/dist/scripts/loadEnvironmentVariables.js +1 -2
- package/dist/scripts/plugins.js +1 -0
- package/dist/scripts/registerAliases.js +6 -2
- package/dist/scripts/scriptUtils.js +2 -1
- package/dist/scripts/serve.js +2 -1
- package/dist/scripts/start.js +7 -2
- package/dist/server/expressServer.js +4 -8
- package/dist/server/renderer/document/Body.js +4 -4
- package/dist/server/renderer/document/Head.js +2 -2
- package/dist/server/renderer/extract.js +31 -27
- package/dist/server/renderer/handler.js +47 -24
- package/dist/server/renderer/index.js +1 -1
- package/dist/server/startServer.js +57 -7
- package/dist/server/utils/userAgentUtil.js +9 -3
- package/dist/server/utils/validator.js +1 -1
- package/dist/webpack/babel-plugins/remove-client.plugin.js +2 -2
- package/dist/webpack/babel-plugins/remove-ssr.plugin.js +2 -2
- package/dist/webpack/babel.config.client.js +1 -1
- package/dist/webpack/babel.config.ssr.js +1 -1
- package/dist/webpack/base.babel.js +7 -8
- package/dist/webpack/development.client.babel.js +7 -16
- package/dist/webpack/production.client.babel.js +1 -1
- package/dist/webpack/production.ssr.babel.js +5 -1
- package/mcp_v2/knowledge-base.json +206 -100
- package/mcp_v2/lib/helpers.js +42 -8
- package/mcp_v2/lib/seed.js +88 -0
- package/mcp_v2/mcp.js +150 -4
- package/mcp_v2/setup.js +30 -193
- package/mcp_v2/tools/build.js +1 -1
- package/mcp_v2/tools/config.js +16 -4
- package/mcp_v2/tools/conversion.js +11 -11
- package/mcp_v2/tools/github.js +1117 -0
- package/mcp_v2/tools/knowledge.js +16 -3
- package/mcp_v2/tools/sync.js +179 -10
- package/mcp_v2/tools/tasks.js +13 -4
- 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&&
|
|
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(
|
|
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(
|
|
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(
|
|
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,//
|
|
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(
|
|
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(
|
|
16
|
-
window.NativeBridge[command](data);return true;}else{console.error(`Android bridge method '${command}' not found`);return false;}}catch(error){console.error(
|
|
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(
|
|
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(
|
|
27
|
-
if(this.isAndroid){return this._executeAndroidCommand(command,data);}else if(this.isIOS){return this._executeIOSCommand(command,data);}return false;}catch(error){console.error(
|
|
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
|
-
|
|
2
|
-
|
|
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};
|