feat(ios): fix Pass A compile blockers from code audit
This commit is contained in:
parent
8967fd23cd
commit
cb54ed3079
|
|
@ -52,6 +52,17 @@ final class AppState: ObservableObject {
|
||||||
@Published var currentCouple: Couple?
|
@Published var currentCouple: Couple?
|
||||||
@Published var isPremium = false
|
@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 authService = AuthService.shared
|
||||||
private let firestore = FirestoreService.shared
|
private let firestore = FirestoreService.shared
|
||||||
private var authTask: Task<Void, Never>?
|
private var authTask: Task<Void, Never>?
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,17 @@ final class BillingService: @unchecked Sendable {
|
||||||
try await FirestoreService.shared.syncEntitlementCallable()
|
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
|
/// Restore previous purchases
|
||||||
func restorePurchases() async throws -> CustomerInfo {
|
func restorePurchases() async throws -> CustomerInfo {
|
||||||
try await Purchases.shared.restorePurchases()
|
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 }
|
var isPremium: AsyncStream<Bool> { get }
|
||||||
func hasPremium() async -> Bool
|
func hasPremium() async -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
final actor DefaultEntitlementChecker: EntitlementChecker {
|
final actor DefaultEntitlementChecker: EntitlementChecking {
|
||||||
nonisolated let isPremium: AsyncStream<Bool>
|
nonisolated let isPremium: AsyncStream<Bool>
|
||||||
private let billing: BillingService
|
private let billing: BillingService
|
||||||
private let firestore: FirestoreService
|
private let firestore: FirestoreService
|
||||||
|
|
@ -90,4 +101,17 @@ final actor DefaultEntitlementChecker: EntitlementChecker {
|
||||||
func hasPremium() async -> Bool {
|
func hasPremium() async -> Bool {
|
||||||
await billing.checkPremiumStatus()
|
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 Foundation
|
||||||
|
|
||||||
|
import FirebaseFirestoreSwift
|
||||||
|
|
||||||
struct User: Codable, Identifiable, Sendable {
|
struct User: Codable, Identifiable, Sendable {
|
||||||
let id: String
|
let id: String
|
||||||
var email: String
|
var email: String
|
||||||
|
|
@ -130,19 +132,3 @@ struct DailyAnswer: Codable, Identifiable, Sendable {
|
||||||
var submittedAt: Date
|
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 {
|
func sendGentleReminderCallable() async throws {
|
||||||
try await functions.httpsCallable("sendGentleReminderCallable").call()
|
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
|
// 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
|
// MARK: - Premium Feature Row
|
||||||
|
|
||||||
struct PremiumFeatureRow: View {
|
struct PremiumFeatureRow: View {
|
||||||
|
|
@ -889,27 +871,3 @@ struct FeatureBullet: View {
|
||||||
.padding(.vertical, 4)
|
.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: [
|
dependencies: [
|
||||||
.product(name: "FirebaseAuth", package: "firebase-ios-sdk"),
|
.product(name: "FirebaseAuth", package: "firebase-ios-sdk"),
|
||||||
.product(name: "FirebaseFirestore", 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: "FirebaseFunctions", package: "firebase-ios-sdk"),
|
||||||
.product(name: "FirebaseMessaging", package: "firebase-ios-sdk"),
|
.product(name: "FirebaseMessaging", package: "firebase-ios-sdk"),
|
||||||
.product(name: "FirebaseStorage", package: "firebase-ios-sdk"),
|
.product(name: "FirebaseStorage", package: "firebase-ios-sdk"),
|
||||||
.product(name: "RevenueCat", package: "purchases-ios"),
|
.product(name: "RevenueCat", package: "purchases-ios"),
|
||||||
|
.product(name: "RevenueCatUI", package: "purchases-ios"),
|
||||||
.product(name: "GoogleSignIn", package: "GoogleSignIn-iOS"),
|
.product(name: "GoogleSignIn", package: "GoogleSignIn-iOS"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,6 @@ targets:
|
||||||
TARGETED_DEVICE_FAMILY: 1
|
TARGETED_DEVICE_FAMILY: 1
|
||||||
INFOPLIST_FILE: Closer/Info.plist
|
INFOPLIST_FILE: Closer/Info.plist
|
||||||
dependencies:
|
dependencies:
|
||||||
- framework: FirebaseAuth
|
|
||||||
- framework: FirebaseFirestore
|
|
||||||
- framework: FirebaseFunctions
|
|
||||||
- framework: FirebaseMessaging
|
|
||||||
- framework: FirebaseStorage
|
|
||||||
- sdk: RevenueCat
|
|
||||||
- sdk: GoogleSignIn
|
|
||||||
preBuildScripts:
|
preBuildScripts:
|
||||||
- name: "Run SwiftLint"
|
- name: "Run SwiftLint"
|
||||||
script: |
|
script: |
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue