// ── Referral Manager ────────────────────────────────────────── // "Invite a friend → both get 1 month Pro free" // Unique referral code per user, track referral chain import Foundation import Combine @MainActor final class ReferralManager: ObservableObject { static let shared = ReferralManager() @Published var myReferralCode: String @Published var referralCount: Int = 0 @Published var referredBy: String? @Published var proRewardMonths: Int = 0 @Published var referralHistory: [ReferralRecord] = [] private let codeKey = "chronomind-referral-code" private let countKey = "chronomind-referral-count" private let referredByKey = "chronomind-referred-by" private let rewardKey = "chronomind-pro-reward-months" private let historyKey = "chronomind-referral-history" private init() { // Generate or load existing referral code if let code = UserDefaults.standard.string(forKey: codeKey) { myReferralCode = code } else { let code = Self.generateCode() UserDefaults.standard.set(code, forKey: codeKey) myReferralCode = code } referralCount = UserDefaults.standard.integer(forKey: countKey) referredBy = UserDefaults.standard.string(forKey: referredByKey) proRewardMonths = UserDefaults.standard.integer(forKey: rewardKey) loadHistory() } // MARK: - Referral Link var referralURL: URL { URL(string: "https://chronomind.app/ref/\(myReferralCode)")! } var shareText: String { "Try ChronoMind — the time management app that actually works! Use my code \(myReferralCode) and we both get 1 month Pro free: \(referralURL.absoluteString)" } // MARK: - Apply Referral Code func applyReferralCode(_ code: String) -> ReferralResult { // Can't refer yourself guard code != myReferralCode else { return .failure("You can't use your own referral code") } // Can only be referred once guard referredBy == nil else { return .failure("You've already used a referral code") } // Validate code format guard Self.isValidCode(code) else { return .failure("Invalid referral code format") } // Apply referral referredBy = code UserDefaults.standard.set(code, forKey: referredByKey) // Reward: +1 month Pro for the new user proRewardMonths += 1 UserDefaults.standard.set(proRewardMonths, forKey: rewardKey) // In production, server would also credit the referrer // For now, just record locally let record = ReferralRecord( code: code, direction: .incoming, date: Date(), rewardMonths: 1 ) referralHistory.append(record) saveHistory() return .success(monthsEarned: 1) } // MARK: - Record Outgoing Referral (called when server confirms) func recordOutgoingReferral() { referralCount += 1 UserDefaults.standard.set(referralCount, forKey: countKey) proRewardMonths += 1 UserDefaults.standard.set(proRewardMonths, forKey: rewardKey) let record = ReferralRecord( code: myReferralCode, direction: .outgoing, date: Date(), rewardMonths: 1 ) referralHistory.append(record) saveHistory() } // MARK: - URL Handling func canHandleURL(_ url: URL) -> Bool { let host = url.host ?? "" return (host == "chronomind.app" || host == "chronom.ind") && url.path.hasPrefix("/ref/") } func extractCode(from url: URL) -> String? { let path = url.path if path.hasPrefix("/ref/") { return String(path.dropFirst(5)) } return nil } // MARK: - Code Generation & Validation static func generateCode() -> String { // Format: CM-XXXX (4 alphanumeric chars) let chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // No O/0/1/I to avoid confusion let random = String((0..<4).map { _ in chars.randomElement()! }) return "CM-\(random)" } static func isValidCode(_ code: String) -> Bool { let pattern = #"^CM-[A-HJ-NP-Z2-9]{4}$"# return code.range(of: pattern, options: .regularExpression) != nil } // MARK: - Persistence private func loadHistory() { guard let data = UserDefaults.standard.data(forKey: historyKey), let decoded = try? JSONDecoder().decode([ReferralRecord].self, from: data) else { return } referralHistory = decoded } private func saveHistory() { if let data = try? JSONEncoder().encode(referralHistory) { UserDefaults.standard.set(data, forKey: historyKey) } } } // MARK: - Models enum ReferralResult { case success(monthsEarned: Int) case failure(String) var isSuccess: Bool { if case .success = self { return true } return false } var message: String { switch self { case .success(let months): return "Referral applied! You earned \(months) month\(months == 1 ? "" : "s") of Pro free." case .failure(let reason): return reason } } } struct ReferralRecord: Codable, Identifiable { let id: String let code: String let direction: ReferralDirection let date: Date let rewardMonths: Int init(code: String, direction: ReferralDirection, date: Date, rewardMonths: Int) { self.id = UUID().uuidString self.code = code self.direction = direction self.date = date self.rewardMonths = rewardMonths } } enum ReferralDirection: String, Codable { case incoming = "incoming" // someone used your code case outgoing = "outgoing" // you used someone's code }