11import SwiftUI
22import UniformTypeIdentifiers
33import ProStoreTools
4+ import ZIPFoundation
45// Centralized types to avoid conflicts
56struct CertificateFileItem {
67 var name : String = " "
@@ -12,16 +13,29 @@ struct CustomCertificate: Identifiable {
1213 let folderName : String
1314}
1415// MARK: - Release Models
15- struct Release : Codable , Identifiable , Equatable {
16+ struct Release : Codable , Identifiable , Equatable , Hashable {
1617 let id : Int
1718 let name : String
1819 let tagName : String
19- let publishedAt : Date
20+ let publishedAt : String
2021 let assets : [ Asset ]
22+
23+ enum CodingKeys : String , CodingKey {
24+ case id, name, tagName = " tag_name " , publishedAt = " published_at " , assets
25+ }
26+
27+ var publishedDate : Date {
28+ // Will be handled by decoder
29+ Date ( )
30+ }
2131}
2232struct Asset : Codable {
2333 let name : String
2434 let browserDownloadUrl : String
35+
36+ enum CodingKeys : String , CodingKey {
37+ case name, browserDownloadUrl = " browser_download_url "
38+ }
2539}
2640// MARK: - Date Extension for Formatting
2741extension Date {
@@ -81,9 +95,9 @@ struct OfficialCertificatesView: View {
8195 Form {
8296 Section ( " Select Official Certificate " ) {
8397 Picker ( " Certificate " , selection: $selectedRelease) {
84- Text ( " Select a certificate " ) . tag ( Release ? . none )
98+ Text ( " Select a certificate " ) . tag ( nil as Release ? )
8599 ForEach ( releases) { release in
86- Text ( cleanName ( release. name) ) . tag ( Optional ( release) )
100+ Text ( cleanName ( release. name) ) . tag ( release as Release ? )
87101 }
88102 }
89103 }
@@ -95,7 +109,7 @@ struct OfficialCertificatesView: View {
95109 if let release = selectedRelease {
96110 Section ( " Details " ) {
97111 Text ( " Tag: \( release. tagName) " )
98- Text ( " Published: \( release . publishedAt , formatter : dateFormatter ) " )
112+ Text ( " Published: \( dateFormatter . string ( from : isoDate ( string : release . publishedAt ) ) ) " )
99113 }
100114 }
101115 Section {
@@ -112,7 +126,7 @@ struct OfficialCertificatesView: View {
112126 Button ( " Add Certificate " ) {
113127 addCertificate ( )
114128 }
115- . disabled ( p12Data == nil || isChecking)
129+ . disabled ( p12Data == nil || provData == nil || password == nil || isChecking)
116130 }
117131 }
118132 . navigationTitle ( " Official Certificates " )
@@ -130,20 +144,47 @@ struct OfficialCertificatesView: View {
130144 }
131145 }
132146
147+ private func isoDate( string: String ) -> Date {
148+ let formatter = ISO8601DateFormatter ( )
149+ return formatter. date ( from: string) ?? Date ( )
150+ }
151+
133152 private func cleanName( _ name: String ) -> String {
134153 name. replacingOccurrences ( of: " \\ \\ " , with: " " ) . replacingOccurrences ( of: " \\ " , with: " " )
135154 }
136155
156+ private func getPAT( ) async -> String ? {
157+ guard let url = URL ( string: " https://certapi.loyah.dev/pac " ) else { return nil }
158+ do {
159+ let ( data, _) = try await URLSession . shared. data ( from: url)
160+ return String ( data: data, encoding: . utf8) ? . trimmingCharacters ( in: . whitespacesAndNewlines)
161+ } catch {
162+ return nil
163+ }
164+ }
165+
137166 private func fetchReleases( ) {
138- guard let url = URL ( string: " https://api.github.com/repos/loyahdev/certificates/releases " ) else { return }
139167 Task {
168+ let pat = await getPAT ( )
169+ let url = URL ( string: " https://api.github.com/repos/loyahdev/certificates/releases " ) !
170+ var request = URLRequest ( url: url)
171+ if let pat = pat {
172+ request. setValue ( " token \( pat) " , forHTTPHeaderField: " Authorization " )
173+ }
140174 do {
141- let ( data, _) = try await URLSession . shared. data ( from: url)
175+ let ( data, response) = try await URLSession . shared. data ( for: request)
176+ var decodeData = data
177+ if let httpResponse = response as? HTTPURLResponse , httpResponse. statusCode != 200 , pat != nil {
178+ // Fallback to unauthenticated
179+ var fallbackRequest = URLRequest ( url: url)
180+ let ( fallbackData, _) = try await URLSession . shared. data ( for: fallbackRequest)
181+ decodeData = fallbackData
182+ }
142183 let decoder = JSONDecoder ( )
143- decoder. dateDecodingStrategy = . iso8601
144- let decoded = try decoder. decode ( [ Release ] . self, from: data )
184+ decoder. dateDecodingStrategy = . deferredToDate
185+ let decoded = try decoder. decode ( [ Release ] . self, from: decodeData )
145186 await MainActor . run {
146- self . releases = decoded. sorted { $0. publishedAt > $1. publishedAt }
187+ self . releases = decoded. sorted { isoDate ( string : $0. publishedAt) > isoDate ( string : $1. publishedAt) }
147188 }
148189 } catch {
149190 await MainActor . run {
@@ -156,47 +197,64 @@ struct OfficialCertificatesView: View {
156197 private func checkCertificate( ) {
157198 guard let release = selectedRelease,
158199 let asset = release. assets. first ( where: { $0. name. hasSuffix ( " .zip " ) } ) ,
159- let downloadUrl = URL ( string: asset. browserDownloadUrl) else {
200+ let downloadStr = asset. browserDownloadUrl,
201+ let downloadUrl = URL ( string: downloadStr) else {
160202 statusMessage = " Invalid release "
161203 return
162204 }
163205 isChecking = true
164206 statusMessage = " Downloading... "
165207 Task {
208+ let pat = await getPAT ( )
209+ var downloadRequest = URLRequest ( url: downloadUrl)
210+ if let pat = pat {
211+ downloadRequest. setValue ( " token \( pat) " , forHTTPHeaderField: " Authorization " )
212+ }
166213 do {
167- let ( tempData, _) = try await URLSession . shared. data ( from: downloadUrl)
214+ var tempData = Data ( )
215+ var response = URLResponse ( )
216+ ( tempData, response) = try await URLSession . shared. data ( for: downloadRequest)
217+ if let httpResponse = response as? HTTPURLResponse , httpResponse. statusCode != 200 , pat != nil {
218+ // Fallback
219+ var fallbackRequest = URLRequest ( url: downloadUrl)
220+ ( tempData, _) = try await URLSession . shared. data ( for: fallbackRequest)
221+ }
168222 let tempDir = FileManager . default. temporaryDirectory. appendingPathComponent ( UUID ( ) . uuidString)
169223 try FileManager . default. createDirectory ( at: tempDir, withIntermediateDirectories: true , attributes: nil )
224+ defer {
225+ try ? FileManager . default. removeItem ( at: tempDir)
226+ }
170227 let zipPath = tempDir. appendingPathComponent ( " temp.zip " )
171228 try tempData. write ( to: zipPath)
172229 let extractDir = tempDir. appendingPathComponent ( " extracted " )
173230 try FileManager . default. unzipItem ( at: zipPath, to: extractDir, progress: nil )
174231 // Find files
175232 var p12Urls : [ URL ] = [ ]
176233 var provUrls : [ URL ] = [ ]
177- if let enumerator = FileManager . default. enumerator ( at: extractDir, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] ) {
234+ if let enumerator = FileManager . default. enumerator ( at: extractDir, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles, . skipsSubdirectoryDescendants ] ) {
178235 for case let fileURL as URL in enumerator {
179236 let path = fileURL. path
180- if path. hasSuffix ( " .p12 " ) {
181- p12Urls. append ( fileURL)
182- } else if path. hasSuffix ( " .mobileprovision " ) {
183- provUrls. append ( fileURL)
237+ if !path. contains ( " __MACOSX " ) {
238+ if path. hasSuffix ( " .p12 " ) {
239+ p12Urls. append ( fileURL)
240+ } else if path. hasSuffix ( " .mobileprovision " ) {
241+ provUrls. append ( fileURL)
242+ }
184243 }
185244 }
186245 }
187- try FileManager . default. removeItem ( at: tempDir)
188246 guard p12Urls. count == 1 , provUrls. count == 1 else {
189247 throw NSError ( domain: " Extraction " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Unable to extract certificate " ] )
190248 }
191249 let p12Url = p12Urls [ 0 ]
192250 let provUrl = provUrls [ 0 ]
193- let p12Data = try Data ( contentsOf: p12Url)
194- let provData = try Data ( contentsOf: provUrl)
251+ let p12DataLocal = try Data ( contentsOf: p12Url)
252+ let provDataLocal = try Data ( contentsOf: provUrl)
195253 var successPw : String ?
196- for pw in [ " Hydrogen " , " Sideloadingdotorg " ] {
197- switch CertificatesManager . check ( p12Data: p12Data , password: pw , mobileProvisionData: provData ) {
254+ for pwCandidate in [ " Hydrogen " , " Sideloadingdotorg " ] {
255+ switch CertificatesManager . check ( p12Data: p12DataLocal , password: pwCandidate , mobileProvisionData: provDataLocal ) {
198256 case . success( . success) :
199- successPw = pw
257+ successPw = pwCandidate
200258 break
201259 default :
202260 break
@@ -205,11 +263,11 @@ struct OfficialCertificatesView: View {
205263 guard let pw = successPw else {
206264 throw NSError ( domain: " Password " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Password check failed " ] )
207265 }
208- let exp = ProStoreTools . getExpirationDate ( provData: provData )
209- let dispName = CertificatesManager . getCertificateName ( mobileProvisionData: provData ) ?? cleanName ( release. name)
266+ let exp = ProStoreTools . getExpirationDate ( provData: provDataLocal )
267+ let dispName = CertificatesManager . getCertificateName ( mobileProvisionData: provDataLocal ) ?? cleanName ( release. name)
210268 await MainActor . run {
211- self . p12Data = p12Data
212- self . provData = provData
269+ self . p12Data = p12DataLocal
270+ self . provData = provDataLocal
213271 self . password = pw
214272 self . displayName = dispName
215273 self . expiry = exp
@@ -226,14 +284,15 @@ struct OfficialCertificatesView: View {
226284 }
227285
228286 private func addCertificate( ) {
229- guard let p12Data = p12Data,
230- let provData = provData,
231- let pw = password else { return }
287+ guard let p12DataLocal = p12Data,
288+ let provDataLocal = provData,
289+ let pw = password,
290+ !displayName. isEmpty else { return }
232291 isChecking = true
233292 statusMessage = " Adding... "
234293 Task {
235294 do {
236- _ = try CertificateFileManager . shared. saveCertificate ( p12Data: p12Data , provData: provData , password: pw, displayName: displayName)
295+ _ = try CertificateFileManager . shared. saveCertificate ( p12Data: p12DataLocal , provData: provDataLocal , password: pw, displayName: displayName)
237296 await MainActor . run {
238297 self . statusMessage = " Added successfully "
239298 self . isChecking = false
0 commit comments