feat(ios): fix Pass A compile blockers from code audit
This commit is contained in:
parent
73910bd459
commit
cd28f25234
|
|
@ -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>?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import XCTest
|
||||
@testable import Closer
|
||||
|
||||
final class CloserTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
// Placeholder — add unit tests here.
|
||||
XCTAssertTrue(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import XCTest
|
||||
|
||||
final class CloserUITests: XCTestCase {
|
||||
func testExample() throws {
|
||||
// Placeholder — add UI tests here.
|
||||
XCTAssertTrue(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
]
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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: |
|
||||
|
|
|
|||
Loading…
Reference in New Issue