Initial commit — iOS share extension for filing Gitea issues
Two-target Xcode project (xcodegen spec). The GiteaIssue container app holds the base URL + personal access token in a shared keychain group; the GiteaIssueShare extension reads them, surfaces a repo picker (with recents) and a title/notes form, then creates the issue, uploads the screenshot as an asset, and patches the body to embed it inline. Min iOS 26.0, signing team V3PF3M6B6U. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
enum Keychain {
|
||||
static let accessGroup = "$(AppIdentifierPrefix)com.treytartt.GiteaIssue"
|
||||
static let service = "com.treytartt.GiteaIssue"
|
||||
|
||||
static func read(_ key: String) -> String? {
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecReturnData as String: true,
|
||||
]
|
||||
if let group = resolvedAccessGroup() { query[kSecAttrAccessGroup as String] = group }
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status == errSecSuccess, let data = item as? Data else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
static func write(_ key: String, value: String) {
|
||||
let data = Data(value.utf8)
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
]
|
||||
if let group = resolvedAccessGroup() { query[kSecAttrAccessGroup as String] = group }
|
||||
|
||||
SecItemDelete(query as CFDictionary)
|
||||
|
||||
var add = query
|
||||
add[kSecValueData as String] = data
|
||||
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
SecItemAdd(add as CFDictionary, nil)
|
||||
}
|
||||
|
||||
static func delete(_ key: String) {
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
]
|
||||
if let group = resolvedAccessGroup() { query[kSecAttrAccessGroup as String] = group }
|
||||
SecItemDelete(query as CFDictionary)
|
||||
}
|
||||
|
||||
private static func resolvedAccessGroup() -> String? {
|
||||
guard let prefix = appIdentifierPrefix() else { return nil }
|
||||
return "\(prefix)com.treytartt.GiteaIssue"
|
||||
}
|
||||
|
||||
private static func appIdentifierPrefix() -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: "__appIdentifierPrefixProbe",
|
||||
kSecAttrService as String: service,
|
||||
kSecReturnAttributes as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
]
|
||||
var result: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
if status == errSecSuccess,
|
||||
let attrs = result as? [String: Any],
|
||||
let group = attrs[kSecAttrAccessGroup as String] as? String,
|
||||
let dot = group.firstIndex(of: ".") {
|
||||
return String(group[..<group.index(after: dot)])
|
||||
}
|
||||
if let bundleSeed = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as? String {
|
||||
return bundleSeed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user