193 lines
5.9 KiB
Swift
193 lines
5.9 KiB
Swift
// ── 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
|
|
}
|