@@ -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