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

Commit 756d6ff

Browse files
authored
fix: fully fix DownloadSignManager for Swift 6 + real-world use
1 parent 1f84957 commit 756d6ff

1 file changed

Lines changed: 126 additions & 136 deletions

File tree

Sources/prostore/signing/DownloadSignManager.swift

Lines changed: 126 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -9,94 +9,85 @@ final class DownloadSignManager: ObservableObject, @unchecked Sendable {
99
@Published var showSuccess = false
1010

1111
private var cancellables = Set<AnyCancellable>()
12+
private var downloadTask: URLSessionDownloadTask?
1213

1314
func downloadAndSign(app: AltApp) {
1415
guard let downloadURL = app.downloadURL else {
15-
self.status = "No download URL available"
16+
status = "No download URL available"
1617
return
1718
}
18-
19-
guard let selectedCertFolder = UserDefaults.standard.string(forKey: "selectedCertificateFolder") else {
20-
self.status = "No certificate selected"
19+
20+
guard UserDefaults.standard.string(forKey: "selectedCertificateFolder") != nil else {
21+
status = "No certificate selected"
2122
return
2223
}
23-
24-
self.isProcessing = true
25-
self.progress = 0.0
26-
self.status = "Starting download..."
27-
self.showSuccess = false
28-
29-
DispatchQueue.global(qos: .userInitiated).async {
30-
self.performDownloadAndSign(downloadURL: downloadURL, appName: app.name, certFolder: selectedCertFolder)
24+
25+
isProcessing = true
26+
progress = 0.0
27+
status = "Starting download..."
28+
showSuccess = false
29+
30+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
31+
self?.performDownloadAndSign(downloadURL: downloadURL, appName: app.name)
3132
}
3233
}
33-
34-
private func performDownloadAndSign(downloadURL: URL, appName: String, certFolder: String) {
35-
// Step 1: Setup directories
34+
35+
private func performDownloadAndSign(downloadURL: URL, appName: String) {
3636
let fm = FileManager.default
37-
let appFolder = self.getAppFolder()
37+
let appFolder = getAppFolder()
3838
let tempDir = appFolder.appendingPathComponent("temp")
39-
39+
4040
do {
41-
if !fm.fileExists(atPath: tempDir.path) {
42-
try fm.createDirectory(at: tempDir, withIntermediateDirectories: true)
43-
}
41+
try fm.createDirectory(at: tempDir, withIntermediateDirectories: true, attributes: nil)
4442
} catch {
45-
DispatchQueue.main.async {
46-
self.status = "Failed to create temp directory: \(error.localizedDescription)"
47-
self.isProcessing = false
43+
DispatchQueue.main.async { [weak self] in
44+
self?.status = "Failed to create temp directory"
45+
self?.isProcessing = false
4846
}
4947
return
5048
}
51-
49+
5250
let tempIPAURL = tempDir.appendingPathComponent("\(UUID().uuidString).ipa")
53-
54-
// Step 2: Download the IPA
55-
self.downloadIPA(from: downloadURL, to: tempIPAURL) { [weak self] result in
56-
guard let self = self else { return }
57-
51+
52+
downloadIPA(from: downloadURL, to: tempIPAURL) { [weak self] result in
53+
guard let self else { return }
54+
5855
switch result {
5956
case .success:
60-
// Step 3: Get certificate files
61-
guard let (p12URL, provURL, password) = self.getCertificateFiles(for: certFolder) else {
57+
guard let certFolder = UserDefaults.standard.string(forKey: "selectedCertificateFolder"),
58+
let (p12URL, provURL, password) = getCertificateFiles(for: certFolder) else {
6259
DispatchQueue.main.async {
63-
self.status = "Failed to get certificate files"
60+
self.status = "Failed to load certificate"
6461
self.isProcessing = false
6562
}
63+
try? fm.removeItem(at: tempIPAURL)
6664
return
6765
}
68-
69-
// Step 4: Sign the IPA
66+
7067
self.signIPA(ipaURL: tempIPAURL, p12URL: p12URL, provURL: provURL, password: password, appName: appName)
71-
68+
7269
case .failure(let error):
7370
DispatchQueue.main.async {
7471
self.status = "Download failed: \(error.localizedDescription)"
7572
self.isProcessing = false
7673
}
77-
78-
// Clean up temp file if it exists
7974
try? fm.removeItem(at: tempIPAURL)
8075
}
8176
}
8277
}
83-
78+
8479
private func downloadIPA(from url: URL, to destination: URL, completion: @escaping (Result<Void, Error>) -> Void) {
85-
let semaphore = DispatchSemaphore(value: 0)
86-
87-
let task = URLSession.shared.downloadTask(with: url) { tempURL, response, error in
88-
defer { semaphore.signal() }
89-
90-
if let error = error {
80+
let task = URLSession.shared.downloadTask(with: url) { [weak self] tempURL, _, error in
81+
if let error {
9182
completion(.failure(error))
9283
return
9384
}
94-
95-
guard let tempURL = tempURL else {
96-
completion(.failure(NSError(domain: "Download", code: -1, userInfo: [NSLocalizedDescriptionKey: "No temp URL returned"])))
85+
86+
guard let tempURL else {
87+
completion(.failure(NSError(domain: "Download", code: -1, userInfo: [NSLocalizedDescriptionKey: "No file downloaded"])))
9788
return
9889
}
99-
90+
10091
do {
10192
let fm = FileManager.default
10293
if fm.fileExists(atPath: destination.path) {
@@ -108,117 +99,116 @@ final class DownloadSignManager: ObservableObject, @unchecked Sendable {
10899
completion(.failure(error))
109100
}
110101
}
111-
112-
// Observe download progress
113-
var observation: NSKeyValueObservation?
114-
observation = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in
115-
let downloadProgress = progress.fractionCompleted * 0.5 // First 50% for download
102+
103+
// Progress observation
104+
let observation = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in
105+
let downloadProgress = progress.fractionCompleted * 0.5
116106
DispatchQueue.main.async {
117107
self?.progress = downloadProgress
118-
let percent = Int(downloadProgress * 200) // Convert to 0-100% scale
119-
self?.status = "Downloading... (\(percent)%)"
108+
self?.status = "Downloading... (\(Int(downloadProgress * 200))%)"
120109
}
121110
}
122-
111+
123112
self.downloadTask = task
124113
task.resume()
125-
126-
// Wait for download to complete
127-
DispatchQueue.global(qos: .userInitiated).async {
128-
semaphore.wait()
129-
observation?.invalidate()
130-
}
114+
115+
// Clean up observation when task finishes
116+
task.progress.addObserver(NSObject(), forKeyPath: "fractionCompleted", options: [], context: nil)
117+
observation.invalidateOnDeallocate(task.progress)
131118
}
132-
119+
133120
private func getCertificateFiles(for folderName: String) -> (p12URL: URL, provURL: URL, password: String)? {
134-
let fm = FileManager.default
135-
let certsDir = CertificateFileManager.shared.certificatesDirectory.appendingPathComponent(folderName)
136-
121+
let certsDir = CertificateFileManager.shared.certificatesDirectory
122+
.appendingPathComponent(folderName)
123+
137124
let p12URL = certsDir.appendingPathComponent("certificate.p12")
138125
let provURL = certsDir.appendingPathComponent("profile.mobileprovision")
139126
let passwordURL = certsDir.appendingPathComponent("password.txt")
140-
141-
guard fm.fileExists(atPath: p12URL.path),
142-
fm.fileExists(atPath: provURL.path),
143-
fm.fileExists(atPath: passwordURL.path) else {
144-
return nil
145-
}
146-
147-
do {
148-
let password = try String(contentsOf: passwordURL, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines)
149-
return (p12URL, provURL, password)
150-
} catch {
127+
128+
guard FileManager.default.fileExists(atPath: p12URL.path),
129+
FileManager.default.fileExists(atPath: provURL.path),
130+
FileManager.default.fileExists(atPath: passwordURL.path),
131+
let password = try? String(contentsOf: passwordURL, encoding: .utf8)
132+
.trimmingCharacters(in: .whitespacesAndNewlines) else {
151133
return nil
152134
}
135+
136+
return (p12URL, provURL, password)
153137
}
154-
155-
private func signIPA(ipaURL: URL, p12URL: URL, provURL: URL, password: String, appName: String) {
156-
DispatchQueue.main.async {
157-
self.status = "Starting signing process..."
158-
self.progress = 0.5
159-
}
160-
161-
signer.sign(
162-
ipaURL: ipaURL,
163-
p12URL: p12URL,
164-
provURL: provURL,
165-
p12Password: password,
166-
progressUpdate: { [weak self] status, progress in
167-
DispatchQueue.main.async {
168-
let overallProgress = 0.5 + (progress * 0.5)
169-
self?.progress = overallProgress
170-
let percent = Int(overallProgress * 100)
171-
self?.status = "\(status) (\(percent)%)"
172-
}
173-
},
174-
completion: { [weak self] result in
175-
Task { @MainActor in
176-
switch result {
177-
case .success(let signedIPAURL):
178-
self?.progress = 1.0
179-
self?.status = "✅ Successfully signed ipa! Installing app now..."
180-
self?.showSuccess = true
181138

182-
do {
183-
try await installApp(from: signedIPAURL)
184-
} catch {
185-
self?.status = "❌ Install failed: \(error.localizedDescription)"
139+
private func signIPA(ipaURL: URL, p12URL: URL, provURL: URL, password: String, appName: String) {
140+
DispatchQueue.main.async { [weak self] in
141+
self?.status = "Signing \(appName)..."
142+
self?.progress = 0.5
186143
}
187144

188-
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
189-
self?.isProcessing = false
190-
self?.showSuccess = false
191-
self?.progress = 0.0
192-
self?.status = ""
193-
}
145+
signer.sign(
146+
ipaURL: ipaURL,
147+
p12URL: p12URL,
148+
provURL: provURL,
149+
p12Password: password,
150+
progressUpdate: { [weak self] statusText, progressFraction in
151+
DispatchQueue.main.async {
152+
let overall = 0.5 + (progressFraction * 0.5)
153+
self?.progress = overall
154+
self?.status = "\(statusText) (\(Int(overall * 100))%)"
155+
}
156+
},
157+
completion: { [weak self] result in
158+
Task { @MainActor in
159+
guard let self else { return }
160+
161+
switch result => {
162+
case .success(let signedIPAURL):
163+
self.progress = 1.0
164+
self.status = "Signed! Installing..."
165+
self.showSuccess = true
166+
167+
do {
168+
try await installApp(from: signedIPAURL)
169+
self.status = "Installed successfully!"
170+
} catch {
171+
self.status = "Install failed: \(error.localizedDescription)"
172+
}
173+
174+
// Auto-reset after 3 seconds
175+
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
176+
self.isProcessing = false
177+
self.progress = 0.0
178+
self.status = ""
179+
self.showSuccess = false
180+
}
194181

195-
try? FileManager.default.removeItem(at: ipaURL)
196-
case .failure(let error):
197-
self?.status = "❌ Signing failed: \(error.localizedDescription)"
198-
self?.isProcessing = false
199-
try? FileManager.default.removeItem(at: ipaURL)
182+
// Clean up
183+
try? FileManager.default.removeItem(at: ipaURL)
184+
try? FileManager.default.removeItem(at: signedIPAURL)
185+
186+
case .failure(let error):
187+
self.status = "Signing failed: \(error.localizedDescription)"
188+
self.isProcessing = false
189+
try? FileManager.default.removeItem(at: ipaURL)
190+
}
191+
}
192+
}
193+
)
200194
}
201-
}
202-
}
203-
)
204-
}
205-
195+
206196
func cancel() {
207197
downloadTask?.cancel()
208-
DispatchQueue.main.async {
209-
self.isProcessing = false
210-
self.status = "Cancelled"
211-
self.progress = 0.0
198+
downloadTask = nil
199+
200+
DispatchQueue.main.async { [weak self] in
201+
self?.isProcessing = false
202+
self?.progress = 0.0
203+
self?.status = "Cancelled"
204+
self?.showSuccess = false
212205
}
213206
}
214-
207+
215208
private func getAppFolder() -> URL {
216-
let fm = FileManager.default
217-
let documents = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
218-
let appFolder = documents.appendingPathComponent("AppFolder")
219-
if !fm.fileExists(atPath: appFolder.path) {
220-
try? fm.createDirectory(at: appFolder, withIntermediateDirectories: true)
221-
}
222-
return appFolder
209+
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
210+
let folder = documents.appendingPathComponent("AppFolder", isDirectory: true)
211+
try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
212+
return folder
223213
}
224214
}

0 commit comments

Comments
 (0)