feat(share): add shareable timer links with universal link support + import/export
This commit is contained in:
parent
9be48f0abf
commit
fdcae8297a
177
ios/ChronoMind/Shared/Sharing/ShareableTimerManager.swift
Normal file
177
ios/ChronoMind/Shared/Sharing/ShareableTimerManager.swift
Normal file
@ -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)")!
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user