diff --git a/Feather/Backend/Observable/OptionsManager.swift b/Feather/Backend/Observable/OptionsManager.swift
index 8a3f96c4..4b47a623 100644
--- a/Feather/Backend/Observable/OptionsManager.swift
+++ b/Feather/Backend/Observable/OptionsManager.swift
@@ -74,6 +74,8 @@ struct Options: Codable, Equatable {
var ppqProtection: Bool
/// (Better) protection against PPQ
var dynamicProtection: Bool
+ /// Automatically select certificate that matches target bundle identifier
+ var autoSelectMatchingCertificate: Bool = false
/// App identifiers list which matches and replaces
var identifiers: [String: String]
/// App name list which matches and replaces
@@ -130,6 +132,7 @@ struct Options: Codable, Equatable {
ppqString: randomString(),
ppqProtection: false,
dynamicProtection: false,
+ autoSelectMatchingCertificate: false,
identifiers: [:],
displayNames: [:],
injectionFiles: [],
@@ -155,8 +158,6 @@ struct Options: Codable, Equatable {
post_deleteAppAfterSigned: false
)
- // MARK: duplicate values are not recommended!
-
enum AppAppearance: String, Codable, CaseIterable, LocalizedDescribable {
case `default`
case light = "Light"
@@ -233,3 +234,4 @@ extension LocalizedDescribable where Self: RawRepresentable, RawValue == String
return localized == self.rawValue ? self.rawValue : localized
}
}
+
diff --git a/Feather/Resources/Assets.xcassets/discord.symbolset/Contents.json b/Feather/Resources/Assets.xcassets/discord.symbolset/Contents.json
new file mode 100644
index 00000000..24f139da
--- /dev/null
+++ b/Feather/Resources/Assets.xcassets/discord.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "discord.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Feather/Resources/Assets.xcassets/discord.symbolset/discord.svg b/Feather/Resources/Assets.xcassets/discord.symbolset/discord.svg
new file mode 100644
index 00000000..baa9398a
--- /dev/null
+++ b/Feather/Resources/Assets.xcassets/discord.symbolset/discord.svg
@@ -0,0 +1,199 @@
+
+
+
+
diff --git a/Feather/Resources/Assets.xcassets/github.symbolset/Contents.json b/Feather/Resources/Assets.xcassets/github.symbolset/Contents.json
new file mode 100644
index 00000000..c1e81e10
--- /dev/null
+++ b/Feather/Resources/Assets.xcassets/github.symbolset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "symbols" : [
+ {
+ "filename" : "github.svg",
+ "idiom" : "universal"
+ }
+ ]
+}
diff --git a/Feather/Resources/Assets.xcassets/github.symbolset/github.svg b/Feather/Resources/Assets.xcassets/github.symbolset/github.svg
new file mode 100644
index 00000000..9964bc12
--- /dev/null
+++ b/Feather/Resources/Assets.xcassets/github.symbolset/github.svg
@@ -0,0 +1,167 @@
+
+
+
+
diff --git a/Feather/Resources/Localizable.xcstrings b/Feather/Resources/Localizable.xcstrings
index fa6c6e15..2945bf7c 100644
--- a/Feather/Resources/Localizable.xcstrings
+++ b/Feather/Resources/Localizable.xcstrings
@@ -1504,6 +1504,23 @@
}
}
},
+ "Auto-select Matching Certificate" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Auto-select Matching Certificate"
+ }
+ },
+ "pl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Automatyczny wybór pasującego certyfikatu"
+ }
+ }
+ }
+ },
"Bad Password" : {
"comment" : "Bad certificate password",
"extractionState" : "manual",
@@ -2541,6 +2558,23 @@
}
}
},
+ "Connect to LocalDevVPN" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Connect to LocalDevVPN"
+ }
+ },
+ "pl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Połącz do LocalDevVPN"
+ }
+ }
+ }
+ },
"Copy" : {
"extractionState" : "manual",
"localizations" : {
@@ -3622,6 +3656,23 @@
}
}
},
+ "Download LocalDevVPN" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Download LocalDevVPN"
+ }
+ },
+ "pl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Pobierz LocalDevVPN"
+ }
+ }
+ }
+ },
"Download StosVPN" : {
"comment" : "Don't translate StosVPN, this is for linking the download to the required VPN for idevice",
"extractionState" : "manual",
@@ -6943,6 +6994,12 @@
"value" : "Install After Signing"
}
},
+ "pl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Zainstaluj po podpisaniu"
+ }
+ },
"vi" : {
"stringUnit" : {
"state" : "translated",
@@ -7244,10 +7301,16 @@
"value" : "Join Us on Discord"
}
},
- "vi": {
- "stringUnit": {
- "state": "translated",
- "value": "Hãy tham gia cùng chúng tôi trên Discord!"
+ "pl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Dołącz do nas na Discordzie"
+ }
+ },
+ "vi" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Hãy tham gia cùng chúng tôi trên Discord!"
}
}
}
@@ -8430,10 +8493,16 @@
"value" : "No sources to copy"
}
},
- "vi": {
- "stringUnit": {
- "state": "translated",
- "value": "Không có nguồn để sao chép"
+ "pl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Brak źródeł do skopiowania"
+ }
+ },
+ "vi" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Không có nguồn để sao chép"
}
}
}
@@ -12800,10 +12869,16 @@
"value" : "Sources copied to clipboard"
}
},
- "vi": {
- "stringUnit": {
- "state": "translated",
- "value": "Nguồn đã được sao chép vào bộ nhớ tạm"
+ "pl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Źródła skopiowane do schowka"
+ }
+ },
+ "vi" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Nguồn đã được sao chép vào bộ nhớ tạm"
}
}
}
@@ -14687,6 +14762,23 @@
}
}
},
+ "When enabled, Feather will automatically select the certificate whose application-identifier exactly matches the target bundle identifier." : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "When enabled, Feather will automatically select the certificate whose application-identifier exactly matches the target bundle identifier."
+ }
+ },
+ "pl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Gdy włączone, Feather automatycznie wybierze certyfikat, którego application-identifier dokładnie odpowiada docelowemu identyfikatorowi pakietu (bundle identifier)."
+ }
+ }
+ }
+ },
"You cannot update ‘%@‘ with itself, please use an alternative tool to update it." : {
"extractionState" : "manual",
"localizations" : {
@@ -14789,4 +14881,4 @@
}
},
"version" : "1.0"
-}
+}
\ No newline at end of file
diff --git a/Feather/Views/Settings/Archive & Compression/ArchiveView.swift b/Feather/Views/Settings/Archive & Compression/ArchiveView.swift
index 35adcecf..9e0ddbdf 100644
--- a/Feather/Views/Settings/Archive & Compression/ArchiveView.swift
+++ b/Feather/Views/Settings/Archive & Compression/ArchiveView.swift
@@ -18,7 +18,7 @@ struct ArchiveView: View {
var body: some View {
NBList(.localized("Archive & Compression")) {
Section {
- Picker(.localized("Compression Level"), systemImage: "archivebox", selection: $_compressionLevel) {
+ Picker(.localized("Compression Level"), systemImage: "doc.zipper", selection: $_compressionLevel) {
ForEach(ZipCompression.allCases, id: \.rawValue) { level in
Text(level.label).tag(level)
}
diff --git a/Feather/Views/Settings/Certificates/CertificatesView.swift b/Feather/Views/Settings/Certificates/CertificatesView.swift
index f2c99841..1f61c2c8 100644
--- a/Feather/Views/Settings/Certificates/CertificatesView.swift
+++ b/Feather/Views/Settings/Certificates/CertificatesView.swift
@@ -14,6 +14,7 @@ struct CertificatesView: View {
@State private var _isAddingPresenting = false
@State private var _isSelectedInfoPresenting: CertificatePair?
+ @State private var _searchText: String = ""
// MARK: Fetch
@FetchRequest(
@@ -27,6 +28,22 @@ struct CertificatesView: View {
private var _selectedCertBinding: Binding {
_bindingSelectedCert ?? $_storedSelectedCert
}
+
+ // Filtered certificates based on search
+ private var _filteredCertificates: [CertificatePair] {
+ let items = Array(_certificates)
+ let query = _searchText.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !query.isEmpty else { return items }
+ let q = query.lowercased()
+ return items.filter { cert in
+ let nickname = (cert.nickname ?? "").lowercased()
+ let decoded = Storage.shared.getProvisionFileDecoded(for: cert)
+ let name = (decoded?.Name ?? "").lowercased()
+ let appIDName = (decoded?.AppIDName ?? "").lowercased()
+ let haystack = "\(nickname) \(name) \(appIDName)"
+ return haystack.contains(q)
+ }
+ }
init(selectedCert: Binding? = nil) {
self._bindingSelectedCert = selectedCert
@@ -35,13 +52,17 @@ struct CertificatesView: View {
// MARK: Body
var body: some View {
NBGrid {
- ForEach(Array(_certificates.enumerated()), id: \.element.uuid) { index, cert in
- _cellButton(for: cert, at: index)
- }
+ ForEach(_filteredCertificates, id: \.uuid) { cert in
+ if let originalIndex = _originalIndex(for: cert) {
+ _cellButton(for: cert, originalIndex: originalIndex)
+ }
+ }
}
.navigationTitle(.localized("Certificates"))
+ .searchable(text: $_searchText, placement: .platform())
+ .scrollDismissesKeyboard(.interactively)
.overlay {
- if _certificates.isEmpty {
+ if _filteredCertificates.isEmpty {
if #available(iOS 17, *) {
ContentUnavailableView {
Label(.localized("No Certificates"), systemImage: "questionmark.folder.fill")
@@ -81,7 +102,7 @@ struct CertificatesView: View {
// MARK: - View extension
extension CertificatesView {
@ViewBuilder
- private func _cellButton(for cert: CertificatePair, at index: Int) -> some View {
+ private func _cellButton(for cert: CertificatePair, originalIndex index: Int) -> some View {
Button {
_selectedCertBinding.wrappedValue = index
} label: {
@@ -114,6 +135,10 @@ extension CertificatesView {
.buttonStyle(.plain)
}
+ private func _originalIndex(for cert: CertificatePair) -> Int? {
+ return _certificates.firstIndex(where: { $0.objectID == cert.objectID })
+ }
+
@ViewBuilder
private func _actions(for cert: CertificatePair) -> some View {
Button(.localized("Delete"), systemImage: "trash", role: .destructive) {
diff --git a/Feather/Views/Settings/Installation/Tunnel & Pairing/TunnelView.swift b/Feather/Views/Settings/Installation/Tunnel & Pairing/TunnelView.swift
index f7c70fa1..1aad1dda 100644
--- a/Feather/Views/Settings/Installation/Tunnel & Pairing/TunnelView.swift
+++ b/Feather/Views/Settings/Installation/Tunnel & Pairing/TunnelView.swift
@@ -60,7 +60,7 @@ struct TunnelView: View {
UIApplication.open("localdevvpn://enable?scheme=feather")
}
} else {
- Button(.localized("Download LocalDevVPN"), systemImage: "arrow.down.app") {
+ Button(.localized("Download LocalDevVPN"), systemImage: "link.badge.plus") {
UIApplication.open("https://apps.apple.com/us/app/localdevvpn/id6755608044")
}
}
@@ -103,4 +103,4 @@ struct TunnelView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
}
-}
\ No newline at end of file
+}
diff --git a/Feather/Views/Settings/SettingsView.swift b/Feather/Views/Settings/SettingsView.swift
index 618e5287..87ace0df 100644
--- a/Feather/Views/Settings/SettingsView.swift
+++ b/Feather/Views/Settings/SettingsView.swift
@@ -46,10 +46,10 @@ struct SettingsView: View {
Label(.localized("Signing Options"), systemImage: "signature")
}
NavigationLink(destination: ArchiveView()) {
- Label(.localized("Archive & Compression"), systemImage: "archivebox")
+ Label(.localized("Archive & Compression"), systemImage: "doc.zipper")
}
NavigationLink(destination: InstallationView()) {
- Label(.localized("Installation"), systemImage: "arrow.down.circle")
+ Label(.localized("Installation"), systemImage: "plus.app")
}
} footer: {
Text(.localized("Configure the apps way of installing, its zip compression levels, and custom modifications to apps."))
@@ -83,7 +83,7 @@ extension SettingsView {
}
}
- Button(.localized("Submit Feedback"), systemImage: "safari") {
+ Button(.localized("Submit Feedback"), systemImage: "square.text.square") {
let bugAction: UIAlertAction = .init(title: .localized("Bug Report"), style: .default) { _ in
UIApplication.open(_makeGitHubIssueURL(url: _githubUrl))
}
@@ -98,11 +98,23 @@ extension SettingsView {
actions: [bugAction, chooseAction]
)
}
- Button(.localized("GitHub Repository"), systemImage: "safari") {
+ Button {
UIApplication.open(_githubUrl)
+ } label: {
+ Label {
+ Text(.localized("GitHub Repository"))
+ } icon: {
+ Image("github")
+ }
}
- Button(.localized("Join Us on Discord"), systemImage: "safari") {
+ Button {
UIApplication.open(_discordServer)
+ } label: {
+ Label {
+ Text(.localized("Join Us on Discord"))
+ } icon: {
+ Image("discord")
+ }
}
} footer: {
Text(.localized("If any issues occur within the app please report it via the GitHub repository. When submitting an issue, make sure to submit detailed information."))
diff --git a/Feather/Views/Signing/Shared/SigningOptionsView.swift b/Feather/Views/Signing/Shared/SigningOptionsView.swift
index 90943259..17833a76 100644
--- a/Feather/Views/Signing/Shared/SigningOptionsView.swift
+++ b/Feather/Views/Signing/Shared/SigningOptionsView.swift
@@ -26,6 +26,17 @@ struct SigningOptionsView: View {
} footer: {
Text(.localized("Enabling any protection will append a random string to the bundleidentifiers of the apps you sign, this is to ensure your Apple ID does not get flagged by Apple. However, when using a signing service you can ignore this."))
}
+
+ NBSection(.localized("Certificate")) {
+ _toggle(
+ .localized("Auto-select Matching Certificate"),
+ systemImage: "checkmark.shield",
+ isOn: $options.autoSelectMatchingCertificate,
+ temporaryValue: temporaryOptions?.autoSelectMatchingCertificate
+ )
+ } footer: {
+ Text(.localized("When enabled, Feather will automatically select the certificate whose application-identifier exactly matches the target bundle identifier."))
+ }
}
NBSection(.localized("General")) {
@@ -122,7 +133,7 @@ struct SigningOptionsView: View {
NBSection(.localized("Post Signing")) {
_toggle(
.localized("Install After Signing"),
- systemImage: "arrow.down.circle",
+ systemImage: "plus.app",
isOn: $options.post_installAppAfterSigned,
temporaryValue: temporaryOptions?.post_installAppAfterSigned
)
diff --git a/Feather/Views/Signing/SigningView.swift b/Feather/Views/Signing/SigningView.swift
index 01a123f1..16e701d9 100644
--- a/Feather/Views/Signing/SigningView.swift
+++ b/Feather/Views/Signing/SigningView.swift
@@ -117,11 +117,22 @@ struct SigningView: View {
// ppq protection
if
_optionsManager.options.ppqProtection,
- let identifier = app.identifier,
let cert = _selectedCert(),
cert.ppQCheck
{
- _temporaryOptions.appIdentifier = "\(identifier).\(_optionsManager.options.ppqString)"
+ // Use the current identifier candidate: prefer temporary override if already set
+ let baseIdentifier = _temporaryOptions.appIdentifier ?? app.identifier
+ if let baseIdentifier,
+ !_optionsManager.options.ppqString.isEmpty
+ {
+ // Check only the last segment after the final dot
+ let lastSegment = baseIdentifier.split(separator: ".").last.map(String.init)
+ if lastSegment != _optionsManager.options.ppqString {
+ _temporaryOptions.appIdentifier = baseIdentifier + ".\(_optionsManager.options.ppqString)"
+ } else {
+ _temporaryOptions.appIdentifier = baseIdentifier
+ }
+ }
}
if
@@ -137,6 +148,23 @@ struct SigningView: View {
{
_temporaryOptions.appName = newName
}
+
+ // Auto-select certificate whose application-identifier exactly matches the target bundle id
+ if _optionsManager.options.autoSelectMatchingCertificate {
+ if let targetIdentifier = _temporaryOptions.appIdentifier ?? app.identifier {
+ if let exactIndex = certificates.firstIndex(where: { cert in
+ guard let ai = Storage.shared.getProvisionFileDecoded(for: cert)?.Entitlements?["application-identifier"]?.value as? String else { return false }
+ if ai == targetIdentifier { return true }
+ if let dotIndex = ai.firstIndex(of: ".") {
+ let remainder = String(ai[ai.index(after: dotIndex)...])
+ return remainder == targetIdentifier
+ }
+ return false
+ }) {
+ _temporaryCertificate = exactIndex
+ }
+ }
+ }
}
}
}
@@ -307,3 +335,4 @@ extension SigningView {
}
}
}
+
diff --git a/README.md b/README.md
index b2d89bce..7bf88f0d 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,7 @@ Due to how it works right now we need both a VPN and a lockdownd pairing file, t
- [Nuke](https://github.com/kean/Nuke) - Image caching.
- [Asspp](https://github.com/Lakr233/Asspp) - Some code for setting up the http server.
- [plistserver](https://github.com/nekohaxx/plistserver) - Hosted on https://api.palera.in.
+- [social-symbols](https://github.com/jeremieb/social-symbols/) - Additional logos.
## License
diff --git a/license_plist.yml b/license_plist.yml
index b2ac4c2a..b6cd8e4a 100644
--- a/license_plist.yml
+++ b/license_plist.yml
@@ -15,6 +15,11 @@ manual:
name: idevice
body: |-
MIT
+
+ - source: https://github.com/jeremieb/social-symbols
+ name: social-symbols
+ file: "Zsign/LICENSE_LC"
+
- name: "Zsign"
file: "Zsign/LICENSE"