fix: harden ios app build hygiene

This commit is contained in:
Peter Steinberger
2026-04-28 01:41:59 +01:00
parent 2fe213ebf2
commit b294f7c467
97 changed files with 1150 additions and 1044 deletions

View File

@@ -1,7 +1,7 @@
import Foundation
import UserNotifications
@preconcurrency import UserNotifications
struct ExecApprovalNotificationPrompt: Sendable, Equatable {
struct ExecApprovalNotificationPrompt: Equatable {
let approvalId: String
}
@@ -38,8 +38,7 @@ enum ExecApprovalNotificationBridge {
static func parsePrompt(
actionIdentifier: String,
userInfo: [AnyHashable: Any]
) -> ExecApprovalNotificationPrompt?
userInfo: [AnyHashable: Any]) -> ExecApprovalNotificationPrompt?
{
guard actionIdentifier == UNNotificationDefaultActionIdentifier
|| actionIdentifier == self.reviewActionIdentifier
@@ -54,8 +53,7 @@ enum ExecApprovalNotificationBridge {
@MainActor
static func handleResolvedPushIfNeeded(
userInfo: [AnyHashable: Any],
notificationCenter: NotificationCentering
) async -> Bool
notificationCenter: NotificationCentering) async -> Bool
{
guard self.payloadKind(userInfo: userInfo) == self.resolvedKind,
let approvalId = self.approvalID(from: userInfo)
@@ -70,8 +68,8 @@ enum ExecApprovalNotificationBridge {
@MainActor
static func removeNotifications(
forApprovalID approvalId: String,
notificationCenter: NotificationCentering
) async {
notificationCenter: NotificationCentering) async
{
let normalizedID = approvalId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalizedID.isEmpty else { return }

View File

@@ -84,7 +84,7 @@ actor PushRegistrationManager {
}
guard let installationId = GatewaySettingsStore.loadStableInstanceID()?
.trimmingCharacters(in: .whitespacesAndNewlines),
!installationId.isEmpty
!installationId.isEmpty
else {
throw PushRelayError.relayMisconfigured("Missing stable installation ID for relay registration")
}
@@ -145,7 +145,7 @@ actor PushRegistrationManager {
guard let expiresAtMs else { return true }
let nowMs = Int64(Date().timeIntervalSince1970 * 1000)
// Refresh shortly before expiry so reconnect-path republishes a live handle.
return expiresAtMs <= nowMs + 60_000
return expiresAtMs <= nowMs + 60000
}
private static func sha256Hex(_ value: String) -> String {

View File

@@ -24,7 +24,7 @@ enum PushRelayError: LocalizedError {
case .unsupportedAppAttest:
"App Attest unavailable on this device"
case .missingReceipt:
"App Store receipt missing after refresh"
"App Store app transaction missing after refresh"
}
}
}
@@ -85,33 +85,6 @@ private struct RelayErrorResponse: Decodable {
var reason: String?
}
private final class PushRelayReceiptRefreshCoordinator: NSObject, SKRequestDelegate {
private var continuation: CheckedContinuation<Void, Error>?
private var activeRequest: SKReceiptRefreshRequest?
func refresh() async throws {
try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
let request = SKReceiptRefreshRequest()
self.activeRequest = request
request.delegate = self
request.start()
}
}
func requestDidFinish(_ request: SKRequest) {
self.continuation?.resume(returning: ())
self.continuation = nil
self.activeRequest = nil
}
func request(_ request: SKRequest, didFailWithError error: Error) {
self.continuation?.resume(throwing: error)
self.continuation = nil
self.activeRequest = nil
}
}
private struct PushRelayAppAttestProof {
var keyId: String
var attestationObject: String?
@@ -197,25 +170,27 @@ private final class PushRelayAppAttestService {
private final class PushRelayReceiptProvider {
func loadReceiptBase64() async throws -> String {
if let receipt = self.readReceiptData() {
return receipt.base64EncodedString()
do {
let result = try await AppTransaction.shared
return try Self.appTransactionBase64(result)
} catch {
let refreshed = try await AppTransaction.refresh()
return try Self.appTransactionBase64(refreshed)
}
let refreshCoordinator = PushRelayReceiptRefreshCoordinator()
try await refreshCoordinator.refresh()
if let refreshed = self.readReceiptData() {
return refreshed.base64EncodedString()
}
throw PushRelayError.missingReceipt
}
private func readReceiptData() -> Data? {
guard let url = Bundle.main.appStoreReceiptURL else { return nil }
guard let data = try? Data(contentsOf: url), !data.isEmpty else { return nil }
return data
private static func appTransactionBase64(
_ result: StoreKit.VerificationResult<AppTransaction>) throws -> String
{
let jws = result.jwsRepresentation.trimmingCharacters(in: .whitespacesAndNewlines)
guard !jws.isEmpty else {
throw PushRelayError.missingReceipt
}
return Data(jws.utf8).base64EncodedString()
}
}
// The client is constructed once and used behind PushRegistrationManager actor isolation.
/// The client is constructed once and used behind PushRegistrationManager actor isolation.
final class PushRelayClient: @unchecked Sendable {
private let baseURL: URL
private let session: URLSession
@@ -294,8 +269,7 @@ final class PushRelayClient: @unchecked Sendable {
status: status,
message: Self.decodeErrorMessage(data: data))
}
let decoded = try self.decode(PushRelayRegisterResponse.self, from: data)
return decoded
return try self.decode(PushRelayRegisterResponse.self, from: data)
}
private func fetchChallenge() async throws -> PushRelayChallengeResponse {