@@ -109,6 +109,53 @@ class CertificateFileManager {
109109 return candidate
110110 }
111111
112+ func updateCertificate( folderName: String , p12Data: Data , provData: Data , password: String , displayName: String ) throws {
113+ let certificateFolder = certificatesDirectory. appendingPathComponent ( folderName)
114+ guard fileManager. fileExists ( atPath: certificateFolder. path) else {
115+ throw NSError ( domain: " CertificateFileManager " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Certificate folder not found " ] )
116+ }
117+
118+ let p12HashNew = CertificatesManager . sha256Hex ( p12Data)
119+ let provHashNew = CertificatesManager . sha256Hex ( provData)
120+ let passwordHashNew = CertificatesManager . sha256Hex ( password. data ( using: . utf8) ?? Data ( ) )
121+
122+ // Check if new version identical to any other existing (exclude self)
123+ let existingFolders = try fileManager. contentsOfDirectory ( at: certificatesDirectory, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles] )
124+ for folder in existingFolders {
125+ if folder == certificateFolder { continue }
126+
127+ let p12URL = folder. appendingPathComponent ( " certificate.p12 " )
128+ let provURL = folder. appendingPathComponent ( " profile.mobileprovision " )
129+ let passwordURL = folder. appendingPathComponent ( " password.txt " )
130+
131+ if fileManager. fileExists ( atPath: p12URL. path) && fileManager. fileExists ( atPath: provURL. path) && fileManager. fileExists ( atPath: passwordURL. path) {
132+ do {
133+ let existingP12Data = try Data ( contentsOf: p12URL)
134+ let existingProvData = try Data ( contentsOf: provURL)
135+ let existingPasswordData = try Data ( contentsOf: passwordURL)
136+ let existingPassword = String ( data: existingPasswordData, encoding: . utf8) ?? " "
137+
138+ let p12HashExisting = CertificatesManager . sha256Hex ( existingP12Data)
139+ let provHashExisting = CertificatesManager . sha256Hex ( existingProvData)
140+ let passwordHashExisting = CertificatesManager . sha256Hex ( existingPassword. data ( using: . utf8) ?? Data ( ) )
141+
142+ if p12HashNew == p12HashExisting && provHashNew == provHashExisting && passwordHashNew == passwordHashExisting {
143+ throw NSError ( domain: " CertificateFileManager " , code: 2 , userInfo: [ NSLocalizedDescriptionKey: " This updated certificate matches another existing one " ] )
144+ }
145+ } catch {
146+ // Skip if can't read existing
147+ continue
148+ }
149+ }
150+ }
151+
152+ // Overwrite files
153+ try p12Data. write ( to: certificateFolder. appendingPathComponent ( " certificate.p12 " ) )
154+ try provData. write ( to: certificateFolder. appendingPathComponent ( " profile.mobileprovision " ) )
155+ try password. data ( using: . utf8) ? . write ( to: certificateFolder. appendingPathComponent ( " password.txt " ) )
156+ try displayName. data ( using: . utf8) ? . write ( to: certificateFolder. appendingPathComponent ( " name.txt " ) )
157+ }
158+
112159 func deleteCertificate( folderName: String ) throws {
113160 let certificateFolder = certificatesDirectory. appendingPathComponent ( folderName)
114161 try fileManager. removeItem ( at: certificateFolder)
@@ -123,35 +170,70 @@ class CertificateFileManager {
123170struct CertificateView : View {
124171 @State private var customCertificates : [ CustomCertificate ] = [ ]
125172 @State private var showAddCertificateSheet = false
173+ @State private var editingFolder : String ? = nil
126174 @State private var selectedCert : String ? = nil
175+ @State private var showingDeleteAlert = false
176+ @State private var certToDelete : CustomCertificate ?
127177
128178 var body : some View {
129179 NavigationStack {
130180 ScrollView {
131- LazyVStack ( spacing: 20 ) {
181+ LazyVGrid ( columns : [ GridItem ( . flexible ( ) ) , GridItem ( . flexible ( ) ) ] , spacing: 20 ) {
132182 ForEach ( customCertificates) { cert in
133- VStack ( alignment: . leading, spacing: 12 ) {
134- Text ( cert. displayName)
135- . font ( . title2)
136- . fontWeight ( . semibold)
137- . foregroundColor ( . primary)
138- }
139- . padding ( 20 )
140- . frame ( maxWidth: . infinity)
141- . background ( Color ( . systemGray6) )
142- . cornerRadius ( 16 )
143- . overlay (
144- RoundedRectangle ( cornerRadius: 16 )
145- . stroke ( selectedCert == cert. folderName ? Color . blue : Color . clear, lineWidth: 3 )
146- )
147- . onTapGesture {
148- if selectedCert == cert. folderName {
149- selectedCert = nil
150- UserDefaults . standard. removeObject ( forKey: " selectedCertificateFolder " )
151- } else {
152- selectedCert = cert. folderName
153- UserDefaults . standard. set ( selectedCert, forKey: " selectedCertificateFolder " )
183+ ZStack ( alignment: . top) {
184+ VStack ( alignment: . leading, spacing: 12 ) {
185+ Text ( cert. displayName)
186+ . font ( . title2)
187+ . fontWeight ( . semibold)
188+ . foregroundColor ( . primary)
189+ }
190+ . padding ( 20 )
191+ . frame ( maxWidth: . infinity)
192+ . background ( Color ( . systemGray6) )
193+ . cornerRadius ( 16 )
194+ . overlay (
195+ RoundedRectangle ( cornerRadius: 16 )
196+ . stroke ( selectedCert == cert. folderName ? Color . blue : Color . clear, lineWidth: 3 )
197+ )
198+ . onTapGesture {
199+ if selectedCert == cert. folderName {
200+ selectedCert = nil
201+ UserDefaults . standard. removeObject ( forKey: " selectedCertificateFolder " )
202+ } else {
203+ selectedCert = cert. folderName
204+ UserDefaults . standard. set ( selectedCert, forKey: " selectedCertificateFolder " )
205+ }
206+ }
207+
208+ HStack {
209+ Button ( action: {
210+ editingFolder = cert. folderName
211+ showAddCertificateSheet = true
212+ } ) {
213+ Image ( systemName: " pencil " )
214+ . foregroundColor ( . blue)
215+ . font ( . caption)
216+ . padding ( 8 )
217+ . background ( Color . white. opacity ( 0.8 ) )
218+ . clipShape ( Circle ( ) )
219+ }
220+
221+ Spacer ( )
222+
223+ Button ( action: {
224+ certToDelete = cert
225+ showingDeleteAlert = true
226+ } ) {
227+ Image ( systemName: " trash " )
228+ . foregroundColor ( . red)
229+ . font ( . caption)
230+ . padding ( 8 )
231+ . background ( Color . white. opacity ( 0.8 ) )
232+ . clipShape ( Circle ( ) )
233+ }
154234 }
235+ . padding ( . top, 12 )
236+ . padding ( . horizontal, 12 )
155237 }
156238 }
157239 }
@@ -161,22 +243,36 @@ struct CertificateView: View {
161243 . navigationBarTitleDisplayMode ( . inline)
162244 . toolbar {
163245 ToolbarItem ( placement: . navigationBarTrailing) {
164- Button ( action: { showAddCertificateSheet = true } ) {
246+ Button ( action: {
247+ editingFolder = nil
248+ showAddCertificateSheet = true
249+ } ) {
165250 Image ( systemName: " plus " )
166251 }
167252 }
168253 }
169254 . sheet ( isPresented: $showAddCertificateSheet, onDismiss: {
170255 customCertificates = CertificateFileManager . shared. loadCertificates ( )
256+ editingFolder = nil
171257 // Re-check selected after reload
172258 if let sel = selectedCert, !customCertificates. contains ( where: { $0. folderName == sel } ) {
173259 selectedCert = nil
174260 UserDefaults . standard. removeObject ( forKey: " selectedCertificateFolder " )
175261 }
176262 } ) {
177- AddCertificateView ( )
263+ AddCertificateView ( editingFolder : editingFolder )
178264 . presentationDetents ( [ . large] )
179265 }
266+ . alert ( " Delete Certificate? " , isPresented: $showingDeleteAlert) {
267+ Button ( " Delete " , role: . destructive) {
268+ if let cert = certToDelete {
269+ deleteCertificate ( cert)
270+ }
271+ }
272+ Button ( " Cancel " , role: . cancel) { }
273+ } message: {
274+ Text ( " Are you sure? This can't be undone. " )
275+ }
180276 . onAppear {
181277 customCertificates = CertificateFileManager . shared. loadCertificates ( )
182278 selectedCert = UserDefaults . standard. string ( forKey: " selectedCertificateFolder " )
@@ -187,10 +283,20 @@ struct CertificateView: View {
187283 }
188284 }
189285 }
286+
287+ private func deleteCertificate( _ cert: CustomCertificate ) {
288+ try ? CertificateFileManager . shared. deleteCertificate ( folderName: cert. folderName)
289+ customCertificates = CertificateFileManager . shared. loadCertificates ( )
290+ if selectedCert == cert. folderName {
291+ selectedCert = nil
292+ UserDefaults . standard. removeObject ( forKey: " selectedCertificateFolder " )
293+ }
294+ }
190295}
191296
192297struct AddCertificateView : View {
193298 @Environment ( \. dismiss) private var dismiss
299+ let editingFolder : String ?
194300
195301 @State private var p12File : CertificateFileItem ?
196302 @State private var provFile : CertificateFileItem ?
@@ -199,6 +305,10 @@ struct AddCertificateView: View {
199305 @State private var isChecking = false
200306 @State private var errorMessage = " "
201307
308+ init ( editingFolder: String ? = nil ) {
309+ self . editingFolder = editingFolder
310+ }
311+
202312 var body : some View {
203313 NavigationView {
204314 Form {
@@ -248,11 +358,11 @@ struct AddCertificateView: View {
248358 . font ( . subheadline)
249359 }
250360 }
251- . navigationTitle ( " New Certificate " )
361+ . navigationTitle ( editingFolder != nil ? " Edit Certificate " : " New Certificate " )
252362 . navigationBarTitleDisplayMode ( . inline)
253363 . toolbar {
254364 ToolbarItem ( placement: . navigationBarLeading) {
255- Button ( " X " ) {
365+ Button ( " × " ) {
256366 dismiss ( )
257367 }
258368 . disabled ( isChecking)
@@ -282,6 +392,25 @@ struct AddCertificateView: View {
282392 . onChange ( of: password) { _ in
283393 errorMessage = " "
284394 }
395+ . onAppear {
396+ if let folder = editingFolder {
397+ loadForEdit ( folder: folder)
398+ }
399+ }
400+ }
401+ }
402+
403+ private func loadForEdit( folder: String ) {
404+ let certFolder = CertificateFileManager . shared. certificatesDirectory. appendingPathComponent ( folder)
405+ let p12URL = certFolder. appendingPathComponent ( " certificate.p12 " )
406+ let provURL = certFolder. appendingPathComponent ( " profile.mobileprovision " )
407+ let passwordURL = certFolder. appendingPathComponent ( " password.txt " )
408+
409+ p12File = CertificateFileItem ( name: " certificate.p12 " , url: p12URL)
410+ provFile = CertificateFileItem ( name: " profile.mobileprovision " , url: provURL)
411+
412+ if let pwData = try ? Data ( contentsOf: passwordURL) , let pw = String ( data: pwData, encoding: . utf8) {
413+ password = pw
285414 }
286415 }
287416
@@ -293,30 +422,40 @@ struct AddCertificateView: View {
293422
294423 DispatchQueue . global ( qos: . userInitiated) . async {
295424 do {
296- // Access security-scoped resources
297- guard p12URL. startAccessingSecurityScopedResource ( ) else {
298- throw NSError ( domain: " AccessError " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Cannot access P12 file " ] )
299- }
300- defer { p12URL. stopAccessingSecurityScopedResource ( ) }
301-
302- guard provURL. startAccessingSecurityScopedResource ( ) else {
303- throw NSError ( domain: " AccessError " , code: 2 , userInfo: [ NSLocalizedDescriptionKey: " Cannot access Provision file " ] )
425+ var p12Data : Data
426+ var provData : Data
427+ if editingFolder != nil {
428+ // For edit, files are in app container, no security scope needed
429+ p12Data = try Data ( contentsOf: p12URL)
430+ provData = try Data ( contentsOf: provURL)
431+ } else {
432+ // For new, access security-scoped
433+ guard p12URL. startAccessingSecurityScopedResource ( ) else {
434+ throw NSError ( domain: " AccessError " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Cannot access P12 file " ] )
435+ }
436+ defer { p12URL. stopAccessingSecurityScopedResource ( ) }
437+
438+ guard provURL. startAccessingSecurityScopedResource ( ) else {
439+ throw NSError ( domain: " AccessError " , code: 2 , userInfo: [ NSLocalizedDescriptionKey: " Cannot access Provision file " ] )
440+ }
441+ defer { provURL. stopAccessingSecurityScopedResource ( ) }
442+
443+ p12Data = try Data ( contentsOf: p12URL)
444+ provData = try Data ( contentsOf: provURL)
304445 }
305- defer { provURL. stopAccessingSecurityScopedResource ( ) }
306-
307- let p12Data = try Data ( contentsOf: p12URL)
308- let provData = try Data ( contentsOf: provURL)
309446
310447 let result = CertificatesManager . check ( p12Data: p12Data, password: password, mobileProvisionData: provData)
311448
312449 var dispatchError : String ?
313450 switch result {
314451 case . success( . success) :
315- // Get dynamic name
316452 let displayName = CertificatesManager . getCertificateName ( p12Data: p12Data, password: password) ?? " Custom Certificate "
317453
318- // Save
319- _ = try CertificateFileManager . shared. saveCertificate ( p12Data: p12Data, provData: provData, password: password, displayName: displayName)
454+ if let folder = editingFolder {
455+ try CertificateFileManager . shared. updateCertificate ( folderName: folder, p12Data: p12Data, provData: provData, password: password, displayName: displayName)
456+ } else {
457+ _ = try CertificateFileManager . shared. saveCertificate ( p12Data: p12Data, provData: provData, password: password, displayName: displayName)
458+ }
320459 case . success( . incorrectPassword) :
321460 dispatchError = " Incorrect Password "
322461 case . success( . noMatch) :
0 commit comments