@@ -11,6 +11,18 @@ struct CustomCertificate: Identifiable {
1111 let displayName : String
1212 let folderName : String
1313}
14+ // MARK: - Release Models
15+ struct Release : Codable , Identifiable , Equatable {
16+ let id : Int
17+ let name : String
18+ let tagName : String
19+ let publishedAt : Date
20+ let assets : [ Asset ]
21+ }
22+ struct Asset : Codable {
23+ let name : String
24+ let browserDownloadUrl : String
25+ }
1426// MARK: - Date Extension for Formatting
1527extension Date {
1628 func formattedWithOrdinal( ) -> String {
@@ -41,11 +53,207 @@ extension Date {
4153 return " \( number) \( suffix) "
4254 }
4355}
56+ // MARK: - Official Certificates View
57+ struct OfficialCertificatesView : View {
58+ @Environment ( \. dismiss) private var dismiss
59+ @State private var releases : [ Release ] = [ ]
60+ @State private var selectedRelease : Release ? = nil
61+ @State private var statusMessage = " "
62+ @State private var isChecking = false
63+ @State private var p12Data : Data ? = nil
64+ @State private var provData : Data ? = nil
65+ @State private var password : String ? = nil
66+ @State private var displayName = " "
67+ @State private var expiry : Date ? = nil
68+
69+ private var isSuccess : Bool {
70+ statusMessage. contains ( " Success " )
71+ }
72+
73+ private let dateFormatter : DateFormatter = {
74+ let f = DateFormatter ( )
75+ f. dateStyle = . medium
76+ return f
77+ } ( )
78+
79+ var body : some View {
80+ NavigationStack {
81+ Form {
82+ Section ( " Select Official Certificate " ) {
83+ Picker ( " Certificate " , selection: $selectedRelease) {
84+ Text ( " Select a certificate " ) . tag ( Release ? . none)
85+ ForEach ( releases) { release in
86+ Text ( cleanName ( release. name) ) . tag ( Optional ( release) )
87+ }
88+ }
89+ }
90+ Section {
91+ Text ( " Provided by loyahdev " )
92+ . font ( . caption)
93+ . foregroundColor ( . secondary)
94+ }
95+ if let release = selectedRelease {
96+ Section ( " Details " ) {
97+ Text ( " Tag: \( release. tagName) " )
98+ Text ( " Published: \( release. publishedAt, formatter: dateFormatter) " )
99+ }
100+ }
101+ Section {
102+ Button ( " Check Certificate " ) {
103+ checkCertificate ( )
104+ }
105+ . disabled ( selectedRelease == nil || isChecking)
106+ if !statusMessage. isEmpty {
107+ Text ( statusMessage)
108+ . foregroundColor ( isSuccess ? . green : . red)
109+ }
110+ }
111+ Section {
112+ Button ( " Add Certificate " ) {
113+ addCertificate ( )
114+ }
115+ . disabled ( p12Data == nil || isChecking)
116+ }
117+ }
118+ . navigationTitle ( " Official Certificates " )
119+ . navigationBarTitleDisplayMode ( . inline)
120+ . toolbar {
121+ ToolbarItem ( placement: . navigationBarLeading) {
122+ Button ( " × " ) {
123+ dismiss ( )
124+ }
125+ }
126+ }
127+ . onAppear {
128+ fetchReleases ( )
129+ }
130+ }
131+ }
132+
133+ private func cleanName( _ name: String ) -> String {
134+ name. replacingOccurrences ( of: " \\ \\ " , with: " " ) . replacingOccurrences ( of: " \\ " , with: " " )
135+ }
136+
137+ private func fetchReleases( ) {
138+ guard let url = URL ( string: " https://api.github.com/repos/loyahdev/certificates/releases " ) else { return }
139+ Task {
140+ do {
141+ let ( data, _) = try await URLSession . shared. data ( from: url)
142+ let decoder = JSONDecoder ( )
143+ decoder. dateDecodingStrategy = . iso8601
144+ let decoded = try decoder. decode ( [ Release ] . self, from: data)
145+ await MainActor . run {
146+ self . releases = decoded. sorted { $0. publishedAt > $1. publishedAt }
147+ }
148+ } catch {
149+ await MainActor . run {
150+ self . statusMessage = " Failed to fetch releases: \( error. localizedDescription) "
151+ }
152+ }
153+ }
154+ }
155+
156+ private func checkCertificate( ) {
157+ guard let release = selectedRelease,
158+ let asset = release. assets. first ( where: { $0. name. hasSuffix ( " .zip " ) } ) ,
159+ let downloadUrl = URL ( string: asset. browserDownloadUrl) else {
160+ statusMessage = " Invalid release "
161+ return
162+ }
163+ isChecking = true
164+ statusMessage = " Downloading... "
165+ Task {
166+ do {
167+ let ( tempData, _) = try await URLSession . shared. data ( from: downloadUrl)
168+ let tempDir = FileManager . default. temporaryDirectory. appendingPathComponent ( UUID ( ) . uuidString)
169+ try FileManager . default. createDirectory ( at: tempDir, withIntermediateDirectories: true , attributes: nil )
170+ let zipPath = tempDir. appendingPathComponent ( " temp.zip " )
171+ try tempData. write ( to: zipPath)
172+ let extractDir = tempDir. appendingPathComponent ( " extracted " )
173+ try FileManager . default. unzipItem ( at: zipPath, to: extractDir, progress: nil )
174+ // Find files
175+ var p12Urls : [ URL ] = [ ]
176+ var provUrls : [ URL ] = [ ]
177+ if let enumerator = FileManager . default. enumerator ( at: extractDir, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] ) {
178+ for case let fileURL as URL in enumerator {
179+ let path = fileURL. path
180+ if path. hasSuffix ( " .p12 " ) {
181+ p12Urls. append ( fileURL)
182+ } else if path. hasSuffix ( " .mobileprovision " ) {
183+ provUrls. append ( fileURL)
184+ }
185+ }
186+ }
187+ try FileManager . default. removeItem ( at: tempDir)
188+ guard p12Urls. count == 1 , provUrls. count == 1 else {
189+ throw NSError ( domain: " Extraction " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Unable to extract certificate " ] )
190+ }
191+ let p12Url = p12Urls [ 0 ]
192+ let provUrl = provUrls [ 0 ]
193+ let p12Data = try Data ( contentsOf: p12Url)
194+ let provData = try Data ( contentsOf: provUrl)
195+ var successPw : String ?
196+ for pw in [ " Hydrogen " , " Sideloadingdotorg " ] {
197+ switch CertificatesManager . check ( p12Data: p12Data, password: pw, mobileProvisionData: provData) {
198+ case . success( . success) :
199+ successPw = pw
200+ break
201+ default :
202+ break
203+ }
204+ }
205+ guard let pw = successPw else {
206+ throw NSError ( domain: " Password " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Password check failed " ] )
207+ }
208+ let exp = ProStoreTools . getExpirationDate ( provData: provData)
209+ let dispName = CertificatesManager . getCertificateName ( mobileProvisionData: provData) ?? cleanName ( release. name)
210+ await MainActor . run {
211+ self . p12Data = p12Data
212+ self . provData = provData
213+ self . password = pw
214+ self . displayName = dispName
215+ self . expiry = exp
216+ self . statusMessage = " Success: Ready to add \( dispName) , expires \( exp? . formattedWithOrdinal ( ) ?? " Unknown " ) "
217+ self . isChecking = false
218+ }
219+ } catch {
220+ await MainActor . run {
221+ self . statusMessage = " Error: \( error. localizedDescription) "
222+ self . isChecking = false
223+ }
224+ }
225+ }
226+ }
227+
228+ private func addCertificate( ) {
229+ guard let p12Data = p12Data,
230+ let provData = provData,
231+ let pw = password else { return }
232+ isChecking = true
233+ statusMessage = " Adding... "
234+ Task {
235+ do {
236+ _ = try CertificateFileManager . shared. saveCertificate ( p12Data: p12Data, provData: provData, password: pw, displayName: displayName)
237+ await MainActor . run {
238+ self . statusMessage = " Added successfully "
239+ self . isChecking = false
240+ self . dismiss ( )
241+ }
242+ } catch {
243+ await MainActor . run {
244+ self . statusMessage = " Error adding: \( error. localizedDescription) "
245+ self . isChecking = false
246+ }
247+ }
248+ }
249+ }
250+ }
44251// MARK: - CertificateView (List + Add/Edit launchers)
45252struct CertificateView : View {
46253 @State private var customCertificates : [ CustomCertificate ] = [ ]
47254 @State private var certExpiries : [ String : Date ? ] = [ : ]
48255 @State private var showAddCertificateSheet = false
256+ @State private var showOfficialSheet = false
49257 @State private var editingCertificate : CustomCertificate ? = nil // Used only for edit sheet (.sheet(item:))
50258 @State private var selectedCert : String ? = nil
51259 @State private var showingDeleteAlert = false
@@ -65,9 +273,18 @@ struct CertificateView: View {
65273 . background ( Color ( . systemGray6) )
66274 . toolbar {
67275 ToolbarItem ( placement: . navigationBarTrailing) {
68- Button ( action: {
69- showAddCertificateSheet = true
70- } ) {
276+ Menu {
277+ Button {
278+ showAddCertificateSheet = true
279+ } label: {
280+ Label ( " Add from Files " , systemImage: " folder.badge.plus " )
281+ }
282+ Button {
283+ showOfficialSheet = true
284+ } label: {
285+ Label ( " Add from Official " , systemImage: " globe " )
286+ }
287+ } label: {
71288 Image ( systemName: " plus " )
72289 }
73290 }
@@ -84,6 +301,13 @@ struct CertificateView: View {
84301 AddCertificateView ( onSave: { newlyAddedFolder = $0 } )
85302 . presentationDetents ( [ . large] )
86303 }
304+ // Official sheet
305+ . sheet ( isPresented: $showOfficialSheet, onDismiss: {
306+ reloadCertificatesAndEnsureSelection ( )
307+ } ) {
308+ OfficialCertificatesView ( )
309+ . presentationDetents ( [ . large] )
310+ }
87311 // EDIT sheet (identifiable)
88312 . sheet ( item: $editingCertificate, onDismiss: {
89313 reloadCertificatesAndEnsureSelection ( )
0 commit comments