Skip to content

Commit 80c46dc

Browse files
authored
Improve user-friendly error messages.
1 parent 35b7d1e commit 80c46dc

File tree

3 files changed

+228
-56
lines changed

3 files changed

+228
-56
lines changed

Sources/prostore/install/installApp.swift

Lines changed: 151 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,180 @@ import Combine
66
// MARK: - Error Transformer
77
private 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
51151
public 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

Comments
 (0)