Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit ede7a09

Browse files
authored
Merge pull request #5 from ProStore-iOS/main
Update branch
2 parents 32df3a3 + d45403e commit ede7a09

3 files changed

Lines changed: 72 additions & 151 deletions

File tree

Sources/prostore/install/installApp.swift

Lines changed: 68 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// installApp.swift
21
import Foundation
32
import IDeviceSwift
43
import Combine
@@ -7,75 +6,34 @@ import Combine
76
private func transformInstallError(_ error: Error) -> Error {
87
let nsError = error as NSError
98
let errorString = String(describing: error)
10-
11-
// Extract real error message from various formats
12-
var userMessage = extractUserReadableErrorMessage(from: error)
13-
14-
// If we got a good message, use it
15-
if let userMessage = userMessage, !userMessage.isEmpty {
16-
return NSError(
17-
domain: nsError.domain,
18-
code: nsError.code,
19-
userInfo: [NSLocalizedDescriptionKey: userMessage]
20-
)
9+
10+
if let userMessage = extractUserReadableErrorMessage(from: error), !userMessage.isEmpty {
11+
return NSError(domain: nsError.domain, code: nsError.code, userInfo: [NSLocalizedDescriptionKey: userMessage])
2112
}
22-
23-
// Fallback: Generic error 1 handling
13+
2414
if errorString.contains("error 1.") {
25-
// Check for specific patterns in the error string
2615
if errorString.contains("Missing Pairing") {
27-
return NSError(
28-
domain: nsError.domain,
29-
code: nsError.code,
30-
userInfo: [NSLocalizedDescriptionKey: "Missing pairing file. Please ensure pairing file exists in ProStore folder."]
31-
)
16+
return NSError(domain: nsError.domain, code: nsError.code, userInfo: [NSLocalizedDescriptionKey: "Missing pairing file. Please ensure pairing file exists in ProStore folder."])
3217
}
33-
3418
if errorString.contains("Cannot connect to AFC") || errorString.contains("afc_client_connect") {
35-
return NSError(
36-
domain: nsError.domain,
37-
code: nsError.code,
38-
userInfo: [NSLocalizedDescriptionKey: "Cannot connect to AFC. Check USB connection, VPN, and accept trust dialog on device."]
39-
)
19+
return NSError(domain: nsError.domain, code: nsError.code, userInfo: [NSLocalizedDescriptionKey: "Cannot connect to AFC. Check USB connection, VPN, and accept trust dialog on device."])
4020
}
41-
4221
if errorString.contains("installation_proxy") {
43-
return NSError(
44-
domain: nsError.domain,
45-
code: nsError.code,
46-
userInfo: [NSLocalizedDescriptionKey: "Installation service failed. The app may already be installed or device storage full."]
47-
)
22+
return NSError(domain: nsError.domain, code: nsError.code, userInfo: [NSLocalizedDescriptionKey: "Installation service failed. The app may already be installed or device storage full."])
4823
}
49-
50-
// Generic error 1 message
51-
return NSError(
52-
domain: nsError.domain,
53-
code: nsError.code,
54-
userInfo: [NSLocalizedDescriptionKey: "Installation failed. Make sure: 1) VPN is on, 2) Device is connected via USB, 3) Trust dialog is accepted, 4) Pairing file is in ProStore folder."]
55-
)
24+
return NSError(domain: nsError.domain, code: nsError.code, userInfo: [NSLocalizedDescriptionKey: "Installation failed. Make sure: 1) VPN is on, 2) Device is connected via USB, 3) Trust dialog is accepted, 4) Pairing file is in ProStore folder."])
5625
}
57-
58-
// Try to clean up the generic message
59-
let originalMessage = nsError.localizedDescription
60-
let cleanedMessage = cleanGenericErrorMessage(originalMessage)
61-
62-
return NSError(
63-
domain: nsError.domain,
64-
code: nsError.code,
65-
userInfo: [NSLocalizedDescriptionKey: cleanedMessage]
66-
)
26+
27+
let cleanedMessage = cleanGenericErrorMessage(nsError.localizedDescription)
28+
return NSError(domain: nsError.domain, code: nsError.code, userInfo: [NSLocalizedDescriptionKey: cleanedMessage])
6729
}
6830

69-
// Extract user-readable message from error
7031
private func extractUserReadableErrorMessage(from error: Error) -> String? {
71-
// Try to get error description from LocalizedError
7232
if let localizedError = error as? LocalizedError {
7333
return localizedError.errorDescription
7434
}
75-
35+
7636
let errorString = String(describing: error)
77-
78-
// Look for specific error patterns in the string representation
7937
let patterns = [
8038
"Missing Pairing": "Missing pairing file. Please check ProStore folder.",
8139
"Cannot connect to AFC": "Cannot connect to device. Check USB and VPN.",
@@ -84,158 +42,120 @@ private func extractUserReadableErrorMessage(from error: Error) -> String? {
8442
"File Error:": "File operation failed.",
8543
"Connection Failed:": "Connection to device failed."
8644
]
87-
88-
for (pattern, message) in patterns {
89-
if errorString.contains(pattern) {
90-
return message
91-
}
45+
46+
for (pattern, message) in patterns where errorString.contains(pattern) {
47+
return message
9248
}
93-
94-
// Try to extract from NSError userInfo
49+
9550
let nsError = error as NSError
9651
if let userInfoMessage = nsError.userInfo[NSLocalizedDescriptionKey] as? String,
9752
!userInfoMessage.isEmpty,
9853
userInfoMessage != nsError.localizedDescription {
9954
return userInfoMessage
10055
}
101-
56+
10257
return nil
10358
}
10459

105-
// Clean up generic error messages
10660
private func cleanGenericErrorMessage(_ message: String) -> String {
10761
var cleaned = message
108-
109-
// Remove the generic prefix
11062
let genericPrefixes = [
11163
"The operation couldn't be completed. ",
11264
"The operation could not be completed. ",
11365
"IDeviceSwift.IDeviceSwiftError ",
11466
"IDeviceSwiftError "
11567
]
116-
117-
for prefix in genericPrefixes {
118-
if cleaned.hasPrefix(prefix) {
119-
cleaned = String(cleaned.dropFirst(prefix.count))
120-
break
121-
}
122-
}
123-
124-
// Remove trailing period if present
125-
if cleaned.hasSuffix(".") {
126-
cleaned = String(cleaned.dropLast())
68+
for prefix in genericPrefixes where cleaned.hasPrefix(prefix) {
69+
cleaned = String(cleaned.dropFirst(prefix.count))
70+
break
12771
}
128-
129-
// If it's just "error 1.", provide more helpful message
72+
if cleaned.hasSuffix(".") { cleaned = String(cleaned.dropLast()) }
13073
if cleaned == "error 1" || cleaned == "error 1." {
13174
return "Device installation failed. Please check: 1) VPN connection, 2) USB cable, 3) Trust dialog, 4) Pairing file."
13275
}
133-
13476
return cleaned.isEmpty ? "Unknown installation error" : cleaned
13577
}
13678

13779
// MARK: - Install App
13880
/// Installs a signed IPA on the device using InstallationProxy
139-
public func installApp(from ipaURL: URL) async throws
140-
-> AsyncThrowingStream<(progress: Double, status: String), Error> {
81+
public func installApp(from ipaURL: URL) async throws -> AsyncThrowingStream<(progress: Double, status: String), Error> {
14182

142-
// Pre-flight check: verify IPA exists
83+
// Pre-flight IPA check
14384
let fileManager = FileManager.default
14485
guard fileManager.fileExists(atPath: ipaURL.path) else {
145-
throw NSError(
146-
domain: "InstallApp",
147-
code: -1,
148-
userInfo: [NSLocalizedDescriptionKey: "IPA file not found: \(ipaURL.lastPathComponent)"]
149-
)
86+
throw NSError(domain: "InstallApp", code: -1, userInfo: [NSLocalizedDescriptionKey: "IPA file not found: \(ipaURL.lastPathComponent)"])
15087
}
151-
152-
// Check file size
88+
89+
// Validate file size
15390
do {
15491
let attributes = try fileManager.attributesOfItem(atPath: ipaURL.path)
15592
let fileSize = attributes[.size] as? Int64 ?? 0
15693
guard fileSize > 1024 else {
157-
throw NSError(
158-
domain: "InstallApp",
159-
code: -1,
160-
userInfo: [NSLocalizedDescriptionKey: "IPA file is too small or invalid"]
161-
)
94+
throw NSError(domain: "InstallApp", code: -1, userInfo: [NSLocalizedDescriptionKey: "IPA file is too small or invalid"])
16295
}
16396
} catch {
164-
throw NSError(
165-
domain: "InstallApp",
166-
code: -1,
167-
userInfo: [NSLocalizedDescriptionKey: "Cannot read IPA file"]
168-
)
97+
throw NSError(domain: "InstallApp", code: -1, userInfo: [NSLocalizedDescriptionKey: "Cannot read IPA file"])
16998
}
17099

171100
print("Installing app from: \(ipaURL.path)")
172101

173-
return AsyncThrowingStream { continuation in
174-
Task {
175-
// Start heartbeat to keep connection alive
176-
HeartbeatManager.shared.start()
102+
typealias InstallUpdate = (progress: Double, status: String)
103+
typealias StreamContinuation = AsyncThrowingStream<InstallUpdate, Error>.Continuation
177104

178-
let viewModel = InstallerStatusViewModel()
179-
var cancellables = Set<AnyCancellable>()
105+
return AsyncThrowingStream<InstallUpdate, Error> { continuation in
106+
var cancellables = Set<AnyCancellable>()
107+
var installTask: Task<Void, Never>?
180108

181-
// Progress stream
182-
viewModel.$uploadProgress
183-
.combineLatest(viewModel.$installProgress)
184-
.sink { uploadProgress, installProgress in
185-
let overallProgress = (uploadProgress + installProgress) / 2.0
186-
let status: String
187-
188-
if uploadProgress < 1.0 {
189-
status = "📤 Uploading..."
190-
} else if installProgress < 1.0 {
191-
status = "📲 Installing..."
192-
} else {
193-
status = "🏁 Finalizing..."
194-
}
109+
continuation.onTermination = { @Sendable reason in
110+
print("Install stream terminated: \(reason)")
111+
cancellables.removeAll()
112+
installTask?.cancel()
113+
}
195114

196-
continuation.yield((overallProgress, status))
197-
}
198-
.store(in: &cancellables)
115+
installTask = Task {
116+
HeartbeatManager.shared.start()
117+
let isIdevice = UserDefaults.standard.integer(forKey: "ProStore.installationMethod") == 1
118+
let viewModel = InstallerStatusViewModel(isIdevice: isIdevice)
199119

200-
// Completion handling
120+
// Status updates
201121
viewModel.$status
202-
.sink { installerStatus in
203-
switch installerStatus {
204-
205-
case .completed(.success):
122+
.sink { newStatus in
123+
if viewModel.isCompleted {
124+
print("[Installer] detected completion via isCompleted")
206125
continuation.yield((1.0, "✅ Successfully installed app!"))
207126
continuation.finish()
208127
cancellables.removeAll()
209-
210-
case .completed(.failure(let error)):
211-
continuation.finish(
212-
throwing: transformInstallError(error)
213-
)
214-
cancellables.removeAll()
215-
216-
case .broken(let error):
217-
continuation.finish(
218-
throwing: transformInstallError(error)
219-
)
128+
}
129+
if case .broken(let error) = newStatus {
130+
continuation.finish(throwing: transformInstallError(error))
220131
cancellables.removeAll()
221-
222-
default:
223-
break
224132
}
225133
}
226134
.store(in: &cancellables)
227135

136+
// Progress stream (upload + install)
137+
viewModel.$uploadProgress
138+
.combineLatest(viewModel.$installProgress)
139+
.sink { upload, install in
140+
let overall = (upload + install) / 2
141+
let statusText: String
142+
if upload < 1.0 { statusText = "📤 Uploading..." }
143+
else if install < 1.0 { statusText = "📲 Installing..." }
144+
else { statusText = "🏁 Finalizing..." }
145+
print("[Installer] progress upload:\(upload) install:\(install) overall:\(overall)")
146+
continuation.yield((overall, statusText))
147+
}
148+
.store(in: &cancellables)
149+
228150
do {
229151
let installer = await InstallationProxy(viewModel: viewModel)
230152
try await installer.install(at: ipaURL)
231-
232-
try await Task.sleep(nanoseconds: 1_000_000_000)
233-
print("Installation completed successfully!")
234-
153+
try await Task.sleep(nanoseconds: 500_000_000)
154+
print("Installation call returned — waiting for viewModel to report completion.")
155+
// Stream finishes when viewModel.isCompleted becomes true or status reports broken/error
235156
} catch {
236-
continuation.finish(
237-
throwing: transformInstallError(error)
238-
)
157+
print("[Installer] install threw error ->", error)
158+
continuation.finish(throwing: transformInstallError(error))
239159
cancellables.removeAll()
240160
}
241161
}

Sources/prostore/views/AppsView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public struct AppRaw: Decodable {
8686
}
8787
}
8888
}
89-
public struct AppVersion: Decodable {
89+
public struct AppVersion: Decodable, Sendable {
9090
let version: String?
9191
let date: String?
9292
let downloadURL: String?

project.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ targets:
3636
properties:
3737
CFBundleDisplayName: "ProStore"
3838
CFBundleName: "prostore"
39-
CFBundleVersion: "70"
40-
CFBundleShortVersionString: "1.2.8"
39+
CFBundleVersion: "71"
40+
CFBundleShortVersionString: "1.2.9"
4141
UILaunchStoryboardName: "LaunchScreen"
4242
NSPrincipalClass: "UIApplication"
4343
NSAppTransportSecurity:
@@ -59,3 +59,4 @@ targets:
5959
- package: IDeviceKit
6060

6161
product: IDeviceSwift
62+

0 commit comments

Comments
 (0)