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:
alexandrev-tibco
2026-05-21 23:09:36 +02:00
parent 773da6800b
commit b48a47ce10
41 changed files with 2659 additions and 483 deletions
@@ -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