Add push notification deep linking and sharing subscription checks
- Add deep link navigation from push notifications to specific task column on kanban board - Fix subscription check in push notification handler to allow navigation when limitations disabled - Add pendingNavigationTaskId to handle notifications when app isn't ready - Add ScrollViewReader to AllTasksView for programmatic scrolling to task column - Add canShareResidence() and canShareContractor() subscription checks (iOS & Android) - Add test APNS file for simulator push notification testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,9 @@ struct CompleteTaskIntent: AppIntent {
|
||||
// Mark task as pending completion immediately (optimistic UI)
|
||||
WidgetActionManager.shared.markTaskPendingCompletion(taskId: taskId)
|
||||
|
||||
// Reload widget immediately to update task list and stats
|
||||
WidgetCenter.shared.reloadTimelines(ofKind: "Casera")
|
||||
|
||||
// Get auth token and API URL from shared container
|
||||
guard let token = WidgetActionManager.shared.getAuthToken() else {
|
||||
print("CompleteTaskIntent: No auth token available")
|
||||
|
||||
@@ -202,6 +202,52 @@ struct SimpleEntry: TimelineEntry {
|
||||
var nextTask: CacheManager.CustomTask? {
|
||||
upcomingTasks.first
|
||||
}
|
||||
|
||||
/// Tasks due within the next 7 days
|
||||
var dueThisWeekCount: Int {
|
||||
let calendar = Calendar.current
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let weekFromNow = calendar.date(byAdding: .day, value: 7, to: today)!
|
||||
|
||||
return upcomingTasks.filter { task in
|
||||
guard let dueDateString = task.dueDate else { return false }
|
||||
guard let dueDate = parseDate(dueDateString) else { return false }
|
||||
let dueDay = calendar.startOfDay(for: dueDate)
|
||||
return dueDay <= weekFromNow
|
||||
}.count
|
||||
}
|
||||
|
||||
/// Tasks due within the next 30 days
|
||||
var dueNext30DaysCount: Int {
|
||||
let calendar = Calendar.current
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let thirtyDaysFromNow = calendar.date(byAdding: .day, value: 30, to: today)!
|
||||
|
||||
return upcomingTasks.filter { task in
|
||||
guard let dueDateString = task.dueDate else { return false }
|
||||
guard let dueDate = parseDate(dueDateString) else { return false }
|
||||
let dueDay = calendar.startOfDay(for: dueDate)
|
||||
return dueDay <= thirtyDaysFromNow
|
||||
}.count
|
||||
}
|
||||
|
||||
/// Parse date string to Date
|
||||
private func parseDate(_ dateString: String) -> Date? {
|
||||
let dateOnlyFormatter = DateFormatter()
|
||||
dateOnlyFormatter.dateFormat = "yyyy-MM-dd"
|
||||
if let date = dateOnlyFormatter.date(from: dateString) {
|
||||
return date
|
||||
}
|
||||
|
||||
let isoFormatter = ISO8601DateFormatter()
|
||||
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
if let date = isoFormatter.date(from: dateString) {
|
||||
return date
|
||||
}
|
||||
|
||||
isoFormatter.formatOptions = [.withInternetDateTime]
|
||||
return isoFormatter.date(from: dateString)
|
||||
}
|
||||
}
|
||||
|
||||
struct CaseraEntryView : View {
|
||||
@@ -461,26 +507,12 @@ struct InteractiveTaskRowView: View {
|
||||
struct LargeWidgetView: View {
|
||||
let entry: SimpleEntry
|
||||
|
||||
private var maxTasksToShow: Int {
|
||||
entry.upcomingTasks.count > 6 ? 5 : 6
|
||||
}
|
||||
private var maxTasksToShow: Int { 5 }
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
// Header
|
||||
HStack(spacing: 6) {
|
||||
Spacer()
|
||||
|
||||
Text("\(entry.taskCount)")
|
||||
.font(.system(size: 22, weight: .bold))
|
||||
.foregroundStyle(.blue)
|
||||
|
||||
Text(entry.taskCount == 1 ? "task" : "tasks")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if entry.upcomingTasks.isEmpty {
|
||||
// Empty state - centered
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 12) {
|
||||
@@ -494,25 +526,99 @@ struct LargeWidgetView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Stats even when empty
|
||||
LargeWidgetStatsView(entry: entry)
|
||||
} else {
|
||||
// Show tasks with interactive buttons
|
||||
ForEach(Array(entry.upcomingTasks.prefix(maxTasksToShow).enumerated()), id: \.element.id) { index, task in
|
||||
LargeInteractiveTaskRowView(task: task)
|
||||
// Tasks section - always at top
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
ForEach(Array(entry.upcomingTasks.prefix(maxTasksToShow).enumerated()), id: \.element.id) { index, task in
|
||||
LargeInteractiveTaskRowView(task: task)
|
||||
}
|
||||
|
||||
if entry.upcomingTasks.count > maxTasksToShow {
|
||||
Text("+ \(entry.upcomingTasks.count - maxTasksToShow) more")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
}
|
||||
|
||||
if entry.upcomingTasks.count > 6 {
|
||||
Text("+ \(entry.upcomingTasks.count - 5) more tasks")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
Spacer(minLength: 12)
|
||||
|
||||
// Stats section at bottom
|
||||
LargeWidgetStatsView(entry: entry)
|
||||
}
|
||||
}
|
||||
.padding(14)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Large Widget Stats View
|
||||
struct LargeWidgetStatsView: View {
|
||||
let entry: SimpleEntry
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Total Tasks
|
||||
StatItem(
|
||||
value: entry.taskCount,
|
||||
label: "Total",
|
||||
color: .blue
|
||||
)
|
||||
|
||||
Divider()
|
||||
.frame(height: 30)
|
||||
|
||||
// Due This Week
|
||||
StatItem(
|
||||
value: entry.dueThisWeekCount,
|
||||
label: "This Week",
|
||||
color: .orange
|
||||
)
|
||||
|
||||
Divider()
|
||||
.frame(height: 30)
|
||||
|
||||
// Due Next 30 Days
|
||||
StatItem(
|
||||
value: entry.dueNext30DaysCount,
|
||||
label: "Next 30 Days",
|
||||
color: .green
|
||||
)
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 8)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.primary.opacity(0.05))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Stat Item View
|
||||
struct StatItem: View {
|
||||
let value: Int
|
||||
let label: String
|
||||
let color: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 2) {
|
||||
Text("\(value)")
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
.foregroundStyle(color)
|
||||
|
||||
Text(label)
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Large Interactive Task Row
|
||||
struct LargeInteractiveTaskRowView: View {
|
||||
let task: CacheManager.CustomTask
|
||||
@@ -530,7 +636,7 @@ struct LargeInteractiveTaskRowView: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(task.title)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.lineLimit(1)
|
||||
.lineLimit(2)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
HStack(spacing: 10) {
|
||||
|
||||
Reference in New Issue
Block a user