feat(ios): fix Pass A compile blockers from code audit

This commit is contained in:
null 2026-06-20 22:54:21 -05:00
parent 8967fd23cd
commit cb54ed3079
9 changed files with 84 additions and 68 deletions

View File

@ -52,6 +52,17 @@ final class AppState: ObservableObject {
@Published var currentCouple: Couple?
@Published var isPremium = false
/// Derives the partner from the current couple + current user.
/// TODO(iOS): Full implementation needs a Firestore lookup of the partner user document.
/// For MVP, returns nil.
var currentPartner: User? {
guard let couple = currentCouple, let me = currentUser else { return nil }
let partnerId = couple.userIds.first { $0 != me.id }
_ = partnerId
// TODO(iOS): Fetch partner User from Firestore using partnerId.
return nil
}
private let authService = AuthService.shared
private let firestore = FirestoreService.shared
private var authTask: Task<Void, Never>?

View File

@ -30,6 +30,17 @@ final class BillingService: @unchecked Sendable {
try await FirestoreService.shared.syncEntitlementCallable()
}
/// Purchase the first package from the current offering
func purchase() async throws -> CustomerInfo {
let offerings = try await getOfferings()
guard let package = offerings.current?.availablePackages.first else {
throw BillingError.noAvailablePackage
}
let result = try await Purchases.shared.purchase(package: package)
try await FirestoreService.shared.syncEntitlementCallable()
return result.customerInfo
}
/// Restore previous purchases
func restorePurchases() async throws -> CustomerInfo {
try await Purchases.shared.restorePurchases()
@ -58,14 +69,14 @@ final class BillingService: @unchecked Sendable {
}
}
// MARK: - Entitlement Checker
// MARK: - Entitlement Checking
protocol EntitlementChecker: Actor {
protocol EntitlementChecking: Actor {
var isPremium: AsyncStream<Bool> { get }
func hasPremium() async -> Bool
}
final actor DefaultEntitlementChecker: EntitlementChecker {
final actor DefaultEntitlementChecker: EntitlementChecking {
nonisolated let isPremium: AsyncStream<Bool>
private let billing: BillingService
private let firestore: FirestoreService
@ -91,3 +102,16 @@ final actor DefaultEntitlementChecker: EntitlementChecker {
await billing.checkPremiumStatus()
}
}
// MARK: - Billing Errors
enum BillingError: LocalizedError {
case noAvailablePackage
var errorDescription: String? {
switch self {
case .noAvailablePackage:
return "No purchase package is currently available."
}
}
}

View File

@ -1,5 +1,7 @@
import Foundation
import FirebaseFirestoreSwift
struct User: Codable, Identifiable, Sendable {
let id: String
var email: String
@ -130,19 +132,3 @@ struct DailyAnswer: Codable, Identifiable, Sendable {
var submittedAt: Date
}
/// Timestamp wrapper for Firestore decoding
@propertyWrapper
struct DocumentID: Codable, Sendable {
var wrappedValue: String?
init(wrappedValue: String?) {
self.wrappedValue = wrappedValue
}
enum CodingKeys: CodingKey {}
func encode(to encoder: Encoder) throws {}
init(from decoder: Decoder) throws {
wrappedValue = nil
}
}

View File

@ -173,6 +173,31 @@ extension FirestoreService {
func sendGentleReminderCallable() async throws {
try await functions.httpsCallable("sendGentleReminderCallable").call()
}
func deleteUserCallable() async throws {
let result = try await functions.httpsCallable("deleteUserCallable").call()
guard let success = (result.data as? [String: Any])?["success"] as? Bool, success else {
throw FirestoreError.invalidResponse
}
}
func updateUserCallable(displayName: String, bio: String?) async throws {
var data: [String: Any] = ["displayName": displayName]
if let bio = bio {
data["bio"] = bio
}
let result = try await functions.httpsCallable("updateUserCallable").call(data)
guard let success = (result.data as? [String: Any])?["success"] as? Bool, success else {
throw FirestoreError.invalidResponse
}
}
func exportDataCallable() async throws {
let result = try await functions.httpsCallable("exportDataCallable").call()
guard let success = (result.data as? [String: Any])?["success"] as? Bool, success else {
throw FirestoreError.invalidResponse
}
}
}
// MARK: - Errors

View File

@ -795,24 +795,6 @@ struct PaywallView: View {
}
}
// MARK: - Premium Badge
struct PremiumBadge: View {
var body: some View {
HStack(spacing: 2) {
Image(systemName: "sparkle")
.font(.system(size: 8))
Text("Premium")
.font(.system(size: 9, weight: .bold))
}
.foregroundColor(.closerGold)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.closerGold.opacity(0.15))
.cornerRadius(CloserRadius.full)
}
}
// MARK: - Premium Feature Row
struct PremiumFeatureRow: View {
@ -889,27 +871,3 @@ struct FeatureBullet: View {
.padding(.vertical, 4)
}
}
// MARK: - Entitlement Checker
protocol EntitlementChecking {
func hasPremium() async -> Bool
}
struct DefaultEntitlementChecker: EntitlementChecking {
func hasPremium() async -> Bool {
do {
let customerInfo = try await Purchases.shared.customerInfo()
return customerInfo.entitlements.active["closer_premium"] != nil
} catch {
return false
}
}
}
#if DEBUG
struct MockEntitlementChecker: EntitlementChecking {
let isPremium: Bool
func hasPremium() async -> Bool { isPremium }
}
#endif

View File

@ -0,0 +1,9 @@
import XCTest
@testable import Closer
final class CloserTests: XCTestCase {
func testExample() throws {
// Placeholder add unit tests here.
XCTAssertTrue(true)
}
}

View File

@ -0,0 +1,8 @@
import XCTest
final class CloserUITests: XCTestCase {
func testExample() throws {
// Placeholder add UI tests here.
XCTAssertTrue(true)
}
}

View File

@ -24,10 +24,12 @@ let package = Package(
dependencies: [
.product(name: "FirebaseAuth", package: "firebase-ios-sdk"),
.product(name: "FirebaseFirestore", package: "firebase-ios-sdk"),
.product(name: "FirebaseFirestoreSwift", package: "firebase-ios-sdk"),
.product(name: "FirebaseFunctions", package: "firebase-ios-sdk"),
.product(name: "FirebaseMessaging", package: "firebase-ios-sdk"),
.product(name: "FirebaseStorage", package: "firebase-ios-sdk"),
.product(name: "RevenueCat", package: "purchases-ios"),
.product(name: "RevenueCatUI", package: "purchases-ios"),
.product(name: "GoogleSignIn", package: "GoogleSignIn-iOS"),
]
),

View File

@ -23,13 +23,6 @@ targets:
TARGETED_DEVICE_FAMILY: 1
INFOPLIST_FILE: Closer/Info.plist
dependencies:
- framework: FirebaseAuth
- framework: FirebaseFirestore
- framework: FirebaseFunctions
- framework: FirebaseMessaging
- framework: FirebaseStorage
- sdk: RevenueCat
- sdk: GoogleSignIn
preBuildScripts:
- name: "Run SwiftLint"
script: |