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

Commit 8f6c617

Browse files
authored
Add Official Certificates View
1 parent 7a05782 commit 8f6c617

1 file changed

Lines changed: 227 additions & 3 deletions

File tree

Sources/prosign/views/CertificateView.swift

Lines changed: 227 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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
1527
extension 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)
45252
struct 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

Comments
 (0)