Growth y monetización: fixes críticos, paywall multi-plan, teasers y App Intents
CRÍTICO — enlaces del App Store rotos (adquisición viral = 0): - GoalShareService.appStoreURL apuntaba a id6744983373 (404): el QR de TODAS las imágenes compartidas llevaba a una página muerta. Corregido a id6757678318. - SettingsView enlazaba id6741412965 (una novela romántica). Ahora usa la constante. Monetización: - IAPService multi-producto: sub anual (com.portfoliojournal.premium.annual, con intro offer/trial) + lifetime como ancla. isPremium acepta cualquiera de los dos. Paywall con selector de plan (anual por defecto si existe; degrada a solo lifetime mientras el producto no esté creado en App Store Connect). - paywallBenefits: añadidos Multiple Accounts y Family Sharing (diferenciadores). - Teaser de historia bloqueada en Charts: "N meses más con Premium" en vez de truncar en silencio (FreemiumValidator.hiddenHistoryMonths + banner). - Trigger de paywall de backups ahora instrumentado (logPaywallShown "backups"). Retención / adquisición: - Rating velocity: prompt tras 2 check-ins y 30 días (antes 3 + 90 — en una app mensual eso era >3 meses sin poder pedir la primera review con ~0 ratings). - Sample data ON por defecto en onboarding (skip ya no aterriza en dashboard vacío). - Notificación de protección de streak el día 25 si el check-in del mes está pendiente. - App Intents + AppShortcutsProvider: "Update my portfolio" / "Check my portfolio" vía Siri/Shortcuts/Spotlight. - Instrumentación: onboarding_step/onboarding_skipped y content_shared (viral loop). - ScreenshotMode (--screenshots) para capturas de marketing automatizadas. ASO: - 6 locales nuevos en metadata (en-GB, en-CA, en-AU, es-MX, fr-CA, pt-PT) reusando traducciones — 6 campos de keywords extra; pt-PT adaptado (reforma). - Categoría secundaria: PRODUCTIVITY. - 17 strings nuevas localizadas en los 7 idiomas. Pendiente manual: crear la sub anual en App Store Connect (grupo de suscripción + intro offer 7 días) con el id exacto com.portfoliojournal.premium.annual. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
This commit is contained in:
@@ -260,6 +260,44 @@ extension NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Streak protection: reminds the user on the 25th of the CURRENT month if this
|
||||
/// month's check-in is still pending — loss aversion beats the day-1 nudge alone.
|
||||
/// Safe to call on every app activation (no-op if already done or date passed).
|
||||
func scheduleStreakProtectionReminder() {
|
||||
guard isAuthorized else { return }
|
||||
|
||||
let identifier = "streak_protection"
|
||||
center.removePendingNotificationRequests(withIdentifiers: [identifier])
|
||||
|
||||
let calendar = Calendar.current
|
||||
let now = Date()
|
||||
guard let thisMonthStart = calendar.date(from: calendar.dateComponents([.year, .month], from: now)) else { return }
|
||||
|
||||
// Already checked in this month → nothing to protect
|
||||
guard MonthlyCheckInStore.completionDate(for: thisMonthStart.adding(days: 1)) == nil else { return }
|
||||
|
||||
var components = calendar.dateComponents([.year, .month], from: thisMonthStart)
|
||||
components.day = 25
|
||||
components.hour = 18
|
||||
components.minute = 0
|
||||
guard let fireDate = calendar.date(from: components), fireDate > now else { return }
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = String(localized: "streak_protection_notification_title")
|
||||
content.body = String(localized: "streak_protection_notification_body")
|
||||
content.sound = .default
|
||||
content.userInfo = ["action": "batchUpdate"]
|
||||
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
|
||||
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
||||
|
||||
center.add(request) { error in
|
||||
if let error = error {
|
||||
print("Streak protection notification error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules a monthly portfolio summary notification on the 5th of each month at 9am.
|
||||
func scheduleMonthlyPerformanceSummary() {
|
||||
guard isAuthorized else { return }
|
||||
|
||||
Reference in New Issue
Block a user