Release 1.4.0 (build 31): retención, quick update, streak, what's new
- 1A: Notificación mensual de resumen de portfolio (día 5 de cada mes) - 1B: Badge de racha mensual en Dashboard (streak counter) - 1C: Empty states mejorados en Goals y Journal con CTAs claros - 2A: Quick Update sheet — actualiza todas las fuentes desde una pantalla - 2B: Notificación batch update redirige al Quick Update sheet - 2C: Widget deep link portfoliojournal://quickupdate abre Quick Update - 3A: App Store version check banner en Settings - 3C: What's New sheet en primer launch de versión nueva - Localización completa en 7 idiomas para todas las nuevas strings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,9 @@ class NotificationService: ObservableObject {
|
||||
await MainActor.run {
|
||||
self.isAuthorized = granted
|
||||
}
|
||||
if granted {
|
||||
scheduleMonthlyPerformanceSummary()
|
||||
}
|
||||
return granted
|
||||
} catch {
|
||||
print("Notification authorization error: \(error)")
|
||||
@@ -155,6 +158,18 @@ extension NotificationService {
|
||||
func handleNotificationResponse(_ response: UNNotificationResponse) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
|
||||
// Handle batch update deep link from monthly check-in
|
||||
if let action = userInfo["action"] as? String, action == "batchUpdate" {
|
||||
NotificationCenter.default.post(name: .openBatchUpdate, object: nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle openDashboard deep link from monthly summary notification
|
||||
if let action = userInfo["action"] as? String, action == "openDashboard" {
|
||||
NotificationCenter.default.post(name: .openDashboard, object: nil)
|
||||
return
|
||||
}
|
||||
|
||||
guard let sourceIdString = userInfo["sourceId"] as? String,
|
||||
let sourceId = UUID(uuidString: sourceIdString) else {
|
||||
return
|
||||
@@ -174,6 +189,9 @@ extension NotificationService {
|
||||
extension Notification.Name {
|
||||
static let openSourceDetail = Notification.Name("openSourceDetail")
|
||||
static let didResetData = Notification.Name("didResetData")
|
||||
static let openBatchUpdate = Notification.Name("openBatchUpdate")
|
||||
static let openDashboard = Notification.Name("openDashboard")
|
||||
static let openQuickUpdate = Notification.Name("openQuickUpdate")
|
||||
}
|
||||
|
||||
// MARK: - Re-engagement Notifications
|
||||
@@ -214,6 +232,7 @@ extension NotificationService {
|
||||
content.title = String(localized: "monthly_checkin_notification_title")
|
||||
content.body = String(localized: "monthly_checkin_notification_body")
|
||||
content.sound = .default
|
||||
content.userInfo = ["action": "batchUpdate"]
|
||||
|
||||
var components = DateComponents()
|
||||
components.day = 1
|
||||
@@ -230,6 +249,56 @@ extension NotificationService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules a monthly portfolio summary notification on the 5th of each month at 9am.
|
||||
func scheduleMonthlyPerformanceSummary() {
|
||||
guard isAuthorized else { return }
|
||||
|
||||
let identifier = "monthly_summary"
|
||||
center.getPendingNotificationRequests { [weak self] requests in
|
||||
guard let self, !requests.contains(where: { $0.identifier == identifier }) else { return }
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = String(localized: "monthly_summary_notification_title")
|
||||
content.body = String(localized: "monthly_summary_notification_body")
|
||||
content.sound = .default
|
||||
content.userInfo = ["action": "openDashboard"]
|
||||
|
||||
var components = DateComponents()
|
||||
components.day = 5
|
||||
components.hour = 9
|
||||
components.minute = 0
|
||||
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||||
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
||||
|
||||
self.center.add(request) { error in
|
||||
if let error = error {
|
||||
print("Monthly summary notification error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fires a one-time local notification celebrating goal achievement.
|
||||
func scheduleGoalAchievedNotification(goalName: String) {
|
||||
guard isAuthorized else { return }
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = String(localized: "goal_achieved_notification_title")
|
||||
content.body = String(format: String(localized: "goal_achieved_notification_body"), goalName)
|
||||
content.sound = .default
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
let id = "goal_achieved_\(UUID().uuidString)"
|
||||
let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger)
|
||||
|
||||
center.add(request) { error in
|
||||
if let error = error {
|
||||
print("Goal achieved notification error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Background Refresh
|
||||
|
||||
Reference in New Issue
Block a user