catalyst-core 0.1.0-beta.2 → 0.1.0-beta.4
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 +9 -0
- 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 +17 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +31 -3
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +148 -0
- 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/TransitionManager.kt +202 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +3 -3
- 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/plugins/PluginBridgeTest.kt +139 -0
- package/dist/native/androidSetup.js +1 -1
- 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/buildAppAndroid.js +7 -9
- package/dist/native/buildAppIos.js +14 -22
- package/dist/native/internalPluginUtils.js +1 -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/Plugins/CatalystPlugin.swift +5 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/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 +38 -7
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +6 -2
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +15 -4
- package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
- package/dist/native/plugin-bridge/PluginBridge.js +1 -0
- package/dist/native/pluginComposerAndroid.js +9 -0
- package/dist/native/pluginComposerIos.js +7 -0
- package/dist/native/renameAndroidProject.js +3 -3
- package/dist/native/setupServer.js +2 -3
- package/dist/native/utils.js +4 -1
- package/dist/otel.js +70 -2
- package/dist/scripts/devServe.js +1 -1
- 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 +4 -1
- package/dist/server/renderer/extract.js +5 -5
- package/dist/server/renderer/handler.js +15 -14
- package/dist/server/utils/validator.js +1 -1
- package/dist/webpack/base.babel.js +2 -1
- package/dist/webpack/development.client.babel.js +6 -2
- package/dist/webpack/production.ssr.babel.js +5 -1
- package/mcp_v2/knowledge-base.json +1564 -1067
- package/mcp_v2/lib/helpers.js +19 -7
- 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/knowledge.js +16 -3
- package/mcp_v2/tools/tasks.js +13 -4
- package/package.json +2 -2
|
@@ -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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/* eslint-disable no-extra-semi */"use strict";var _renameAndroidProject=require("./renameAndroidProject.js");var _child_process=require("child_process");var _fs=_interopRequireDefault(require("fs"));var _path=_interopRequireDefault(require("path"));var _utils=require("./utils.js");var _TerminalProgress=_interopRequireDefault(require("./TerminalProgress.js"));// Import the AAB builder
|
|
2
|
-
function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}const configPath=`${process.
|
|
1
|
+
/* eslint-disable no-extra-semi */"use strict";var _renameAndroidProject=require("./renameAndroidProject.js");var _child_process=require("child_process");var _fs=_interopRequireDefault(require("fs"));var _path=_interopRequireDefault(require("path"));var _utils=require("./utils.js");var _TerminalProgress=_interopRequireDefault(require("./TerminalProgress.js"));var _pluginComposerAndroid=require("./pluginComposerAndroid.js");var _internalPluginUtils=require("./internalPluginUtils.js");// Import the AAB builder
|
|
2
|
+
function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}const configPath=`${process.cwd()}/config/config.json`;const publicPath=`${process.cwd()}/public`;const catalystCorePath=_path.default.dirname(require.resolve("catalyst-core/package.json"));const pwd=_path.default.join(catalystCorePath,"dist/native");const ANDROID_PACKAGE="io.yourname.androidproject";// Default values for AAB building
|
|
3
3
|
const DEFAULT_PROJECT_PATH=`${pwd}/androidProject`;const DEFAULT_DEPLOYMENT_PATH="./deployment";const DEFAULT_OLD_PROJECT_NAME="androidProject";// Use actual project name in catalyst
|
|
4
4
|
const DEFAULT_OVERWRITE_EXISTING=true;const steps={config:"Initialize Configuration",tools:"Validate Android Tools",emulator:"Check and Start Emulator",copyAssets:"Copy Build Assets",build:"Build and Install Application",aab:"Build Signed AAB"};const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};const progress=new _TerminalProgress.default(steps,"Catalyst Android Build",progressConfig);async function initializeConfig(){const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=config;if(!WEBVIEW_CONFIG||Object.keys(WEBVIEW_CONFIG).length===0){throw new Error("WebView Config missing in "+configPath);}if(!WEBVIEW_CONFIG.android){throw new Error("Android config missing in WebView Config");}// Log build type information
|
|
5
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
|
|
@@ -13,7 +13,7 @@ continue;}}}}progress.log("No physical devices detected","info");return null;}ca
|
|
|
13
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
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
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
|
|
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(0,_utils.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 copyBuildAssets(androidConfig,buildOptimisation=false){if(!buildOptimisation)return;progress.log("Copying build assets to Android project...","info");try{// Define source and destination paths
|
|
17
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
18
|
;(0,_utils.runCommand)(`mkdir -p ${destPath}`)// Clear existing destination to avoid conflicts
|
|
19
19
|
;(0,_utils.runCommand)(`rm -rf ${destPath}/*`);// Files to exclude from copying
|
|
@@ -86,13 +86,11 @@ const iconXml=`<vector xmlns:android="http://schemas.android.com/apk/res/android
|
|
|
86
86
|
<path
|
|
87
87
|
android:fillColor="@android:color/white"
|
|
88
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);}
|
|
90
|
-
|
|
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
|
|
89
|
+
</vector>`;_fs.default.writeFileSync(`${destPath}/drawable/${iconName}.xml`,iconXml);}async function cleanupNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");// Remove notification permissions
|
|
90
|
+
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.default.writeFileSync(manifestPath,manifestContent);}catch(error){// Don't throw error for cleanup operations, just log
|
|
92
91
|
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
92
|
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.
|
|
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
|
|
93
|
+
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"];manifestContent=manifestContent.split("\n").filter(line=>{return!metadataNames.some(metadataName=>line.includes(`android:name="${metadataName}"`));}).join("\n");// Remove Push Notification Service
|
|
96
94
|
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
95
|
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
96
|
const notificationIcons=["ic_notification","ic_notification_large"];// Define notification sounds to clean up
|
|
@@ -131,7 +129,7 @@ const canInstallOnPhysical=await testPhysicalDeviceInstallation(ADB_PATH,physica
|
|
|
131
129
|
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
130
|
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
131
|
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
|
|
132
|
+
progress.start("copyAssets");await copyBuildAssets(androidConfig,buildOptimisation);await copySplashscreenAssets();await copyOfflinePage();await copyIconAssets();await configureAppName(androidConfig);const pluginConfig=(0,_internalPluginUtils.resolvePluginConfig)(WEBVIEW_CONFIG);(0,_pluginComposerAndroid.composeAndroidPlugins)({corePluginsRoot:(0,_internalPluginUtils.resolveInternalPluginsRoot)(catalystCorePath),androidProjectPath:`${pwd}/androidProject`,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await processNotifications(WEBVIEW_CONFIG);progress.log(`Build optimization: ${buildOptimisation?"Enabled":"Disabled"}`,"info");progress.complete("copyAssets");// Build based on type
|
|
135
133
|
let movedApkPath=null;if(buildType==="release"){// Build signed AAB for release
|
|
136
134
|
progress.start("aab");await buildSignedAAB(androidConfig);progress.complete("aab");// Move APK to output directory
|
|
137
135
|
movedApkPath=await moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,androidConfig.appName);}else{// Install debug app for development
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
"use strict";const{
|
|
2
|
-
const iosConfig=WEBVIEW_CONFIG.ios;const isGoogleSignInEnabled=WEBVIEW_CONFIG.googleSignIn?.enabled??false;const protocol=WEBVIEW_CONFIG.useHttps?"https":"http";const ip=WEBVIEW_CONFIG.LOCAL_IP||"localhost";const port=WEBVIEW_CONFIG.port?WEBVIEW_CONFIG.useHttps?443:WEBVIEW_CONFIG.port:null;let url=port?`${protocol}://${ip}:${port}`:`${protocol}://${ip}`;const PUBLIC_PATH=`${process.env.PWD}/public`;const PROJECT_DIR=`${pwd}/iosnativeWebView`;const SCHEME_NAME="iosnativeWebView";const APP_BUNDLE_ID=iosConfig.appBundleId||"com.debug.webview";const PROJECT_NAME=path.basename(PROJECT_DIR);const IPHONE_MODEL=iosConfig.simulatorName;const GOOGLE_SERVICES_FILENAME="GoogleService-Info.plist";function generateProjectObjectId(identifier,suffix=""){return crypto.createHash("md5").update(`${identifier}:${suffix}`).digest("hex").substring(0,24).toUpperCase();}function getXcodeProjectFilePath(){return path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");}// Define build steps for progress tracking
|
|
1
|
+
"use strict";const{execFile,execSync,execFileSync}=require("child_process");const fs=require("fs");const path=require("path");const TerminalProgress=require("./TerminalProgress.js").default;const crypto=require("crypto");const{runInteractiveCommand}=require("./utils.js");const{composeIosPlugins}=require("./pluginComposerIos.js");const{resolveInternalPluginsRoot,resolvePluginConfig}=require("./internalPluginUtils.js");const catalystCorePath=path.dirname(require.resolve("catalyst-core/package.json"));const pwd=path.join(catalystCorePath,"dist/native");const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=require(`${process.cwd()}/config/config.json`);// Configuration constants
|
|
2
|
+
const iosConfig=WEBVIEW_CONFIG.ios;const isGoogleSignInEnabled=WEBVIEW_CONFIG.googleSignIn?.enabled??false;const protocol=WEBVIEW_CONFIG.useHttps?"https":"http";const ip=WEBVIEW_CONFIG.LOCAL_IP||"localhost";const port=WEBVIEW_CONFIG.port?WEBVIEW_CONFIG.useHttps?443:WEBVIEW_CONFIG.port:null;let url=port?`${protocol}://${ip}:${port}`:`${protocol}://${ip}`;const PUBLIC_PATH=`${process.env.PWD}/public`;const PROJECT_DIR=`${pwd}/iosnativeWebView`;const SCHEME_NAME="iosnativeWebView";const APP_BUNDLE_ID=iosConfig.appBundleId||"com.debug.webview";const PROJECT_NAME=path.basename(PROJECT_DIR);const IPHONE_MODEL=iosConfig.simulatorName;const GOOGLE_SERVICES_FILENAME="GoogleService-Info.plist";const MANAGED_BASELINE_SUFFIX=".catalyst-base";const PLUGIN_RESOURCE_ROOT="PluginResources";function packageRequirementKey(dependency){return`${dependency.requirement.type}:${dependency.requirement.version}`;}function mergePackageDependency(map,dependency,sourceLabel){const existing=map.get(dependency.url);if(!existing){map.set(dependency.url,{url:dependency.url,package:dependency.package,requirement:dependency.requirement,products:[...new Set(dependency.products)]});return;}if(packageRequirementKey(existing)!==packageRequirementKey(dependency)){throw new Error(`iOS package dependency conflict for '${dependency.url}' while processing ${sourceLabel}: '${existing.requirement.type}:${existing.requirement.version}' vs '${dependency.requirement.type}:${dependency.requirement.version}'`);}if(existing.package!==dependency.package){throw new Error(`iOS package identity conflict for '${dependency.url}' while processing ${sourceLabel}: '${existing.package}' vs '${dependency.package}'`);}existing.products=[...new Set([...existing.products,...dependency.products])].sort();}function formatSwiftPackageRequirement(dependency){if(dependency.requirement.type==="from"){return`from: "${dependency.requirement.version}"`;}if(dependency.requirement.type==="exact"){return`exact: "${dependency.requirement.version}"`;}throw new Error(`Unsupported iOS package requirement type: ${dependency.requirement.type}`);}function formatSwiftPackageEntries(dependencies){return dependencies.map(dependency=>` .package(url: "${dependency.url}", ${formatSwiftPackageRequirement(dependency)})`);}function formatSwiftProductEntries(dependencies){return dependencies.flatMap(dependency=>dependency.products.map(product=>` .product(name: "${product}", package: "${dependency.package}")`));}function isPlainObject(value){return!!value&&typeof value==="object"&&!Array.isArray(value);}function deepEqual(left,right){return JSON.stringify(left)===JSON.stringify(right);}function mergeStructuredValues(existing,incoming,fieldName){if(existing===undefined){return JSON.parse(JSON.stringify(incoming));}if(Array.isArray(existing)&&Array.isArray(incoming)){const merged=[];const seen=new Set();for(const value of[...existing,...incoming]){const key=JSON.stringify(value);if(seen.has(key)){continue;}seen.add(key);merged.push(value);}return merged;}if(isPlainObject(existing)&&isPlainObject(incoming)){const merged={...existing};for(const[key,value]of Object.entries(incoming)){merged[key]=mergeStructuredValues(merged[key],value,`${fieldName}.${key}`);}return merged;}if(deepEqual(existing,incoming)){return existing;}throw new Error(`Conflicting values for '${fieldName}' while composing iOS build metadata`);}function ensureManagedBaseline(filePath){const baselinePath=`${filePath}${MANAGED_BASELINE_SUFFIX}`;if(!fs.existsSync(baselinePath)){if(!fs.existsSync(filePath)){throw new Error(`Managed baseline source file not found: ${filePath}`);}fs.copyFileSync(filePath,baselinePath);}return baselinePath;}function restoreManagedFileFromBaseline(filePath){const baselinePath=ensureManagedBaseline(filePath);fs.copyFileSync(baselinePath,filePath);}function readPlistObject(filePath){const output=execFileSync("plutil",["-convert","json","-o","-",filePath],{encoding:"utf8"});return JSON.parse(output);}function writePlistObject(filePath,value){const tempPath=path.join(PROJECT_DIR,`.catalyst-${path.basename(filePath)}-${process.pid}.json`);fs.writeFileSync(tempPath,JSON.stringify(value,null,2),"utf8");try{execFileSync("plutil",["-convert","xml1","-o",filePath,tempPath]);}finally{fs.rmSync(tempPath,{force:true});}}function mergeIntoTopLevelObject(target,source,fieldName){for(const[key,value]of Object.entries(source||{})){target[key]=mergeStructuredValues(target[key],value,`${fieldName}.${key}`);}}function mergeUrlSchemes(plistObject,entries){const existing=Array.isArray(plistObject.CFBundleURLTypes)?plistObject.CFBundleURLTypes:[];const merged=existing.map(entry=>({...entry,CFBundleURLSchemes:Array.isArray(entry?.CFBundleURLSchemes)?[...new Set(entry.CFBundleURLSchemes)]:[]}));for(const entry of entries){const targetName=entry.name||null;let match=merged.find(item=>item.CFBundleURLName===targetName);if(!match){match={CFBundleURLName:targetName,CFBundleURLSchemes:[]};merged.push(match);}match.CFBundleURLSchemes=[...new Set([...match.CFBundleURLSchemes,...entry.schemes])];}if(merged.length>0){plistObject.CFBundleURLTypes=merged;}}function mergeQuerySchemes(plistObject,schemes){const existing=Array.isArray(plistObject.LSApplicationQueriesSchemes)?plistObject.LSApplicationQueriesSchemes:[];const merged=[...new Set([...existing,...schemes.filter(Boolean)])];if(merged.length>0){plistObject.LSApplicationQueriesSchemes=merged;}}function formatPbxprojPath(value){return /[^A-Za-z0-9_./-]/.test(value)?`"${value}"`:value;}function detectPbxprojFileType(filePath){const extension=path.extname(filePath).toLowerCase();const fileTypes={".json":"text.json",".plist":"text.plist.xml",".txt":"text",".html":"text.html",".js":"sourcecode.javascript",".css":"text.css",".png":"image.png",".jpg":"image.jpeg",".jpeg":"image.jpeg",".gif":"image.gif",".svg":"image.svg",".mp3":"audio.mp3",".wav":"audio.wav",".m4a":"audio.m4a",".caf":"audio.caf",".pdf":"image.pdf"};return fileTypes[extension]||"file";}function generateProjectObjectId(identifier,suffix=""){return crypto.createHash("md5").update(`${identifier}:${suffix}`).digest("hex").substring(0,24).toUpperCase();}function getXcodeProjectFilePath(){return path.join(PROJECT_DIR,`${PROJECT_NAME}.xcodeproj`,"project.pbxproj");}function replaceRequired(content,pattern,replacement,errorMessage){if(!pattern.test(content)){throw new Error(errorMessage);}return content.replace(pattern,replacement);}// Define build steps for progress tracking
|
|
3
3
|
const steps={config:"Generating Required Configuration for build",deviceDetection:"Detecting Physical Device",launchSimulator:"Launch iOS Simulator",clean:"Clean Build Artifacts",assets:"Process Notification Assets",build:"Build IOS Project",findApp:"Locate Built Application",install:"Install Application",launch:"Launch Application"};// Configure progress display
|
|
4
|
-
const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};const progress=new TerminalProgress(steps,"Catalyst iOS Build",progressConfig);// Utility function to run shell commands
|
|
5
|
-
function runCommand(command,options={}){return new Promise((resolve,reject)=>{
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const configHash=crypto.createHash("md5").update(JSON.stringify({notifications:isNotificationsEnabled,googleSignIn:isGoogleSignInEnabled})).digest("hex");const hashFilePath=path.join(PROJECT_DIR,".package-config-hash");const targetPath=path.join(PROJECT_DIR,"Package.swift");let shouldUpdate=true;// Check if we need to update based on config change
|
|
4
|
+
const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};const progress=new TerminalProgress(steps,"Catalyst iOS Build",progressConfig);const shellCommand=process.platform==="win32"?"cmd.exe":"sh";const shellArgs=command=>process.platform==="win32"?["/d","/s","/c",command]:["-c",command];// Utility function to run shell commands
|
|
5
|
+
function runCommand(command,options={}){return new Promise((resolve,reject)=>{execFile(shellCommand,shellArgs(command),{maxBuffer:1024*1024*10,...options},(error,stdout,stderr)=>{if(error){console.error(`Command failed: ${command}`);console.error(`Error: ${error.message}`);console.error(`stderr: ${stderr}`);reject(error);return;}if(stderr){console.warn(`Warning: ${stderr}`);}resolve(stdout.trim());});});}async function getBootedSimulatorUUID(modelName){const UUID_RE=/[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/i;try{const output=await runInteractiveCommand("xcrun",["simctl","list","devices"],{});const lines=output.split("\n");// First pass: find a booted simulator matching modelName
|
|
6
|
+
for(const line of lines){if(line.includes(modelName)&&line.includes("Booted")){const m=line.match(UUID_RE);if(m){console.log(`Found booted simulator of model ${modelName}`);return m[0];}}}// Second pass: any booted simulator
|
|
7
|
+
console.log(`No booted simulator of model ${modelName} found, checking for any booted simulator...`);for(const line of lines){if(line.includes("Booted")){const m=line.match(UUID_RE);if(m){console.log("Found another booted simulator, will use it instead");return m[0];}}}return null;}catch(error){console.log("No booted simulators found");return null;}}async function getBootedSimulatorInfo(){try{const listCommand="xcrun simctl list devices --json";const simulatorList=JSON.parse(execSync(listCommand).toString());for(const runtime in simulatorList.devices){const devices=simulatorList.devices[runtime];for(const device of devices){if(device.state==="Booted"){return{udid:device.udid,name:device.name,runtime:runtime};}}}return null;}catch(error){console.log("Failed to get booted simulator info:",error.message);return null;}}async function generatePackageSwift(pluginDependencies=[]){try{const isNotificationsEnabled=WEBVIEW_CONFIG.notifications?.enabled??false;const baseCoreDependencies=[{url:"https://github.com/kylef/JSONSchema.swift",package:"JSONSchema.swift",requirement:{type:"from",version:"0.6.0"},products:["JSONSchema"]},{url:"https://github.com/google/GoogleSignIn-iOS",package:"GoogleSignIn-iOS",requirement:{type:"from",version:"7.0.0"},products:["GoogleSignIn"]}];const notificationsDependencies=isNotificationsEnabled?[{url:"https://github.com/firebase/firebase-ios-sdk",package:"firebase-ios-sdk",requirement:{type:"from",version:"12.3.0"},products:["FirebaseCore","FirebaseMessaging"]}]:[];const coreDependencyMap=new Map();baseCoreDependencies.forEach(dependency=>mergePackageDependency(coreDependencyMap,dependency,"base core"));pluginDependencies.forEach(dependency=>mergePackageDependency(coreDependencyMap,dependency,"plugin manifests"));const coreDependencies=[...coreDependencyMap.values()].sort((left,right)=>left.url.localeCompare(right.url));const packageDependencyMap=new Map();coreDependencies.forEach(dependency=>mergePackageDependency(packageDependencyMap,dependency,"core target"));notificationsDependencies.forEach(dependency=>mergePackageDependency(packageDependencyMap,dependency,"notifications target"));const packageDependencies=[...packageDependencyMap.values()].sort((left,right)=>left.url.localeCompare(right.url));progress.log(`🔧 Generating Package.swift (notifications: ${isNotificationsEnabled})`,"info");// Create hash of current config to detect changes
|
|
8
|
+
const configHash=crypto.createHash("md5").update(JSON.stringify({notifications:isNotificationsEnabled,googleSignIn:isGoogleSignInEnabled,pluginDependencies:coreDependencies})).digest("hex");const hashFilePath=path.join(PROJECT_DIR,".package-config-hash");const targetPath=path.join(PROJECT_DIR,"Package.swift");let shouldUpdate=true;// Check if we need to update based on config change
|
|
10
9
|
if(fs.existsSync(hashFilePath)){const previousHash=fs.readFileSync(hashFilePath,"utf8");if(previousHash===configHash){shouldUpdate=false;progress.log("Package.swift already up to date","info");}}// Ensure Package.swift exists even if hash matches
|
|
11
10
|
if(!fs.existsSync(targetPath)){shouldUpdate=true;progress.log("Package.swift missing, will generate it now","info");}if(shouldUpdate){progress.log("Generating Package.swift dynamically","info");// Build the Package.swift content dynamically
|
|
12
11
|
let packageContent=`// swift-tools-version: 5.9
|
|
@@ -23,10 +22,7 @@ if(isNotificationsEnabled){packageContent+=`,
|
|
|
23
22
|
.library(name: "CatalystNotifications", targets: ["CatalystNotifications"])`;}packageContent+=`
|
|
24
23
|
],
|
|
25
24
|
dependencies: [
|
|
26
|
-
|
|
27
|
-
.package(url: "https://github.com/google/GoogleSignIn-iOS", from: "7.0.0")`;// Add Firebase dependency only if notifications enabled
|
|
28
|
-
if(isNotificationsEnabled){packageContent+=`,
|
|
29
|
-
.package(url: "https://github.com/firebase/firebase-ios-sdk", from: "12.3.0")`;}packageContent+=`
|
|
25
|
+
${formatSwiftPackageEntries(packageDependencies).join(",\n")}`;packageContent+=`
|
|
30
26
|
],
|
|
31
27
|
targets: [
|
|
32
28
|
// Core functionality (WebView, bridge, utils, constants)
|
|
@@ -34,8 +30,7 @@ if(isNotificationsEnabled){packageContent+=`,
|
|
|
34
30
|
.target(
|
|
35
31
|
name: "CatalystCore",
|
|
36
32
|
dependencies: [
|
|
37
|
-
|
|
38
|
-
.product(name: "GoogleSignIn", package: "GoogleSignIn-iOS")
|
|
33
|
+
${formatSwiftProductEntries(coreDependencies).join(",\n")}
|
|
39
34
|
],
|
|
40
35
|
path: "Sources/Core"
|
|
41
36
|
)`;// Add CatalystNotifications target only if enabled
|
|
@@ -45,8 +40,7 @@ if(isNotificationsEnabled){packageContent+=`,
|
|
|
45
40
|
name: "CatalystNotifications",
|
|
46
41
|
dependencies: [
|
|
47
42
|
"CatalystCore",
|
|
48
|
-
|
|
49
|
-
.product(name: "FirebaseMessaging", package: "firebase-ios-sdk")
|
|
43
|
+
${formatSwiftProductEntries(notificationsDependencies).join(",\n")}
|
|
50
44
|
],
|
|
51
45
|
path: "Sources/CatalystNotifications"
|
|
52
46
|
)`;}packageContent+=`
|
|
@@ -65,9 +59,7 @@ projectContent=projectContent.replace(/(C99974342E97D56900C25611 \/\* CatalystCo
|
|
|
65
59
|
projectContent=projectContent.replace(/(C99974342E97D56900C25611 \/\* CatalystCore in Frameworks \*\/,)/,`$1\n\t\t\t\t${NOTIF_BUILD_FILE_ID} /* CatalystNotifications in Frameworks */,`);// 3. Add to packageProductDependencies array
|
|
66
60
|
projectContent=projectContent.replace(/(packageProductDependencies = \(\s*C99974332E97D56900C25611 \/\* CatalystCore \*\/,)/,`$1\n\t\t\t\t${NOTIF_PRODUCT_ID} /* CatalystNotifications */,`);// 4. Add to XCSwiftPackageProductDependency section
|
|
67
61
|
projectContent=projectContent.replace(/(\/\* End XCSwiftPackageProductDependency section \*\/)/,`\t\t${NOTIF_PRODUCT_ID} /* CatalystNotifications */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = C99974322E97D56900C25611 /* XCLocalSwiftPackageReference "." */;\n\t\t\tproductName = CatalystNotifications;\n\t\t};\n$1`);fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("✅ CatalystNotifications added to Xcode project","success");}else if(!isNotificationsEnabled&&hasNotifications){progress.log("Removing CatalystNotifications from Xcode project","info");// Remove all CatalystNotifications entries
|
|
68
|
-
projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/ = {isa = PBXBuildFile; productRef = [A-F0-9]+ \/\* CatalystNotifications \*\/; };\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = [A-F0-9]+ \/\* XCLocalSwiftPackageReference "." \*\/;\n\t\t\tproductName = CatalystNotifications;\n\t\t};\n/g,"");fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("✅ CatalystNotifications removed from Xcode project","success");}else{progress.log("Package dependencies already correct","info");}}catch(error){progress.log(`❌ Failed to update package dependencies: ${error.message}`,"error");throw error;}}async function updateInfoPlist(){try{const infoPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info.plist");const infoReleasePlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info-Release.plist");const googleServicesPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,GOOGLE_SERVICES_FILENAME);const googleClientId=WEBVIEW_CONFIG.googleSignIn?.clientId||WEBVIEW_CONFIG.googleSignIn?.webClientId||"";const iosClientId=WEBVIEW_CONFIG.googleSignIn?.iosClientId||"";const googleServicesContent=fs.existsSync(googleServicesPlistPath)?fs.readFileSync(googleServicesPlistPath,"utf8"):null;const reversedClientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>REVERSED_CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const clientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const computeReversed=value=>{if(!value)return"";return value.split(".").reverse().join(".");};const resolvedClientIdForScheme=iosClientId||googleClientId||clientIdFromServices||"";const resolvedReversedClientId=reversedClientIdFromServices||computeReversed(resolvedClientIdForScheme);if(isGoogleSignInEnabled&&!resolvedReversedClientId){progress.fail("config","Google Sign-In enabled but no valid clientId found");process.exit(1);}const plistTargets=[infoPlistPath,infoReleasePlistPath];const
|
|
69
|
-
if(!plistContent.includes("CFBundleDisplayName")){const insertPoint=plistContent.lastIndexOf("</dict>");const newEntry=`\t<key>CFBundleDisplayName</key>\n\t<string>${iosConfig.appName||"Catalyst Application"}</string>\n`;plistContent=plistContent.slice(0,insertPoint)+newEntry+plistContent.slice(insertPoint);fs.writeFileSync(plistPath,plistContent,"utf8");}else{// Update existing CFBundleDisplayName with new appName
|
|
70
|
-
const displayNameRegex=/(<key>CFBundleDisplayName<\/key>\s*<string>)([^<]*)(<\/string>)/;if(displayNameRegex.test(plistContent)){plistContent=plistContent.replace(displayNameRegex,`$1${iosConfig.appName||"Catalyst Application"}$3`);fs.writeFileSync(plistPath,plistContent,"utf8");}}};plistTargets.forEach(plistPath=>{ensureDisplayName(plistPath);ensureGoogleUrlScheme(plistPath);ensureLSApplicationQueriesSchemes(plistPath);});}catch(err){progress.fail("config",err);process.exit(1);}}// Function to convert JSON value to Swift property
|
|
62
|
+
projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/ = {isa = PBXBuildFile; productRef = [A-F0-9]+ \/\* CatalystNotifications \*\/; };\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications in Frameworks \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/,\n/g,"");projectContent=projectContent.replace(/\t\t[A-F0-9]+ \/\* CatalystNotifications \*\/ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = [A-F0-9]+ \/\* XCLocalSwiftPackageReference "." \*\/;\n\t\t\tproductName = CatalystNotifications;\n\t\t};\n/g,"");fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("✅ CatalystNotifications removed from Xcode project","success");}else{progress.log("Package dependencies already correct","info");}}catch(error){progress.log(`❌ Failed to update package dependencies: ${error.message}`,"error");throw error;}}async function updateInfoPlist(pluginComposition={}){try{const infoPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info.plist");const infoReleasePlistPath=path.join(PROJECT_DIR,PROJECT_NAME,"Info-Release.plist");const googleServicesPlistPath=path.join(PROJECT_DIR,PROJECT_NAME,GOOGLE_SERVICES_FILENAME);const googleClientId=WEBVIEW_CONFIG.googleSignIn?.clientId||WEBVIEW_CONFIG.googleSignIn?.webClientId||"";const iosClientId=WEBVIEW_CONFIG.googleSignIn?.iosClientId||"";const googleServicesContent=fs.existsSync(googleServicesPlistPath)?fs.readFileSync(googleServicesPlistPath,"utf8"):null;const reversedClientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>REVERSED_CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const clientIdFromServices=googleServicesContent?(googleServicesContent.match(/<key>CLIENT_ID<\/key>\s*<string>([^<]+)<\/string>/)||[])[1]:null;const computeReversed=value=>{if(!value)return"";return value.split(".").reverse().join(".");};const resolvedClientIdForScheme=iosClientId||googleClientId||clientIdFromServices||"";const resolvedReversedClientId=reversedClientIdFromServices||computeReversed(resolvedClientIdForScheme);if(isGoogleSignInEnabled&&!resolvedReversedClientId){progress.fail("config","Google Sign-In enabled but no valid clientId found");process.exit(1);}const plistTargets=[infoPlistPath,infoReleasePlistPath];const pluginUrlSchemes=pluginComposition.urlSchemes||[];const pluginQuerySchemes=pluginComposition.querySchemes||[];const pluginInfoPlist=pluginComposition.infoPlist||{};plistTargets.forEach(plistPath=>{if(!fs.existsSync(plistPath)){return;}restoreManagedFileFromBaseline(plistPath);const plistObject=readPlistObject(plistPath);plistObject.CFBundleDisplayName=iosConfig.appName||"Catalyst Application";mergeIntoTopLevelObject(plistObject,pluginInfoPlist,"ios.infoPlist");if(isGoogleSignInEnabled&&resolvedReversedClientId){mergeUrlSchemes(plistObject,[{name:"googleSignIn",schemes:[resolvedReversedClientId]}]);mergeQuerySchemes(plistObject,["google",resolvedReversedClientId]);}mergeUrlSchemes(plistObject,pluginUrlSchemes);mergeQuerySchemes(plistObject,pluginQuerySchemes);writePlistObject(plistPath,plistObject);});}catch(err){progress.fail("config",err);process.exit(1);}}async function updateEntitlements(pluginComposition={}){try{const entitlementsPath=path.join(PROJECT_DIR,PROJECT_NAME,`${PROJECT_NAME}.entitlements`);if(!fs.existsSync(entitlementsPath)){return;}restoreManagedFileFromBaseline(entitlementsPath);const entitlementsObject=readPlistObject(entitlementsPath);mergeIntoTopLevelObject(entitlementsObject,pluginComposition.entitlements||{},"ios.entitlements");writePlistObject(entitlementsPath,entitlementsObject);}catch(error){progress.fail("config",error);process.exit(1);}}async function removePluginResourcesFromXcodeProject(){try{const projectFilePath=getXcodeProjectFilePath();let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch(err){if(err.code==="ENOENT")return;throw err;}const patterns=[/\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ \*\/ = \{isa = PBXFileReference;[^\n]*path = [^;]*PluginResources\/[^;]+; sourceTree = "<group>"; \};\n/g,/\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ in Resources \*\/ = \{isa = PBXBuildFile;[^\n]*\};\n/g,/\t\t\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ \*\/,\n/g,/\t\t\t\t[A-F0-9]+ \/\* PluginResources\/[^*]+ in Resources \*\/,\n/g];let modified=false;for(const pattern of patterns){const nextContent=projectContent.replace(pattern,"");if(nextContent!==projectContent){projectContent=nextContent;modified=true;}}if(modified){fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log("Removed managed plugin resources from Xcode project","info");}}catch(error){throw new Error(`Could not remove plugin resources from Xcode project: ${error.message}`);}}async function addPluginResourcesToXcodeProject(resources){try{if(resources.length===0){return;}const projectFilePath=getXcodeProjectFilePath();let projectContent;try{projectContent=fs.readFileSync(projectFilePath,"utf8");}catch(err){if(err.code==="ENOENT"){throw new Error(`Xcode project file not found at: ${projectFilePath}`);}throw err;}for(const resource of resources){const label=resource.bundleRelativePath;const fileRefId=generateProjectObjectId(label,"_ref");const buildFileId=generateProjectObjectId(label,"_build");const pbxprojPath=formatPbxprojPath(label);const fileType=detectPbxprojFileType(label);const fileRefEntry=`\t\t${fileRefId} /* ${label} */ = {isa = PBXFileReference; lastKnownFileType = ${fileType}; path = ${pbxprojPath}; sourceTree = "<group>"; };`;projectContent=replaceRequired(projectContent,/(\/\* End PBXFileReference section \*\/)/,`${fileRefEntry}\n$1`,"PBXFileReference section not found while registering plugin resources");const buildFileEntry=`\t\t${buildFileId} /* ${label} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRefId} /* ${label} */; };`;projectContent=replaceRequired(projectContent,/(\/\* End PBXBuildFile section \*\/)/,`${buildFileEntry}\n$1`,"PBXBuildFile section not found while registering plugin resources");const groupPattern=/(\/\* iosnativeWebView \*\/ = \{[^}]*children = \([^)]*)/;projectContent=replaceRequired(projectContent,groupPattern,`$1\n\t\t\t\t${fileRefId} /* ${label} */,`,"iosnativeWebView group not found while registering plugin resources");const resourcesPattern=/(\/\* Resources \*\/ = \{[^}]*files = \([^)]*)/;projectContent=replaceRequired(projectContent,resourcesPattern,`$1\n\t\t\t\t${buildFileId} /* ${label} in Resources */,`,"Resources build phase not found while registering plugin resources");}fs.writeFileSync(projectFilePath,projectContent,"utf8");progress.log(`Registered ${resources.length} managed plugin resource(s) in Xcode project`,"success");}catch(error){throw new Error(`Could not add plugin resources to Xcode project: ${error.message}`);}}async function syncPluginResources(pluginComposition={}){try{const resources=pluginComposition.resources||[];const pluginResourceDir=path.join(PROJECT_DIR,PROJECT_NAME,PLUGIN_RESOURCE_ROOT);fs.rmSync(pluginResourceDir,{recursive:true,force:true});await removePluginResourcesFromXcodeProject();if(resources.length===0){progress.log("No managed plugin resources to sync","info");return;}for(const resource of resources){const normalizedBundleRelativePath=path.posix.normalize(resource.bundleRelativePath||"");const expectedPrefix=`${PLUGIN_RESOURCE_ROOT}/`;if(!normalizedBundleRelativePath.startsWith(expectedPrefix)||normalizedBundleRelativePath.includes("../")||path.posix.isAbsolute(normalizedBundleRelativePath)){throw new Error(`Invalid managed plugin resource path: ${resource.bundleRelativePath}`);}const targetPath=path.resolve(PROJECT_DIR,PROJECT_NAME,normalizedBundleRelativePath);if(targetPath!==pluginResourceDir&&!targetPath.startsWith(`${pluginResourceDir}${path.sep}`)){throw new Error(`Managed plugin resource escaped bundle directory: ${resource.bundleRelativePath}`);}fs.mkdirSync(path.dirname(targetPath),{recursive:true});fs.copyFileSync(resource.sourcePath,targetPath);}await addPluginResourcesToXcodeProject(resources);progress.log(`Synced ${resources.length} managed plugin resource(s)`,"success");}catch(error){progress.log(`❌ Failed to sync plugin resources: ${error.message}`,"error");throw error;}}// Function to convert JSON value to Swift property
|
|
71
63
|
function generateSwiftProperty(key,value,indent=" "){if(value===null||value===undefined){return`${indent}public static let ${key}: String? = nil`;}// Special handling for cachePattern - always convert to array
|
|
72
64
|
if(key==="cachePattern"){if(typeof value==="string"){// Convert single string to array
|
|
73
65
|
return`${indent}public static let ${key}: [String] = ["${value}"]`;}else if(Array.isArray(value)){const arrayValues=value.map(v=>`"${v}"`).join(", ");return`${indent}public static let ${key}: [String] = [${arrayValues}]`;}}if(typeof value==="string"){return`${indent}public static let ${key} = "${value}"`;}if(typeof value==="number"){return Number.isInteger(value)?`${indent}public static let ${key} = ${value}`:`${indent}public static let ${key} = ${value}`;}if(typeof value==="boolean"){return`${indent}public static let ${key} = ${value}`;}if(Array.isArray(value)){if(value.length===0){return`${indent}public static let ${key}: [String] = []`;}// Determine array type from first element
|
|
@@ -282,7 +274,7 @@ progress.log("Opening Simulator.app...");await runCommand("open -a Simulator");/
|
|
|
282
274
|
await new Promise(resolve=>setTimeout(resolve,1000));// Activate the Simulator.app window to bring it to front
|
|
283
275
|
await runCommand("osascript -e 'tell application \"Simulator\" to activate'");progress.log("iOS Simulator launched successfully.","success");progress.complete("launchSimulator");}catch(error){progress.fail("launchSimulator",error.message);// Show detailed troubleshooting info
|
|
284
276
|
progress.printTreeContent("Simulator Troubleshooting",["iOS Simulator failed to launch. Common solutions:",{text:"Delete and recreate the simulator in Xcode",indent:1,prefix:"├─ ",color:"yellow"},{text:"Reset simulator content: Device > Erase All Content and Settings",indent:1,prefix:"├─ ",color:"yellow"},{text:"Check available simulators: xcrun simctl list devices",indent:1,prefix:"├─ ",color:"yellow"},{text:"Restart Xcode and Simulator app",indent:1,prefix:"└─ ",color:"yellow"},"","Error Details:",{text:`Simulator: ${simulatorName}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Error: ${error.message}`,indent:1,prefix:"└─ ",color:"red"}]);console.error("Failed to launch iOS Simulator. Error:",error.message);process.exit(1);}}// Separate build function with proper device routing
|
|
285
|
-
async function buildForIOS(){const originalDir=process.cwd();try{await generatePackageSwift();await updateXcodeProjectPackageDependencies();// Process notification assets
|
|
277
|
+
async function buildForIOS(pluginComposition={}){const originalDir=process.cwd();try{await generatePackageSwift(pluginComposition.iosDependencies);await updateXcodeProjectPackageDependencies();// Process notification assets
|
|
286
278
|
progress.start("assets");await processNotificationAssets(WEBVIEW_CONFIG);await copyOfflinePage();progress.complete("assets");await generateXCConfig();await copySplashscreenAssets();await copyAppIcon();progress.log("Changing directory to: "+PROJECT_DIR,"info");process.chdir(PROJECT_DIR);// Force physical device detection (bypass shouldUsePhysicalDevice check)
|
|
287
279
|
const physicalDevice=await detectPhysicalDevices();let APP_PATH;let targetInfo;if(physicalDevice){// Physical device workflow
|
|
288
280
|
progress.log("🔥 Building for physical device workflow","success");targetInfo={type:"physical",name:physicalDevice.name,udid:physicalDevice.udid};await cleanBuildArtifacts();progress.start("build");try{await buildProjectForPhysicalDevice(SCHEME_NAME,APP_BUNDLE_ID,path.join(process.env.HOME,"Library/Developer/Xcode/DerivedData"),PROJECT_NAME,physicalDevice);progress.complete("build");}catch(error){progress.fail("build",error.message);progress.printTreeContent("Physical Device Build Failed",["Build failed. Please check:",{text:"Code signing certificates are properly installed",indent:1,prefix:"├─ ",color:"yellow"},{text:"Provisioning profile matches your bundle ID",indent:1,prefix:"├─ ",color:"yellow"},{text:"Device is connected and trusted",indent:1,prefix:"└─ ",color:"yellow"}]);throw error;}progress.start("findApp");try{APP_PATH=await findPhysicalDeviceAppPath();progress.log("Found app at: "+APP_PATH,"success");progress.complete("findApp");}catch(error){progress.fail("findApp",error.message);throw error;}await installAndLaunchOnPhysicalDevice(APP_PATH,physicalDevice);}else{// Simulator workflow (with moveAppToBuildOutput improvement)
|
|
@@ -295,7 +287,7 @@ const destinationPath=`${imagesetDir}/launchscreen.${ext}`;fs.copyFileSync(sourc
|
|
|
295
287
|
const contentsJson={images:[{filename:`launchscreen.${ext}`,idiom:"universal",scale:"1x"}],info:{author:"xcode",version:1}};fs.writeFileSync(`${imagesetDir}/Contents.json`,JSON.stringify(contentsJson,null,2));progress.log(`Created launch screen imageset: launchscreen.${ext}`,"success");splashImageFound=true;break;}}if(!splashImageFound){progress.log("No custom splash screen image found in public folder","info");progress.log("Supported formats: splashscreen.png, splashscreen.jpg, splashscreen.jpeg","info");}}catch(error){progress.log(`Warning: Error copying splash screen assets: ${error.message}`,"warning");}}async function copyAppIcon(){try{const publicDir=`${process.env.PWD}/public/ios/appIcons`;const assetsDir=`${PROJECT_DIR}/${PROJECT_NAME}/Assets.xcassets`;const iconSetDir=`${assetsDir}/AppIcon.appiconset`;// Check if public directory exists
|
|
296
288
|
if(!fs.existsSync(publicDir)){progress.log("Public directory not found, skipping app icon copy","info");return;}// Define iPhone icon sizes with their configurations
|
|
297
289
|
const iconSizes=[{size:"20x20",idiom:"iphone",scale:"2x"},{size:"20x20",idiom:"iphone",scale:"3x"},{size:"29x29",idiom:"iphone",scale:"2x"},{size:"29x29",idiom:"iphone",scale:"3x"},{size:"40x40",idiom:"iphone",scale:"2x"},{size:"40x40",idiom:"iphone",scale:"3x"},{size:"60x60",idiom:"iphone",scale:"2x"},{size:"60x60",idiom:"iphone",scale:"3x"},{size:"1024x1024",idiom:"ios-marketing",scale:"1x"}];const imageExtensions=["png","jpg","jpeg"];// Recursively find all image files in a directory
|
|
298
|
-
const findImagesRecursively=(dir,extensions)=>{let results=[];try{const items=fs.readdirSync(dir);for(const item of items){const fullPath
|
|
290
|
+
const findImagesRecursively=(dir,extensions)=>{let results=[];try{const items=fs.readdirSync(dir);for(const item of items){const fullPath=`${dir}${path.sep}${item}`;const stat=fs.statSync(fullPath);if(stat.isDirectory()){results=results.concat(findImagesRecursively(fullPath,extensions));}else if(stat.isFile()){const ext=path.extname(item).toLowerCase().slice(1);if(extensions.includes(ext)){results.push(fullPath);}}}}catch(err){// Ignore directory read errors
|
|
299
291
|
}return results;};// Get all image files from public directory
|
|
300
292
|
const allImages=findImagesRecursively(publicDir,imageExtensions);const foundIcons=[];// Create icon set directory
|
|
301
293
|
if(!fs.existsSync(iconSetDir)){fs.mkdirSync(iconSetDir,{recursive:true});}// Load existing Contents.json or create new
|
|
@@ -309,4 +301,4 @@ fs.copyFileSync(foundImage.path,destinationPath);foundIcons.push({size,scale,fil
|
|
|
309
301
|
const iconKey=`${size}-${idiom}-${scale}`;addedIcons.add(iconKey);// Remove existing entry with same size/idiom/scale
|
|
310
302
|
contents.images=contents.images.filter(img=>`${img.size}-${img.idiom}-${img.scale}`!==iconKey);// Add new entry
|
|
311
303
|
contents.images.push({size,idiom,scale,filename});}}if(foundIcons.length>0){// Write updated Contents.json
|
|
312
|
-
fs.writeFileSync(contentsPath,JSON.stringify(contents,null,2));progress.log(`Updated AppIcon.appiconset with ${foundIcons.length} icon(s):`,"success");foundIcons.forEach(icon=>{progress.log(` • ${icon.size} @${icon.scale} (${icon.idiom})`,"info");});}else{progress.log("No app icon files found in public folder","info");progress.log("Expected naming pattern: icon-{size}-{scale}.{ext}","info");progress.log("Example: icon-20x20-2x.png, icon-60x60-3x.png, icon-1024x1024-1x.png","info");}}catch(error){progress.log(`Warning: Error copying app icons: ${error.message}`,"warning");}}async function main(){try{progress.log("Starting build process...","info");await generateConfigConstants();await updateInfoPlist();await buildForIOS();}catch(error){progress.log("Build failed: "+error.message,"error");process.exit(1);}process.exit(0);}main();
|
|
304
|
+
fs.writeFileSync(contentsPath,JSON.stringify(contents,null,2));progress.log(`Updated AppIcon.appiconset with ${foundIcons.length} icon(s):`,"success");foundIcons.forEach(icon=>{progress.log(` • ${icon.size} @${icon.scale} (${icon.idiom})`,"info");});}else{progress.log("No app icon files found in public folder","info");progress.log("Expected naming pattern: icon-{size}-{scale}.{ext}","info");progress.log("Example: icon-20x20-2x.png, icon-60x60-3x.png, icon-1024x1024-1x.png","info");}}catch(error){progress.log(`Warning: Error copying app icons: ${error.message}`,"warning");}}async function main(){try{progress.log("Starting build process...","info");const pluginConfig=resolvePluginConfig(WEBVIEW_CONFIG);const pluginComposition=composeIosPlugins({corePluginsRoot:resolveInternalPluginsRoot(catalystCorePath),iosProjectPath:PROJECT_DIR,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await generateConfigConstants();await updateInfoPlist(pluginComposition);await updateEntitlements(pluginComposition);await syncPluginResources(pluginComposition);await buildForIOS(pluginComposition);}catch(error){progress.log("Build failed: "+error.message,"error");process.exit(1);}process.exit(0);}main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");function isDir(dirPath){return fs.existsSync(dirPath)&&fs.statSync(dirPath).isDirectory();}function mustBeNonEmptyString(value,fieldName,sourcePath){if(typeof value!=="string"||!value.trim()){throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);}return value.trim();}function readStringArray(value,fieldName,sourcePath,{required=false,nonEmpty=false}={}){if(!Array.isArray(value)){if(required){throw new Error(`'${fieldName}' is required and must be an array in ${sourcePath}`);}return[];}const result=value.map(entry=>mustBeNonEmptyString(entry,`${fieldName}[]`,sourcePath));if(nonEmpty&&result.length===0){throw new Error(`'${fieldName}' is required and must be non-empty in ${sourcePath}`);}return result;}function readPlainObject(value,fieldName,sourcePath){if(value==null){return{};}if(!value||typeof value!=="object"||Array.isArray(value)){throw new Error(`'${fieldName}' must be an object in ${sourcePath}`);}return value;}function cloneJsonValue(value,fieldName,sourcePath){try{return JSON.parse(JSON.stringify(value));}catch(error){throw new Error(`'${fieldName}' must be JSON-serializable in ${sourcePath}`);}}function readIosUrlSchemes(value,fieldName,sourcePath){if(value==null){return[];}if(!Array.isArray(value)){throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);}return value.map((entry,index)=>{const entryField=`${fieldName}[${index}]`;if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new Error(`'${entryField}' must be an object in ${sourcePath}`);}return{name:entry.name==null?null:mustBeNonEmptyString(entry.name,`${entryField}.name`,sourcePath),schemes:readStringArray(entry.schemes,`${entryField}.schemes`,sourcePath,{required:true,nonEmpty:true})};});}function readIosDependencies(value,fieldName,sourcePath){if(value==null){return[];}if(!Array.isArray(value)){throw new Error(`'${fieldName}' must be an array in ${sourcePath}`);}return value.map((entry,index)=>{const entryField=`${fieldName}[${index}]`;if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new Error(`'${entryField}' must be an object in ${sourcePath}`);}const url=mustBeNonEmptyString(entry.url,`${entryField}.url`,sourcePath);const packageIdentity=entry.package==null?derivePackageIdentityFromUrl(url,`${entryField}.url`,sourcePath):mustBeNonEmptyString(entry.package,`${entryField}.package`,sourcePath);const products=readStringArray(entry.products,`${entryField}.products`,sourcePath,{required:true,nonEmpty:true});if(new Set(products).size!==products.length){throw new Error(`Duplicate product(s) found in '${entryField}.products' in ${sourcePath}`);}const hasFrom=entry.from!=null;const hasExact=entry.exact!=null;if(hasFrom===hasExact){throw new Error(`'${entryField}' must define exactly one version requirement: 'from' or 'exact' in ${sourcePath}`);}return{url,package:packageIdentity,products,requirement:hasFrom?{type:"from",version:mustBeNonEmptyString(entry.from,`${entryField}.from`,sourcePath)}:{type:"exact",version:mustBeNonEmptyString(entry.exact,`${entryField}.exact`,sourcePath)}};});}function derivePackageIdentityFromUrl(url,fieldName,sourcePath){const sanitizedUrl=url.replace(/\/+$/,"");const packageIdentity=sanitizedUrl.split("/").pop()?.replace(/\.git$/,"");if(!packageIdentity){throw new Error(`Unable to derive package identity from '${fieldName}' in ${sourcePath}`);}return packageIdentity;}function parsePluginManifest(pluginDir){const manifestPath=path.join(pluginDir,"manifest.json");if(!fs.existsSync(manifestPath)){return null;}let manifestContent;try{manifestContent=fs.readFileSync(manifestPath,"utf8");}catch(error){throw new Error(`Failed to read plugin manifest at ${manifestPath}: ${error.message}`);}let manifest;try{manifest=JSON.parse(manifestContent);}catch(error){throw new Error(`Invalid JSON in plugin manifest ${manifestPath}: ${error.message}`);}const id=mustBeNonEmptyString(manifest.id,"id",manifestPath);const configKey=mustBeNonEmptyString(manifest.configKey,"configKey",manifestPath);const platforms=readStringArray(manifest.platforms,"platforms",manifestPath,{required:true,nonEmpty:true});const androidConfig=platforms.includes("android")?manifest.android:null;const iosConfig=platforms.includes("ios")?manifest.ios:null;if(platforms.includes("android")&&(!androidConfig||typeof androidConfig!=="object")){throw new Error(`'android' config is required for plugin '${id}' in ${manifestPath}`);}if(platforms.includes("ios")&&(!iosConfig||typeof iosConfig!=="object")){throw new Error(`'ios' config is required for plugin '${id}' in ${manifestPath}`);}return{pluginDir,manifestPath,id,configKey,version:mustBeNonEmptyString(manifest.version,"version",manifestPath),displayName:mustBeNonEmptyString(manifest.displayName,"displayName",manifestPath),description:mustBeNonEmptyString(manifest.description,"description",manifestPath),category:mustBeNonEmptyString(manifest.category,"category",manifestPath),platforms,commands:readStringArray(manifest.commands,"commands",manifestPath,{required:true,nonEmpty:true}),android:androidConfig?{sourceDir:path.join(pluginDir,"android"),permissions:readStringArray(androidConfig.permissions,"android.permissions",manifestPath),dependencies:readStringArray(androidConfig.dependencies,"android.dependencies",manifestPath),className:mustBeNonEmptyString(androidConfig.className,"android.className",manifestPath)}:null,ios:iosConfig?{sourceDir:path.join(pluginDir,"ios"),dependencies:readIosDependencies(iosConfig.dependencies,"ios.dependencies",manifestPath),className:mustBeNonEmptyString(iosConfig.className,"ios.className",manifestPath),infoPlist:cloneJsonValue(readPlainObject(iosConfig.infoPlist,"ios.infoPlist",manifestPath),"ios.infoPlist",manifestPath),urlSchemes:readIosUrlSchemes(iosConfig.urlSchemes,"ios.urlSchemes",manifestPath),querySchemes:readStringArray(iosConfig.querySchemes,"ios.querySchemes",manifestPath),entitlements:cloneJsonValue(readPlainObject(iosConfig.entitlements,"ios.entitlements",manifestPath),"ios.entitlements",manifestPath),resources:readStringArray(iosConfig.resources,"ios.resources",manifestPath)}:null};}function discoverInternalPlugins(corePluginsRoot,log=()=>{}){if(!corePluginsRoot||!isDir(corePluginsRoot)){log(`No internal plugin directory found at ${corePluginsRoot||"<empty>"}`,"info");return[];}const plugins=[];const entries=fs.readdirSync(corePluginsRoot,{withFileTypes:true}).sort((left,right)=>left.name.localeCompare(right.name));for(const entry of entries){if(!entry.isDirectory()){continue;}const pluginDir=path.join(corePluginsRoot,entry.name);const parsed=parsePluginManifest(pluginDir);if(parsed){plugins.push(parsed);}}log(`Discovered ${plugins.length} internal plugin manifest(s)`,"info");return plugins;}function resolveInternalPluginsRoot(packageRoot){const distPluginsPath=path.join(packageRoot,"dist","native","internal-plugins");const srcPluginsPath=path.join(packageRoot,"src","native","internal-plugins");return fs.existsSync(distPluginsPath)?distPluginsPath:srcPluginsPath;}function resolvePluginConfig(WEBVIEW_CONFIG){const pluginConfig={};if(WEBVIEW_CONFIG.plugins!=null){if(typeof WEBVIEW_CONFIG.plugins!=="object"||Array.isArray(WEBVIEW_CONFIG.plugins)){throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");}for(const[key,value]of Object.entries(WEBVIEW_CONFIG.plugins)){if(typeof value!=="boolean"){throw new Error(`'WEBVIEW_CONFIG.plugins.${key}' must be boolean`);}pluginConfig[key]=value;}}return pluginConfig;}module.exports={discoverInternalPlugins,parsePluginManifest,resolvePluginConfig,resolveInternalPluginsRoot};
|
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"pins" : [
|
|
3
|
+
{
|
|
4
|
+
"identity" : "appauth-ios",
|
|
5
|
+
"kind" : "remoteSourceControl",
|
|
6
|
+
"location" : "https://github.com/openid/AppAuth-iOS.git",
|
|
7
|
+
"state" : {
|
|
8
|
+
"revision" : "2781038865a80e2c425a1da12cc1327bcd56501f",
|
|
9
|
+
"version" : "1.7.6"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"identity" : "googlesignin-ios",
|
|
14
|
+
"kind" : "remoteSourceControl",
|
|
15
|
+
"location" : "https://github.com/google/GoogleSignIn-iOS",
|
|
16
|
+
"state" : {
|
|
17
|
+
"revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d",
|
|
18
|
+
"version" : "7.1.0"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"identity" : "gtm-session-fetcher",
|
|
23
|
+
"kind" : "remoteSourceControl",
|
|
24
|
+
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
|
25
|
+
"state" : {
|
|
26
|
+
"revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b",
|
|
27
|
+
"version" : "3.5.0"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"identity" : "gtmappauth",
|
|
32
|
+
"kind" : "remoteSourceControl",
|
|
33
|
+
"location" : "https://github.com/google/GTMAppAuth.git",
|
|
34
|
+
"state" : {
|
|
35
|
+
"revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a",
|
|
36
|
+
"version" : "4.1.1"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
3
39
|
{
|
|
4
40
|
"identity" : "jsonschema.swift",
|
|
5
41
|
"kind" : "remoteSourceControl",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: "BarcodeDetector")
|
|
6
|
+
|
|
7
|
+
/// Wraps AVCaptureMetadataOutput for barcode/QR detection.
|
|
8
|
+
/// Hardware-accelerated — lower CPU than Vision framework.
|
|
9
|
+
/// Fires onDetected with the raw string value and the bounding box in
|
|
10
|
+
/// AVCaptureVideoPreviewLayer coordinates (normalized metadata object rect).
|
|
11
|
+
class BarcodeDetector: NSObject {
|
|
12
|
+
|
|
13
|
+
/// Set by NativeCameraManager after all components are wired up.
|
|
14
|
+
var detectionHandler: ((String, AVMetadataObject.ObjectType, CGRect) -> Void)?
|
|
15
|
+
|
|
16
|
+
/// When true, frames still flow through the pipeline but results are not forwarded.
|
|
17
|
+
var suppressResults = false
|
|
18
|
+
|
|
19
|
+
private var metadataOutput: AVCaptureMetadataOutput?
|
|
20
|
+
private weak var previewLayer: AVCaptureVideoPreviewLayer?
|
|
21
|
+
private var activeFormats: [AVMetadataObject.ObjectType] = []
|
|
22
|
+
|
|
23
|
+
// MARK: - Setup
|
|
24
|
+
|
|
25
|
+
/// Attach metadata output to a running session. Call after session is configured but before startRunning.
|
|
26
|
+
func attach(to session: AVCaptureSession,
|
|
27
|
+
previewLayer: AVCaptureVideoPreviewLayer,
|
|
28
|
+
scanFormat: String) {
|
|
29
|
+
self.previewLayer = previewLayer
|
|
30
|
+
self.activeFormats = resolveFormats(scanFormat)
|
|
31
|
+
|
|
32
|
+
let output = AVCaptureMetadataOutput()
|
|
33
|
+
guard session.canAddOutput(output) else {
|
|
34
|
+
logger.error("Cannot add AVCaptureMetadataOutput to session")
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
session.addOutput(output)
|
|
38
|
+
output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
|
39
|
+
|
|
40
|
+
// Set types AFTER adding to session
|
|
41
|
+
let supported = output.availableMetadataObjectTypes
|
|
42
|
+
let filtered = activeFormats.filter { supported.contains($0) }
|
|
43
|
+
let typesToSet = filtered.isEmpty
|
|
44
|
+
? (supported.contains(.qr) ? [AVMetadataObject.ObjectType.qr] : Array(supported.prefix(1)))
|
|
45
|
+
: filtered
|
|
46
|
+
output.metadataObjectTypes = typesToSet
|
|
47
|
+
|
|
48
|
+
self.metadataOutput = output
|
|
49
|
+
logger.debug("BarcodeDetector attached — formats: \(filtered.map { $0.rawValue })")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func detach(from session: AVCaptureSession) {
|
|
53
|
+
if let output = metadataOutput {
|
|
54
|
+
session.removeOutput(output)
|
|
55
|
+
}
|
|
56
|
+
metadataOutput = nil
|
|
57
|
+
previewLayer = nil
|
|
58
|
+
logger.debug("BarcodeDetector detached")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// MARK: - Format resolution
|
|
62
|
+
|
|
63
|
+
private func resolveFormats(_ scan: String) -> [AVMetadataObject.ObjectType] {
|
|
64
|
+
switch scan {
|
|
65
|
+
case "qr":
|
|
66
|
+
return [.qr]
|
|
67
|
+
case "barcode":
|
|
68
|
+
return [.ean13, .ean8, .code128, .code39, .upce, .pdf417, .interleaved2of5]
|
|
69
|
+
default: // "all"
|
|
70
|
+
return [.qr, .ean13, .ean8, .code128, .code39, .upce, .pdf417, .interleaved2of5, .dataMatrix, .aztec]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// MARK: - Format name helper (mirrors Android BarcodeDetector.formatName)
|
|
75
|
+
|
|
76
|
+
static func formatName(_ type: AVMetadataObject.ObjectType) -> String {
|
|
77
|
+
switch type {
|
|
78
|
+
case .qr: return "QR"
|
|
79
|
+
case .ean13: return "EAN_13"
|
|
80
|
+
case .ean8: return "EAN_8"
|
|
81
|
+
case .code128: return "CODE_128"
|
|
82
|
+
case .code39: return "CODE_39"
|
|
83
|
+
case .upce: return "UPC_E"
|
|
84
|
+
case .pdf417: return "PDF417"
|
|
85
|
+
case .dataMatrix: return "DATA_MATRIX"
|
|
86
|
+
case .aztec: return "AZTEC"
|
|
87
|
+
case .interleaved2of5: return "ITF"
|
|
88
|
+
default: return "UNKNOWN"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// MARK: - AVCaptureMetadataOutputObjectsDelegate
|
|
94
|
+
|
|
95
|
+
extension BarcodeDetector: AVCaptureMetadataOutputObjectsDelegate {
|
|
96
|
+
func metadataOutput(_ output: AVCaptureMetadataOutput,
|
|
97
|
+
didOutput metadataObjects: [AVMetadataObject],
|
|
98
|
+
from connection: AVCaptureConnection) {
|
|
99
|
+
guard !suppressResults else { return }
|
|
100
|
+
|
|
101
|
+
for obj in metadataObjects {
|
|
102
|
+
guard let machineReadable = obj as? AVMetadataMachineReadableCodeObject,
|
|
103
|
+
let value = machineReadable.stringValue else { continue }
|
|
104
|
+
|
|
105
|
+
// Transform bounding box from metadata coordinates into preview layer bounds
|
|
106
|
+
let bounds: CGRect
|
|
107
|
+
if let transformed = previewLayer?.transformedMetadataObject(for: machineReadable) {
|
|
108
|
+
bounds = transformed.bounds
|
|
109
|
+
} else {
|
|
110
|
+
bounds = .zero
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
detectionHandler?(value, machineReadable.type, bounds)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|