@@ -6,51 +6,180 @@ import Combine
66// MARK: - Error Transformer
77private func transformInstallError( _ error: Error ) -> Error {
88 let nsError = error as NSError
9- let description = nsError. localizedDescription
10-
11- // Exact LocalDevVPN VPN error
12- if description. contains ( " IDeviceSwiftError " )
13- && description. contains ( " error 1. " ) {
9+
10+ // First, extract any meaningful message from the error string
11+ let errorString = String ( describing: error)
12+
13+ // Check for specific IDeviceSwift error patterns
14+ if let ideviceMessage = extractIDeviceErrorMessage ( from: errorString) {
1415 return NSError (
1516 domain: nsError. domain,
1617 code: nsError. code,
1718 userInfo: [
18- NSLocalizedDescriptionKey:
19- " iDevice failed to install the app! Make sure the LocalDevVPN VPN is turned on! "
19+ NSLocalizedDescriptionKey: " Failed to install app: \( ideviceMessage) "
2020 ]
2121 )
2222 }
23-
24- // Generic: The operation <anything> be completed. (<foo> error <bar>.)
25- // Use non-greedy capture and NSRegularExpression for robust extraction
26- let pattern = #"The operation .*? be completed\. \((.+? error .+?)\.\)"#
27-
28- let nsDesc = description as NSString
23+
24+ // Check for VPN/connection errors
25+ if errorString. contains ( " error 1. " ) {
26+ if errorString. lowercased ( ) . contains ( " vpn " ) ||
27+ errorString. lowercased ( ) . contains ( " connection " ) ||
28+ errorString. lowercased ( ) . contains ( " pairing " ) ||
29+ errorString. lowercased ( ) . contains ( " afc " ) {
30+
31+ return NSError (
32+ domain: nsError. domain,
33+ code: nsError. code,
34+ userInfo: [
35+ NSLocalizedDescriptionKey: " Failed to install app! Make sure the LocalDevVPN VPN is turned on and device is connected. "
36+ ]
37+ )
38+ }
39+ }
40+
41+ // Generic pattern extraction for "The operation couldn't be completed. (IDeviceSwiftError error 1.)"
42+ let pattern = #"The operation .*? be completed\. \((.+? error .+?)\)"#
43+ let nsDesc = errorString as NSString
44+
2945 if let regex = try ? NSRegularExpression ( pattern: pattern) ,
30- let match = regex. firstMatch ( in: description , range: NSRange ( location: 0 , length: nsDesc. length) ) ,
46+ let match = regex. firstMatch ( in: errorString , range: NSRange ( location: 0 , length: nsDesc. length) ) ,
3147 match. numberOfRanges > 1 {
32-
48+
3349 let inner = nsDesc. substring ( with: match. range ( at: 1 ) )
34-
3550 return NSError (
3651 domain: nsError. domain,
3752 code: nsError. code,
3853 userInfo: [
39- NSLocalizedDescriptionKey:
40- " iDevice failed to install the app! ( \( inner) .) "
54+ NSLocalizedDescriptionKey: " Failed to install app! ( \( inner) ) "
4155 ]
4256 )
4357 }
58+
59+ // Return original error with cleaned up message
60+ let originalMessage = nsError. localizedDescription
61+ let cleanedMessage = cleanErrorMessage ( originalMessage)
62+
63+ return NSError (
64+ domain: nsError. domain,
65+ code: nsError. code,
66+ userInfo: [
67+ NSLocalizedDescriptionKey: cleanedMessage
68+ ]
69+ )
70+ }
4471
45- // Otherwise: untouched
46- return error
72+ // Helper to extract IDeviceSwift specific error messages
73+ private func extractIDeviceErrorMessage( from errorString: String ) -> String ? {
74+ let lowercasedError = errorString. lowercased ( )
75+
76+ // Common IDeviceSwift error messages
77+ let errorPatterns = [
78+ " missing pairing " : " Missing pairing file. Please ensure pairing file exists in ProStore folder. " ,
79+ " cannot connect to afc " : " Cannot connect to AFC service. Check USB connection and trust dialog. " ,
80+ " missing file handle " : " File handle error during transfer. " ,
81+ " error writing to afc " : " Failed to write app to device. Check storage space. " ,
82+ " installation_proxy_connect_tcp " : " Failed to connect to installation service. " ,
83+ " afc_make_directory " : " Failed to create staging directory on device. " ,
84+ " afc_file_open " : " Failed to open file on device. " ,
85+ " afc_file_write " : " Failed to write file data to device. " ,
86+ " afc_file_close " : " Failed to close file on device. "
87+ ]
88+
89+ for (pattern, message) in errorPatterns {
90+ if lowercasedError. contains ( pattern. lowercased ( ) ) {
91+ return message
92+ }
93+ }
94+
95+ // Try to extract message from IDeviceSwiftError structure
96+ if let range = errorString. range ( of: " _message = \" " ) {
97+ let start = errorString. index ( range. upperBound, offsetBy: 0 )
98+ if let endRange = errorString [ start... ] . range ( of: " \" " ) {
99+ let message = String ( errorString [ start..< endRange. lowerBound] )
100+ if !message. isEmpty {
101+ return message
102+ }
103+ }
104+ }
105+
106+ // Extract from userInfo if available
107+ if let userInfoRange = errorString. range ( of: " userInfo = " ) {
108+ let userInfoString = String ( errorString [ userInfoRange. upperBound... ] )
109+ if let nsLocalizedRange = userInfoString. range ( of: " NSLocalizedDescription = " ) {
110+ let messageStart = userInfoString. index ( nsLocalizedRange. upperBound, offsetBy: 0 )
111+ if let messageEnd = userInfoString [ messageStart... ] . firstIndex ( of: " ; " ) {
112+ let message = String ( userInfoString [ messageStart..< messageEnd] )
113+ if !message. isEmpty && message != " (null) " {
114+ return message
115+ }
116+ }
117+ }
118+ }
119+
120+ return nil
121+ }
122+
123+ // Clean up generic error messages
124+ private func cleanErrorMessage( _ message: String ) -> String {
125+ var cleaned = message
126+
127+ // Remove redundant prefixes
128+ let prefixes = [
129+ " The operation couldn't be completed. " ,
130+ " The operation could not be completed. " ,
131+ " IDeviceSwift.IDeviceSwiftError error " ,
132+ " IDeviceSwiftError error "
133+ ]
134+
135+ for prefix in prefixes {
136+ if cleaned. hasPrefix ( prefix) {
137+ cleaned = String ( cleaned. dropFirst ( prefix. count) )
138+ }
139+ }
140+
141+ // Clean parentheses
142+ if cleaned. hasSuffix ( " . " ) {
143+ cleaned = String ( cleaned. dropLast ( ) )
144+ }
145+
146+ return cleaned. isEmpty ? " Unknown installation error " : cleaned
47147}
48148
49149// MARK: - Install App
50150/// Installs a signed IPA on the device using InstallationProxy
51151public func installApp( from ipaURL: URL ) async throws
52152-> AsyncThrowingStream < ( progress: Double , status: String ) , Error > {
53153
154+ // Pre-flight check: verify IPA exists and is valid
155+ let fileManager = FileManager . default
156+ guard fileManager. fileExists ( atPath: ipaURL. path) else {
157+ throw NSError (
158+ domain: " InstallApp " ,
159+ code: - 1 ,
160+ userInfo: [ NSLocalizedDescriptionKey: " IPA file not found at: \( ipaURL. path) " ]
161+ )
162+ }
163+
164+ // Check file size
165+ do {
166+ let attributes = try fileManager. attributesOfItem ( atPath: ipaURL. path)
167+ let fileSize = attributes [ . size] as? Int64 ?? 0
168+ guard fileSize > 1024 else { // At least 1KB
169+ throw NSError (
170+ domain: " InstallApp " ,
171+ code: - 1 ,
172+ userInfo: [ NSLocalizedDescriptionKey: " IPA file is too small or invalid " ]
173+ )
174+ }
175+ } catch {
176+ throw NSError (
177+ domain: " InstallApp " ,
178+ code: - 1 ,
179+ userInfo: [ NSLocalizedDescriptionKey: " Failed to read IPA file: \( error. localizedDescription) " ]
180+ )
181+ }
182+
54183 print ( " Installing app from: \( ipaURL. path) " )
55184
56185 return AsyncThrowingStream { continuation in
@@ -86,7 +215,7 @@ public func installApp(from ipaURL: URL) async throws
86215 switch installerStatus {
87216
88217 case . completed( . success) :
89- continuation. yield ( ( 1.0 , " Successfully installed app! " ) )
218+ continuation. yield ( ( 1.0 , " ✅ Successfully installed app!" ) )
90219 continuation. finish ( )
91220 cancellables. removeAll ( )
92221
@@ -123,4 +252,4 @@ public func installApp(from ipaURL: URL) async throws
123252 }
124253 }
125254 }
126- }
255+ }
0 commit comments