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:
alexandrev-tibco
2026-07-02 22:12:32 +02:00
parent 19e80c912b
commit 197bddcd7e
81 changed files with 944 additions and 14 deletions
@@ -1,5 +1,34 @@
import SwiftUI
/// Support for automated App Store screenshot capture. Activated by launching the app
/// with the `--screenshots` argument (used by the UI-test capture flow). Only mutates
/// state when that argument is present, so it never affects normal runs.
enum ScreenshotMode {
static var isActive: Bool { CommandLine.arguments.contains("--screenshots") }
/// Skip onboarding, biometric/PIN lock and the What's New sheet so the app opens
/// straight into content. Called very early, before ContentView reads @AppStorage.
static func applyDefaultsIfNeeded() {
guard isActive else { return }
let d = UserDefaults.standard
d.set(true, forKey: "onboardingCompleted")
d.set(false, forKey: "faceIdEnabled")
d.set(false, forKey: "pinEnabled")
d.set(false, forKey: "lockOnLaunch")
d.set(false, forKey: "lockOnBackground")
d.set(false, forKey: "calmModeEnabled") // show all charts for richer screenshots
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
d.set(version, forKey: "lastSeenWhatsNewVersion")
}
/// Seed demo data (no-op if the store already has sources). Called once Core Data
/// has finished loading so the view context is ready.
static func seedIfNeeded() {
guard isActive else { return }
SampleDataService.shared.seedSampleData()
}
}
@main
struct PortfolioJournalApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@@ -12,6 +41,10 @@ struct PortfolioJournalApp: App {
let coreDataStack = CoreDataStack.shared
init() {
// Screenshot/UI-capture mode: skip onboarding, lock and What's New so the app
// launches straight into content with demo data (see ScreenshotMode).
ScreenshotMode.applyDefaultsIfNeeded()
// Clean up any duplicate objects from previous bugs before initializing stores
CoreDataStack.shared.cleanupDuplicateObjects()
@@ -40,6 +73,7 @@ struct PortfolioJournalApp: App {
coreDataStack.refreshFromCloudKit()
NotificationService.shared.scheduleReEngagementNotification()
NotificationService.shared.scheduleMonthlyCheckIn()
NotificationService.shared.scheduleStreakProtectionReminder()
} else if newPhase == .background {
guard iapService.isPremium else { return }
guard UserDefaults.standard.bool(forKey: "backupsEnabled") else { return }