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

Commit ad3b69b

Browse files
authored
Merge pull request #3 from ProStore-iOS/install-progress-and-user-friendly-errors
Add user friendly errors v1
2 parents 7df88f4 + 5789646 commit ad3b69b

4 files changed

Lines changed: 119 additions & 67 deletions

File tree

Sources/prostore/prostore.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ struct ProStore: App {
1717
}
1818

1919
struct MainSidebarView: View {
20-
@State private var selected: SidebarItem? = .apps
20+
@State private var selected: SidebarItem? = .store
2121

2222
var body: some View {
2323
NavigationSplitView {
2424
List(selection: $selected) {
25-
NavigationLink(value: SidebarItem.apps) {
26-
Label("Apps", systemImage: "square.grid.2x2.fill")
25+
NavigationLink(value: SidebarItem.store) {
26+
Label("Store", systemImage: "cart.fill")
2727
}
2828
NavigationLink(value: SidebarItem.certificates) {
2929
Label("Certificates", systemImage: "key")
@@ -44,7 +44,7 @@ struct MainSidebarView: View {
4444
.navigationTitle("Certificates")
4545
.navigationBarTitleDisplayMode(.large)
4646
}
47-
case .apps:
47+
case .store:
4848
NavigationStack {
4949
AppsView(repoURLs: [
5050
URL(string: "https://repository.apptesters.org/")!,
@@ -54,10 +54,9 @@ struct MainSidebarView: View {
5454
URL(string: "https://ipa.cypwn.xyz/cypwn.json")!,
5555
URL(string: "https://quarksources.github.io/dist/quantumsource.min.json")!,
5656
URL(string: "https://bit.ly/quantumsource-plus-min")!,
57-
URL(string: "https://aio.zxcvbn.fyi/r/repo.esign.json")!,
5857
URL(string: "https://raw.githubusercontent.com/Neoncat-OG/TrollStore-IPAs/main/apps_esign.json")!
5958
])
60-
.navigationTitle("Apps")
59+
.navigationTitle("Store")
6160
.navigationBarTitleDisplayMode(.large)
6261
}
6362
case .about:
@@ -82,8 +81,10 @@ struct MainSidebarView: View {
8281
enum SidebarItem: Hashable {
8382
case updater
8483
case certificates
85-
case apps
84+
case store
8685
case about
8786

8887
}
8988

89+
90+

Sources/prostore/signing/DownloadSignManager.swift

Lines changed: 88 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// DownloadSignManager.swift
1+
// DownloadSignManager.swift - Updated version
22
import Foundation
33
import Combine
44

@@ -7,6 +7,8 @@ class DownloadSignManager: ObservableObject {
77
@Published var status: String = ""
88
@Published var isProcessing: Bool = false
99
@Published var showSuccess: Bool = false
10+
@Published var showError: Bool = false
11+
@Published var errorMessage: String = ""
1012

1113
private var downloadTask: URLSessionDownloadTask?
1214
private var downloadProgressObservation: NSKeyValueObservation?
@@ -20,13 +22,32 @@ class DownloadSignManager: ObservableObject {
2022
private let installPortion: Double = 1.0 - (0.33 + 0.33) // ~0.34
2123

2224
func downloadAndSign(app: AltApp) {
23-
guard let downloadURL = app.downloadURL else {
24-
self.status = "No download URL available"
25+
// Reset error state
26+
self.showError = false
27+
self.errorMessage = ""
28+
29+
// Validate certificate selection
30+
guard let selectedCertFolder = UserDefaults.standard.string(forKey: "selectedCertificateFolder") else {
31+
self.showError(message: "No certificate selected. Please select a certificate first.")
32+
return
33+
}
34+
35+
// Validate certificate files exist
36+
guard let certFiles = getCertificateFiles(for: selectedCertFolder) else {
37+
self.showError(message: "Certificate files not found or incomplete. Please add a certificate.")
38+
return
39+
}
40+
41+
// Check if pairing file exists (for installation)
42+
let fm = FileManager.default
43+
let pairingFile = getAppFolder().appendingPathComponent("pairingFile.plist")
44+
if !fm.fileExists(atPath: pairingFile.path) {
45+
self.showError(message: "Pairing file not found. Please follow setup to place pairing file in the 'ProStore' folder.")
2546
return
2647
}
2748

28-
guard let selectedCertFolder = UserDefaults.standard.string(forKey: "selectedCertificateFolder") else {
29-
self.status = "No certificate selected"
49+
guard let downloadURL = app.downloadURL else {
50+
self.showError(message: "No download URL available for this app")
3051
return
3152
}
3253

@@ -36,11 +57,17 @@ class DownloadSignManager: ObservableObject {
3657
self.showSuccess = false
3758

3859
DispatchQueue.global(qos: .userInitiated).async {
39-
self.performDownloadAndSign(downloadURL: downloadURL, appName: app.name, certFolder: selectedCertFolder)
60+
self.performDownloadAndSign(
61+
downloadURL: downloadURL,
62+
appName: app.name,
63+
p12URL: certFiles.p12URL,
64+
provURL: certFiles.provURL,
65+
password: certFiles.password
66+
)
4067
}
4168
}
4269

43-
private func performDownloadAndSign(downloadURL: URL, appName: String, certFolder: String) {
70+
private func performDownloadAndSign(downloadURL: URL, appName: String, p12URL: URL, provURL: URL, password: String) {
4471
// Step 1: Setup directories
4572
let fm = FileManager.default
4673
let appFolder = self.getAppFolder()
@@ -52,8 +79,7 @@ class DownloadSignManager: ObservableObject {
5279
}
5380
} catch {
5481
DispatchQueue.main.async {
55-
self.status = "Failed to create temp directory: \(error.localizedDescription)"
56-
self.isProcessing = false
82+
self.showError(message: "Failed to create temp directory: \(error.localizedDescription)")
5783
}
5884
return
5985
}
@@ -69,22 +95,12 @@ class DownloadSignManager: ObservableObject {
6995

7096
switch result {
7197
case .success:
72-
// Step 3: Get certificate files
73-
guard let (p12URL, provURL, password) = self.getCertificateFiles(for: certFolder) else {
74-
DispatchQueue.main.async {
75-
self.status = "Failed to get certificate files"
76-
self.isProcessing = false
77-
}
78-
return
79-
}
80-
81-
// Step 4: Sign the IPA
98+
// Step 3: Sign the IPA
8299
self.signIPA(ipaURL: tempIPAURL, p12URL: p12URL, provURL: provURL, password: password, appName: appName)
83100

84101
case .failure(let error):
85102
DispatchQueue.main.async {
86-
self.status = "Download failed: \(error.localizedDescription)"
87-
self.isProcessing = false
103+
self.showError(message: "Download failed: \(error.localizedDescription)")
88104
}
89105

90106
// Clean up temp file if it exists
@@ -108,9 +124,7 @@ class DownloadSignManager: ObservableObject {
108124

109125
if let error = error as NSError?, error.domain == NSURLErrorDomain, error.code == NSURLErrorCancelled {
110126
DispatchQueue.main.async {
111-
self.status = "Cancelled"
112-
self.isProcessing = false
113-
self.progress = 0.0
127+
self.showError(message: "Download cancelled")
114128
}
115129
completion(.failure(error))
116130
return
@@ -166,7 +180,7 @@ class DownloadSignManager: ObservableObject {
166180
task.resume()
167181
}
168182

169-
private func getCertificateFiles(for folderName: String) -> (p12URL: URL, provURL: URL, password: String)? {
183+
private func getCertificateFiles(for folderName: String) -> (p12URL: URL, provURL: URL, password: String)? {
170184
let fm = FileManager.default
171185
let certsDir = CertificateFileManager.shared.certificatesDirectory.appendingPathComponent(folderName)
172186

@@ -194,15 +208,15 @@ class DownloadSignManager: ObservableObject {
194208
p12URL: p12URL,
195209
provURL: provURL,
196210
p12Password: password,
197-
progressUpdate: { [weak self] status, progress in
198-
DispatchQueue.main.async {
199-
guard let self = self else { return }
200-
let overallProgress = self.downloadPortion + (progress * self.signPortion)
201-
self.progress = overallProgress
202-
let percentOfSign = Int(round(progress * 100))
203-
self.status = "\(status)"
204-
}
205-
},
211+
progressUpdate: { [weak self] status, progress in
212+
DispatchQueue.main.async {
213+
guard let self = self else { return }
214+
let overallProgress = self.downloadPortion + (progress * self.signPortion)
215+
self.progress = overallProgress
216+
let percentOfSign = Int(round(progress * 100))
217+
self.status = "\(status)"
218+
}
219+
},
206220
completion: { [weak self] result in
207221
DispatchQueue.main.async {
208222
guard let self = self else { return }
@@ -217,8 +231,7 @@ progressUpdate: { [weak self] status, progress in
217231
try? FileManager.default.removeItem(at: ipaURL)
218232

219233
case .failure(let error):
220-
self.status = "❌ Signing failed: \(error.localizedDescription)"
221-
self.isProcessing = false
234+
self.showError(message: "❌ Signing failed: \(error.localizedDescription)")
222235
try? FileManager.default.removeItem(at: ipaURL)
223236
}
224237
}
@@ -269,39 +282,65 @@ progressUpdate: { [weak self] status, progress in
269282

270283
} catch {
271284
await MainActor.run {
272-
self.status = "❌ Install failed: \(error.localizedDescription)"
273-
self.isProcessing = false
285+
self.showError(message: "❌ Install failed: \(error.localizedDescription)")
274286
self.installationStream = nil
275287
self.installationTask = nil
276288
}
277289
}
278290
}
279291
}
280292

281-
func cancel() {
282-
downloadTask?.cancel()
283-
installationTask?.cancel()
284-
installationStream = nil
285-
installationTask = nil
286-
287-
// Remove observer
288-
downloadProgressObservation = nil
293+
private func showError(message: String) {
294+
DispatchQueue.main.async {
295+
self.progress = 1.0 // Set to 100%
296+
self.status = message
297+
self.errorMessage = message
298+
self.showError = true
299+
self.isProcessing = true // Keep progress bar visible
300+
301+
// Hide progress bar after 5 seconds with red state
302+
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
303+
self.isProcessing = false
304+
self.showError = false
305+
self.progress = 0.0
306+
self.status = ""
307+
self.errorMessage = ""
308+
309+
// Clean up any tasks
310+
self.cancelTasks()
311+
}
312+
}
313+
}
289314

315+
func cancel() {
316+
cancelTasks()
317+
290318
DispatchQueue.main.async {
291319
self.isProcessing = false
320+
self.showSuccess = false
321+
self.showError = false
292322
self.status = "Cancelled"
293323
self.progress = 0.0
324+
self.errorMessage = ""
294325
}
295326
}
327+
328+
private func cancelTasks() {
329+
downloadTask?.cancel()
330+
installationTask?.cancel()
331+
installationStream = nil
332+
installationTask = nil
333+
downloadProgressObservation = nil
334+
}
296335

297336
private func getAppFolder() -> URL {
298337
let fm = FileManager.default
299-
let documents = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
300-
let appFolder = documents.appendingPathComponent("AppFolder")
338+
let appFolder = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
301339
if !fm.fileExists(atPath: appFolder.path) {
302340
try? fm.createDirectory(at: appFolder, withIntermediateDirectories: true)
303341
}
304342
return appFolder
305343
}
306-
307344
}
345+
346+

Sources/prostore/signing/signer.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ fileprivate class SigningManager {
6060
) {
6161
DispatchQueue.global(qos: .userInitiated).async {
6262
do {
63-
progressUpdate("📂 Preparing files", 0.0)
63+
progressUpdate("📂 Preparing files...", 0.0)
6464
let (tmpRoot, inputsDir, workDir) = try prepareTemporaryWorkspace()
6565
defer {
6666
cleanupTemporaryFiles(at: tmpRoot)
@@ -71,16 +71,16 @@ fileprivate class SigningManager {
7171
provURL: provURL,
7272
to: inputsDir
7373
)
74-
progressUpdate("🔓 Unzipping IPA", 0.25)
74+
progressUpdate("🔓 Unzipping IPA...", 0.25)
7575
try extractIPA(ipaURL: localIPA, to: workDir, progressUpdate: { progress in
7676
// Convert 0.0-1.0 progress to 0.25-0.5 range
7777
let overallProgress = 0.25 + (progress * 0.25)
7878
let pct = Int(progress * 100)
79-
progressUpdate("🔓 Unzipping IPA", overallProgress)
79+
progressUpdate("🔓 Unzipping IPA...", overallProgress)
8080
})
8181
let payloadDir = workDir.appendingPathComponent("Payload")
8282
let appDir = try findAppBundle(in: payloadDir)
83-
progressUpdate("✍️ Signing \(appDir.lastPathComponent)", 0.5)
83+
progressUpdate("✍️ Signing \(appDir.lastPathComponent)...", 0.5)
8484
let sema = DispatchSemaphore(value: 0)
8585
var signingError: Error?
8686

@@ -102,7 +102,7 @@ fileprivate class SigningManager {
102102
throw error
103103
}
104104

105-
progressUpdate("📦 Zipping signed IPA", 0.75)
105+
progressUpdate("📦 Zipping signed IPA...", 0.75)
106106
let signedIPAURL = try createSignedIPA(
107107
from: workDir,
108108
originalIPAURL: ipaURL,
@@ -111,7 +111,7 @@ fileprivate class SigningManager {
111111
// Convert 0.0-1.0 progress to 0.75-1.0 range
112112
let overallProgress = 0.75 + (progress * 0.25)
113113
let pct = Int(progress * 100)
114-
progressUpdate("📦 Zipping signed IPA", overallProgress)
114+
progressUpdate("📦 Zipping signed IPA...", overallProgress)
115115
}
116116
)
117117
completion(.success(signedIPAURL))
@@ -227,3 +227,4 @@ fileprivate class SigningManager {
227227
}
228228
}
229229

230+

Sources/prostore/views/AppsDetailView.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,20 @@ public struct AppDetailView: View {
229229
VStack(spacing: 8) {
230230
HStack {
231231
ProgressView(value: downloadManager.progress, total: 1.0)
232-
.progressViewStyle(LinearProgressViewStyle(tint: downloadManager.showSuccess ? .green : .blue))
232+
.progressViewStyle(LinearProgressViewStyle(
233+
tint: downloadManager.showSuccess ? .green :
234+
(downloadManager.showError ? .red : .blue)
235+
))
233236
.scaleEffect(x: 1, y: 1.5, anchor: .center)
234237

235238
if downloadManager.showSuccess {
236239
Image(systemName: "checkmark.circle.fill")
237240
.foregroundColor(.green)
238241
.font(.title2)
242+
} else if downloadManager.showError {
243+
Image(systemName: "xmark.circle.fill")
244+
.foregroundColor(.red)
245+
.font(.title2)
239246
} else {
240247
Text("\(Int(downloadManager.progress * 100))%")
241248
.font(.caption)
@@ -247,13 +254,16 @@ public struct AppDetailView: View {
247254
HStack {
248255
Text(downloadManager.status)
249256
.font(.caption)
250-
.foregroundColor(downloadManager.showSuccess ? .green : .secondary)
251-
.lineLimit(1)
252-
.truncationMode(.middle)
257+
.foregroundColor(
258+
downloadManager.showSuccess ? .green :
259+
(downloadManager.showError ? .red : .secondary)
260+
)
261+
.lineLimit(2) // Allow 2 lines for error messages
262+
.truncationMode(.tail)
253263

254264
Spacer()
255265

256-
if !downloadManager.showSuccess {
266+
if !downloadManager.showSuccess && !downloadManager.showError {
257267
Button("Cancel") {
258268
downloadManager.cancel()
259269
}
@@ -292,5 +302,6 @@ public struct AppDetailView: View {
292302
}
293303
.animation(.easeInOut(duration: 0.3), value: downloadManager.isProcessing)
294304
.animation(.easeInOut(duration: 0.3), value: downloadManager.showSuccess)
305+
.animation(.easeInOut(duration: 0.3), value: downloadManager.showError)
295306
}
296307
}

0 commit comments

Comments
 (0)