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:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user