From cb54ed30795d76b41b10decad90c8f1c8e5fbd54 Mon Sep 17 00:00:00 2001 From: null Date: Sat, 20 Jun 2026 22:54:21 -0500 Subject: [PATCH] feat(ios): fix Pass A compile blockers from code audit --- iphone/Closer/CloserApp.swift | 11 +++++ .../Closer/Core/Billing/BillingService.swift | 30 +++++++++++-- iphone/Closer/Models/FirestoreModels.swift | 18 +------- iphone/Closer/Services/FirestoreService.swift | 25 +++++++++++ iphone/Closer/Settings/SettingsViews.swift | 42 ------------------- iphone/CloserTests/CloserTests.swift | 9 ++++ iphone/CloserUITests/CloserUITests.swift | 8 ++++ iphone/Package.swift | 2 + iphone/project.yml | 7 ---- 9 files changed, 84 insertions(+), 68 deletions(-) create mode 100644 iphone/CloserTests/CloserTests.swift create mode 100644 iphone/CloserUITests/CloserUITests.swift diff --git a/iphone/Closer/CloserApp.swift b/iphone/Closer/CloserApp.swift index 1696db72..7d141066 100644 --- a/iphone/Closer/CloserApp.swift +++ b/iphone/Closer/CloserApp.swift @@ -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? diff --git a/iphone/Closer/Core/Billing/BillingService.swift b/iphone/Closer/Core/Billing/BillingService.swift index 19abc0d8..78c4daa8 100644 --- a/iphone/Closer/Core/Billing/BillingService.swift +++ b/iphone/Closer/Core/Billing/BillingService.swift @@ -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 { get } func hasPremium() async -> Bool } -final actor DefaultEntitlementChecker: EntitlementChecker { +final actor DefaultEntitlementChecker: EntitlementChecking { nonisolated let isPremium: AsyncStream private let billing: BillingService private let firestore: FirestoreService @@ -90,4 +101,17 @@ final actor DefaultEntitlementChecker: EntitlementChecker { func hasPremium() async -> Bool { 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." + } + } } \ No newline at end of file diff --git a/iphone/Closer/Models/FirestoreModels.swift b/iphone/Closer/Models/FirestoreModels.swift index 26d8e390..d557fee4 100644 --- a/iphone/Closer/Models/FirestoreModels.swift +++ b/iphone/Closer/Models/FirestoreModels.swift @@ -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 - } -} \ No newline at end of file diff --git a/iphone/Closer/Services/FirestoreService.swift b/iphone/Closer/Services/FirestoreService.swift index f9f248ed..a3630ff1 100644 --- a/iphone/Closer/Services/FirestoreService.swift +++ b/iphone/Closer/Services/FirestoreService.swift @@ -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 diff --git a/iphone/Closer/Settings/SettingsViews.swift b/iphone/Closer/Settings/SettingsViews.swift index 80366ef0..1f6c6670 100644 --- a/iphone/Closer/Settings/SettingsViews.swift +++ b/iphone/Closer/Settings/SettingsViews.swift @@ -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 \ No newline at end of file diff --git a/iphone/CloserTests/CloserTests.swift b/iphone/CloserTests/CloserTests.swift new file mode 100644 index 00000000..b116cec5 --- /dev/null +++ b/iphone/CloserTests/CloserTests.swift @@ -0,0 +1,9 @@ +import XCTest +@testable import Closer + +final class CloserTests: XCTestCase { + func testExample() throws { + // Placeholder — add unit tests here. + XCTAssertTrue(true) + } +} diff --git a/iphone/CloserUITests/CloserUITests.swift b/iphone/CloserUITests/CloserUITests.swift new file mode 100644 index 00000000..3bdc9207 --- /dev/null +++ b/iphone/CloserUITests/CloserUITests.swift @@ -0,0 +1,8 @@ +import XCTest + +final class CloserUITests: XCTestCase { + func testExample() throws { + // Placeholder — add UI tests here. + XCTAssertTrue(true) + } +} diff --git a/iphone/Package.swift b/iphone/Package.swift index 3abbfa53..1e495f8f 100644 --- a/iphone/Package.swift +++ b/iphone/Package.swift @@ -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"), ] ), diff --git a/iphone/project.yml b/iphone/project.yml index 8013cd48..41a32703 100644 --- a/iphone/project.yml +++ b/iphone/project.yml @@ -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: |