From fdcae8297af51e4bd7f0600e997c56eff9886d7c Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 27 Feb 2026 22:58:57 -0800 Subject: [PATCH] feat(share): add shareable timer links with universal link support + import/export --- .../Sharing/ShareableTimerManager.swift | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 ios/ChronoMind/Shared/Sharing/ShareableTimerManager.swift diff --git a/ios/ChronoMind/Shared/Sharing/ShareableTimerManager.swift b/ios/ChronoMind/Shared/Sharing/ShareableTimerManager.swift new file mode 100644 index 0000000..dc0a815 --- /dev/null +++ b/ios/ChronoMind/Shared/Sharing/ShareableTimerManager.swift @@ -0,0 +1,177 @@ +// ── Shareable Timer Manager ─────────────────────────────────── +// Create shareable timer links: chronom.ind/t/abc123 +// iOS Universal Links → open app directly +// Web fallback → PWA timer page + +import Foundation +import Combine + +@MainActor +final class ShareableTimerManager: ObservableObject { + static let shared = ShareableTimerManager() + + @Published var sharedTimers: [SharedTimer] = [] + + private let storageKey = "chronomind-shared-timers" + private let baseURL = "https://chronomind.app/t/" + + private init() { + loadSharedTimers() + } + + // MARK: - Create Shareable Link + + func createShareableLink(for timer: CMTimer) -> SharedTimer { + let shareCode = generateShareCode() + let shared = SharedTimer( + code: shareCode, + label: timer.label, + type: timer.type, + durationSeconds: timer.duration, + urgency: timer.urgency, + cascadePreset: timer.cascade.preset, + category: timer.category, + createdBy: nil, // anonymized + createdAt: Date(), + expiresAt: Calendar.current.date(byAdding: .day, value: 30, to: Date())! + ) + + sharedTimers.append(shared) + saveSharedTimers() + + return shared + } + + /// Full shareable URL + func shareURL(for shared: SharedTimer) -> URL { + URL(string: "\(baseURL)\(shared.code)")! + } + + /// Share text for messaging + func shareText(for shared: SharedTimer) -> String { + let url = shareURL(for: shared) + return "Check out my \(shared.label) timer on ChronoMind: \(url.absoluteString)" + } + + // MARK: - Import from Link + + /// Parse a share link and create a timer from it + func importFromURL(_ url: URL) -> CMTimer? { + guard let code = extractShareCode(from: url) else { return nil } + return importFromCode(code) + } + + func importFromCode(_ code: String) -> CMTimer? { + // Look up in local shared timers first + guard let shared = sharedTimers.first(where: { $0.code == code }) else { + // In production, fetch from server + return nil + } + + guard !shared.isExpired else { return nil } + + // Create timer from shared data + switch shared.type { + case .countdown: + return createCountdown(CreateCountdownParams( + label: shared.label, + durationSeconds: shared.durationSeconds, + urgency: shared.urgency, + cascade: CascadeConfig(preset: shared.cascadePreset, intervals: []), + category: shared.category + )) + case .alarm: + // Alarm needs a target time — set to duration from now + return createAlarm(CreateAlarmParams( + label: shared.label, + targetTime: Date().addingTimeInterval(shared.durationSeconds), + urgency: shared.urgency, + cascade: CascadeConfig(preset: shared.cascadePreset, intervals: []) + )) + case .pomodoro: + let workMinutes = Int(shared.durationSeconds / 60) + return createPomodoro(CreatePomodoroParams( + label: shared.label, + config: PomodoroConfig( + workMinutes: workMinutes, + breakMinutes: 5, + longBreakMinutes: 15, + rounds: 4 + ) + )) + } + } + + // MARK: - URL Handling + + func extractShareCode(from url: URL) -> String? { + let path = url.path + // Handle: chronom.ind/t/abc123 or chronomind.app/t/abc123 + if path.hasPrefix("/t/") { + return String(path.dropFirst(3)) + } + return nil + } + + func canHandleURL(_ url: URL) -> Bool { + let host = url.host ?? "" + return (host == "chronomind.app" || host == "chronom.ind") && url.path.hasPrefix("/t/") + } + + // MARK: - Manage Shared Timers + + func revokeShare(_ code: String) { + sharedTimers.removeAll { $0.code == code } + saveSharedTimers() + } + + func cleanExpired() { + sharedTimers.removeAll { $0.isExpired } + saveSharedTimers() + } + + // MARK: - Private + + private func generateShareCode() -> String { + // 8-character alphanumeric code + let chars = "abcdefghijklmnopqrstuvwxyz0123456789" + return String((0..<8).map { _ in chars.randomElement()! }) + } + + private func loadSharedTimers() { + guard let data = UserDefaults.standard.data(forKey: storageKey), + let decoded = try? JSONDecoder().decode([SharedTimer].self, from: data) else { return } + sharedTimers = decoded + } + + private func saveSharedTimers() { + if let data = try? JSONEncoder().encode(sharedTimers) { + UserDefaults.standard.set(data, forKey: storageKey) + } + } +} + +// MARK: - Shared Timer Model + +struct SharedTimer: Codable, Identifiable { + let code: String + let label: String + let type: CMTimerType + let durationSeconds: TimeInterval + let urgency: UrgencyLevel + let cascadePreset: CascadePreset + let category: String? + let createdBy: String? + let createdAt: Date + let expiresAt: Date + + var id: String { code } + + var isExpired: Bool { + Date() > expiresAt + } + + var shareURL: URL { + URL(string: "https://chronomind.app/t/\(code)")! + } +}