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 }
|
||||
|
||||
@@ -190,6 +190,8 @@ class CoreDataStack: ObservableObject {
|
||||
// Migrate device-local UserDefaults data into Core Data so it syncs via iCloud.
|
||||
MonthlyContributionStore.migrateIfNeeded(context: container.viewContext)
|
||||
AllocationTargetStore.migrateIfNeeded(context: container.viewContext)
|
||||
// Seed demo data when running in screenshot capture mode.
|
||||
ScreenshotMode.seedIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -414,3 +414,22 @@
|
||||
"insight_forecast_title" = "Auf dem Weg zu";
|
||||
"notification_milestone_title" = "Portfolio-Meilenstein! 🎉";
|
||||
"notification_milestone_body" = "Dein Portfolio hat gerade %@ überschritten. Herzlichen Glückwunsch!";
|
||||
|
||||
"streak_protection_notification_title" = "Brich deine Serie nicht 🔥";
|
||||
"streak_protection_notification_body" = "Dein monatlicher Check-in steht noch aus. 2 Minuten genügen, um deine Serie zu halten.";
|
||||
|
||||
"paywall_benefit_accounts_title" = "Mehrere Konten";
|
||||
"paywall_benefit_accounts_subtitle" = "Getrennte Portfolios für Familie oder Business";
|
||||
"paywall_benefit_family_title" = "Familienfreigabe";
|
||||
"paywall_benefit_family_subtitle" = "Ein Kauf, bis zu 5 Familienmitglieder";
|
||||
"paywall_plan_annual" = "Jährlich";
|
||||
"paywall_plan_annual_per_year" = "%@ / Jahr";
|
||||
"paywall_plan_lifetime" = "Lebenslang";
|
||||
"paywall_plan_lifetime_badge" = "BESTER WERT";
|
||||
"paywall_trial_days" = "Tage";
|
||||
"paywall_trial_weeks" = "Wochen";
|
||||
"paywall_trial_months" = "Monate";
|
||||
"paywall_trial_years" = "Jahre";
|
||||
"paywall_trial_format" = "%d %@ gratis";
|
||||
|
||||
"chart_history_locked" = "%d weitere Monate Verlauf — mit Premium freischalten";
|
||||
|
||||
@@ -422,3 +422,22 @@
|
||||
"insight_forecast_title" = "On track for";
|
||||
"notification_milestone_title" = "Portfolio Milestone! 🎉";
|
||||
"notification_milestone_body" = "Your portfolio just passed %@. Congrats!";
|
||||
|
||||
"streak_protection_notification_title" = "Don't break your streak 🔥";
|
||||
"streak_protection_notification_body" = "Your monthly check-in is still pending. It takes 2 minutes to keep your streak alive.";
|
||||
|
||||
"paywall_benefit_accounts_title" = "Multiple Accounts";
|
||||
"paywall_benefit_accounts_subtitle" = "Separate portfolios for family or business";
|
||||
"paywall_benefit_family_title" = "Family Sharing";
|
||||
"paywall_benefit_family_subtitle" = "One purchase, up to 5 family members";
|
||||
"paywall_plan_annual" = "Annual";
|
||||
"paywall_plan_annual_per_year" = "%@ / year";
|
||||
"paywall_plan_lifetime" = "Lifetime";
|
||||
"paywall_plan_lifetime_badge" = "BEST VALUE";
|
||||
"paywall_trial_days" = "days";
|
||||
"paywall_trial_weeks" = "weeks";
|
||||
"paywall_trial_months" = "months";
|
||||
"paywall_trial_years" = "years";
|
||||
"paywall_trial_format" = "%d %@ free";
|
||||
|
||||
"chart_history_locked" = "%d more months of history — unlock with Premium";
|
||||
|
||||
@@ -366,3 +366,22 @@
|
||||
"insight_forecast_title" = "En camino hacia";
|
||||
"notification_milestone_title" = "¡Hito de cartera! 🎉";
|
||||
"notification_milestone_body" = "Tu cartera acaba de superar %@. ¡Enhorabuena!";
|
||||
|
||||
"streak_protection_notification_title" = "No rompas tu racha 🔥";
|
||||
"streak_protection_notification_body" = "Aún tienes pendiente el check-in de este mes. Son 2 minutos y tu racha sigue viva.";
|
||||
|
||||
"paywall_benefit_accounts_title" = "Varias cuentas";
|
||||
"paywall_benefit_accounts_subtitle" = "Carteras separadas para familia o negocio";
|
||||
"paywall_benefit_family_title" = "En familia";
|
||||
"paywall_benefit_family_subtitle" = "Una compra, hasta 5 miembros de la familia";
|
||||
"paywall_plan_annual" = "Anual";
|
||||
"paywall_plan_annual_per_year" = "%@ / año";
|
||||
"paywall_plan_lifetime" = "Para siempre";
|
||||
"paywall_plan_lifetime_badge" = "MEJOR VALOR";
|
||||
"paywall_trial_days" = "días";
|
||||
"paywall_trial_weeks" = "semanas";
|
||||
"paywall_trial_months" = "meses";
|
||||
"paywall_trial_years" = "años";
|
||||
"paywall_trial_format" = "%d %@ gratis";
|
||||
|
||||
"chart_history_locked" = "%d meses más de historial — desbloquéalos con Premium";
|
||||
|
||||
@@ -414,3 +414,22 @@
|
||||
"insight_forecast_title" = "En bonne voie pour";
|
||||
"notification_milestone_title" = "Jalon de portefeuille ! 🎉";
|
||||
"notification_milestone_body" = "Votre portefeuille vient de dépasser %@. Félicitations !";
|
||||
|
||||
"streak_protection_notification_title" = "Ne brisez pas votre série 🔥";
|
||||
"streak_protection_notification_body" = "Votre bilan mensuel est encore en attente. 2 minutes suffisent pour garder votre série.";
|
||||
|
||||
"paywall_benefit_accounts_title" = "Comptes multiples";
|
||||
"paywall_benefit_accounts_subtitle" = "Des portefeuilles séparés pour la famille ou le travail";
|
||||
"paywall_benefit_family_title" = "Partage familial";
|
||||
"paywall_benefit_family_subtitle" = "Un achat, jusqu'à 5 membres de la famille";
|
||||
"paywall_plan_annual" = "Annuel";
|
||||
"paywall_plan_annual_per_year" = "%@ / an";
|
||||
"paywall_plan_lifetime" = "À vie";
|
||||
"paywall_plan_lifetime_badge" = "MEILLEURE OFFRE";
|
||||
"paywall_trial_days" = "jours";
|
||||
"paywall_trial_weeks" = "semaines";
|
||||
"paywall_trial_months" = "mois";
|
||||
"paywall_trial_years" = "ans";
|
||||
"paywall_trial_format" = "%d %@ gratuits";
|
||||
|
||||
"chart_history_locked" = "%d mois d'historique en plus — débloquez avec Premium";
|
||||
|
||||
@@ -414,3 +414,22 @@
|
||||
"insight_forecast_title" = "In direzione di";
|
||||
"notification_milestone_title" = "Traguardo raggiunto! 🎉";
|
||||
"notification_milestone_body" = "Il tuo portafoglio ha appena superato %@. Complimenti!";
|
||||
|
||||
"streak_protection_notification_title" = "Non interrompere la tua serie 🔥";
|
||||
"streak_protection_notification_body" = "Il check-in di questo mese è ancora in sospeso. Bastano 2 minuti per mantenere viva la serie.";
|
||||
|
||||
"paywall_benefit_accounts_title" = "Più account";
|
||||
"paywall_benefit_accounts_subtitle" = "Portafogli separati per famiglia o lavoro";
|
||||
"paywall_benefit_family_title" = "In famiglia";
|
||||
"paywall_benefit_family_subtitle" = "Un acquisto, fino a 5 membri della famiglia";
|
||||
"paywall_plan_annual" = "Annuale";
|
||||
"paywall_plan_annual_per_year" = "%@ / anno";
|
||||
"paywall_plan_lifetime" = "Per sempre";
|
||||
"paywall_plan_lifetime_badge" = "MIGLIOR VALORE";
|
||||
"paywall_trial_days" = "giorni";
|
||||
"paywall_trial_weeks" = "settimane";
|
||||
"paywall_trial_months" = "mesi";
|
||||
"paywall_trial_years" = "anni";
|
||||
"paywall_trial_format" = "%d %@ gratis";
|
||||
|
||||
"chart_history_locked" = "%d mesi di storico in più — sblocca con Premium";
|
||||
|
||||
@@ -414,3 +414,22 @@
|
||||
"insight_forecast_title" = "目標に向けて順調";
|
||||
"notification_milestone_title" = "ポートフォリオ達成!🎉";
|
||||
"notification_milestone_body" = "ポートフォリオが%@を突破しました。おめでとうございます!";
|
||||
|
||||
"streak_protection_notification_title" = "連続記録を守ろう 🔥";
|
||||
"streak_protection_notification_body" = "今月のチェックインがまだ完了していません。2分で連続記録を守れます。";
|
||||
|
||||
"paywall_benefit_accounts_title" = "複数アカウント";
|
||||
"paywall_benefit_accounts_subtitle" = "家族用・事業用にポートフォリオを分けて管理";
|
||||
"paywall_benefit_family_title" = "ファミリー共有";
|
||||
"paywall_benefit_family_subtitle" = "1回の購入で家族5人まで利用可能";
|
||||
"paywall_plan_annual" = "年額";
|
||||
"paywall_plan_annual_per_year" = "%@ / 年";
|
||||
"paywall_plan_lifetime" = "買い切り";
|
||||
"paywall_plan_lifetime_badge" = "ベストバリュー";
|
||||
"paywall_trial_days" = "日間";
|
||||
"paywall_trial_weeks" = "週間";
|
||||
"paywall_trial_months" = "か月";
|
||||
"paywall_trial_years" = "年間";
|
||||
"paywall_trial_format" = "%d%@無料";
|
||||
|
||||
"chart_history_locked" = "さらに%dか月分の履歴 — プレミアムで解除";
|
||||
|
||||
@@ -414,3 +414,22 @@
|
||||
"insight_forecast_title" = "Caminhando para";
|
||||
"notification_milestone_title" = "Meta de portfólio! 🎉";
|
||||
"notification_milestone_body" = "Seu portfólio acabou de ultrapassar %@. Parabéns!";
|
||||
|
||||
"streak_protection_notification_title" = "Não quebre sua sequência 🔥";
|
||||
"streak_protection_notification_body" = "O check-in deste mês ainda está pendente. Leva 2 minutos para manter sua sequência viva.";
|
||||
|
||||
"paywall_benefit_accounts_title" = "Várias contas";
|
||||
"paywall_benefit_accounts_subtitle" = "Carteiras separadas para família ou negócios";
|
||||
"paywall_benefit_family_title" = "Compartilhamento Familiar";
|
||||
"paywall_benefit_family_subtitle" = "Uma compra, até 5 membros da família";
|
||||
"paywall_plan_annual" = "Anual";
|
||||
"paywall_plan_annual_per_year" = "%@ / ano";
|
||||
"paywall_plan_lifetime" = "Vitalício";
|
||||
"paywall_plan_lifetime_badge" = "MELHOR VALOR";
|
||||
"paywall_trial_days" = "dias";
|
||||
"paywall_trial_weeks" = "semanas";
|
||||
"paywall_trial_months" = "meses";
|
||||
"paywall_trial_years" = "anos";
|
||||
"paywall_trial_format" = "%d %@ grátis";
|
||||
|
||||
"chart_history_locked" = "%d meses a mais de histórico — desbloqueie com o Premium";
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import AppIntents
|
||||
import Foundation
|
||||
|
||||
// MARK: - App Intents (Siri / Shortcuts / Spotlight)
|
||||
//
|
||||
// Surfaces the core monthly habit outside the app: "Update my portfolio" opens
|
||||
// Quick Update directly, "Check my portfolio" opens the dashboard. Registering an
|
||||
// AppShortcutsProvider also lists these actions in Spotlight and the Shortcuts app
|
||||
// with zero user setup — a discovery/retention surface the app didn't have.
|
||||
|
||||
struct QuickUpdateIntent: AppIntent {
|
||||
static let title: LocalizedStringResource = "Update Portfolio"
|
||||
static let description = IntentDescription("Open Quick Update to record your latest portfolio values.")
|
||||
static let openAppWhenRun = true
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
// Same signal the widget deep link uses; ContentView routes it to the sheet.
|
||||
NotificationCenter.default.post(name: .openQuickUpdate, object: nil)
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenDashboardIntent: AppIntent {
|
||||
static let title: LocalizedStringResource = "Check Portfolio"
|
||||
static let description = IntentDescription("Open the dashboard to see your net worth and charts.")
|
||||
static let openAppWhenRun = true
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
NotificationCenter.default.post(name: .openDashboard, object: nil)
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
struct PortfolioJournalShortcuts: AppShortcutsProvider {
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
AppShortcut(
|
||||
intent: QuickUpdateIntent(),
|
||||
phrases: [
|
||||
"Update my portfolio in \(.applicationName)",
|
||||
"Add a snapshot in \(.applicationName)"
|
||||
],
|
||||
shortTitle: "Update Portfolio",
|
||||
systemImageName: "plus.circle.fill"
|
||||
)
|
||||
AppShortcut(
|
||||
intent: OpenDashboardIntent(),
|
||||
phrases: [
|
||||
"Check my portfolio in \(.applicationName)",
|
||||
"Show my net worth in \(.applicationName)"
|
||||
],
|
||||
shortTitle: "Check Portfolio",
|
||||
systemImageName: "chart.line.uptrend.xyaxis"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,31 @@ class FirebaseService {
|
||||
])
|
||||
}
|
||||
|
||||
/// Per-step onboarding funnel event — lets us see exactly where users drop off
|
||||
/// inside onboarding (only the final `onboarding_completed` existed before).
|
||||
func logOnboardingStep(step: Int) {
|
||||
guard isConfigured else { return }
|
||||
Analytics.logEvent("onboarding_step", parameters: [
|
||||
"step": step
|
||||
])
|
||||
}
|
||||
|
||||
func logOnboardingSkipped(atStep step: Int) {
|
||||
guard isConfigured else { return }
|
||||
Analytics.logEvent("onboarding_skipped", parameters: [
|
||||
"step": step
|
||||
])
|
||||
}
|
||||
|
||||
/// Viral loop measurement: fired whenever the user opens a share sheet with
|
||||
/// app content (portfolio image, check-in, goal). `type` identifies the surface.
|
||||
func logShare(type: String) {
|
||||
guard isConfigured else { return }
|
||||
Analytics.logEvent("content_shared", parameters: [
|
||||
"share_type": type
|
||||
])
|
||||
}
|
||||
|
||||
func logWidgetUsed(widgetType: String) {
|
||||
guard isConfigured else { return }
|
||||
Analytics.logEvent("widget_used", parameters: [
|
||||
|
||||
@@ -7,7 +7,9 @@ class GoalShareService {
|
||||
static let shared = GoalShareService()
|
||||
|
||||
/// App Store URL - auto-redirects to user's country
|
||||
static let appStoreURL = URL(string: "https://apps.apple.com/app/portfolio-journal/id6744983373")!
|
||||
/// NOTE: 6757678318 is the real ADAM id (verified via App Store Connect API).
|
||||
/// The previous id (6744983373) returned a 404, breaking the share QR loop.
|
||||
static let appStoreURL = URL(string: "https://apps.apple.com/app/portfolio-journal-tracker/id6757678318")!
|
||||
|
||||
/// Website URL - has Open Graph tags for social media previews
|
||||
static let websiteURL = URL(string: "https://portfoliojournal.app")!
|
||||
@@ -58,6 +60,7 @@ class GoalShareService {
|
||||
estimatedCompletionDate: Date? = nil,
|
||||
privacyMode: Bool = false
|
||||
) {
|
||||
FirebaseService.shared.logShare(type: "goal")
|
||||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let viewController = windowScene.windows.first?.rootViewController else {
|
||||
return
|
||||
|
||||
@@ -16,7 +16,13 @@ class IAPService: ObservableObject {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
/// Lifetime unlock (non-consumable). Existing product — keep the id stable.
|
||||
static let premiumProductID = "com.portfoliojournal.premium"
|
||||
/// Annual auto-renewable subscription with intro free trial. Acts as the primary
|
||||
/// offer; lifetime becomes the "best value" anchor. Must be created in App Store
|
||||
/// Connect with this exact id — until then the paywall gracefully shows lifetime only.
|
||||
static let annualProductID = "com.portfoliojournal.premium.annual"
|
||||
static let allProductIDs: Set<String> = [premiumProductID, annualProductID]
|
||||
static let premiumPrice = "€4.69"
|
||||
|
||||
// MARK: - Private Properties
|
||||
@@ -64,7 +70,7 @@ class IAPService: ObservableObject {
|
||||
|
||||
func loadProducts() async {
|
||||
do {
|
||||
products = try await Product.products(for: [Self.premiumProductID])
|
||||
products = try await Product.products(for: Self.allProductIDs)
|
||||
print("Loaded \(products.count) products")
|
||||
} catch {
|
||||
print("Failed to load products: \(error)")
|
||||
@@ -73,10 +79,16 @@ class IAPService: ObservableObject {
|
||||
|
||||
// MARK: - Purchase
|
||||
|
||||
/// Purchases the lifetime unlock (backwards-compatible entry point).
|
||||
func purchase() async throws {
|
||||
guard let product = products.first else {
|
||||
guard let product = premiumProduct ?? products.first else {
|
||||
throw IAPError.productNotFound
|
||||
}
|
||||
try await purchase(product)
|
||||
}
|
||||
|
||||
/// Purchases a specific product (annual subscription or lifetime unlock).
|
||||
func purchase(_ product: Product) async throws {
|
||||
|
||||
purchaseState = .purchasing
|
||||
|
||||
@@ -169,10 +181,13 @@ class IAPService: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
var entitledProductID = Self.premiumProductID
|
||||
for await result in StoreKit.Transaction.currentEntitlements {
|
||||
if case .verified(let transaction) = result {
|
||||
if transaction.productID == Self.premiumProductID {
|
||||
// Lifetime unlock OR an active annual subscription both grant premium.
|
||||
if Self.allProductIDs.contains(transaction.productID) {
|
||||
isEntitled = true
|
||||
entitledProductID = transaction.productID
|
||||
familyShared = transaction.ownershipType == .familyShared
|
||||
break
|
||||
}
|
||||
@@ -187,7 +202,7 @@ class IAPService: ObservableObject {
|
||||
let context = CoreDataStack.shared.viewContext
|
||||
PremiumStatus.updateStatus(
|
||||
isPremium: isEntitled,
|
||||
productIdentifier: Self.premiumProductID,
|
||||
productIdentifier: entitledProductID,
|
||||
transactionId: nil,
|
||||
isFamilyShared: familyShared,
|
||||
in: context
|
||||
@@ -238,9 +253,34 @@ class IAPService: ObservableObject {
|
||||
products.first { $0.id == Self.premiumProductID }
|
||||
}
|
||||
|
||||
/// Annual subscription, nil until the product exists in App Store Connect.
|
||||
var annualProduct: Product? {
|
||||
products.first { $0.id == Self.annualProductID }
|
||||
}
|
||||
|
||||
var formattedPrice: String {
|
||||
premiumProduct?.displayPrice ?? Self.premiumPrice
|
||||
}
|
||||
|
||||
var formattedAnnualPrice: String? {
|
||||
annualProduct?.displayPrice
|
||||
}
|
||||
|
||||
/// Localized description of the annual intro offer ("7 days free"), if configured.
|
||||
var annualTrialDescription: String? {
|
||||
guard let intro = annualProduct?.subscription?.introductoryOffer,
|
||||
intro.paymentMode == .freeTrial else { return nil }
|
||||
let period = intro.period
|
||||
let unit: String
|
||||
switch period.unit {
|
||||
case .day: unit = String(localized: "paywall_trial_days")
|
||||
case .week: unit = String(localized: "paywall_trial_weeks")
|
||||
case .month: unit = String(localized: "paywall_trial_months")
|
||||
case .year: unit = String(localized: "paywall_trial_years")
|
||||
@unknown default: unit = ""
|
||||
}
|
||||
return String(format: String(localized: "paywall_trial_format"), period.value, unit)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - IAP Error
|
||||
@@ -276,7 +316,8 @@ extension IAPService {
|
||||
("person.2", "Family Sharing", "Share with up to 5 family members")
|
||||
]
|
||||
|
||||
/// Condensed benefits shown on the paywall (4 max, outcome-focused)
|
||||
/// Condensed benefits shown on the paywall (outcome-focused). Multiple Accounts and
|
||||
/// Family Sharing are key differentiators vs competitors — keep them visible here.
|
||||
static var paywallBenefits: [(icon: String, title: String, subtitle: String)] {[
|
||||
("clock.arrow.circlepath",
|
||||
String(localized: "paywall_benefit_history_title"),
|
||||
@@ -287,6 +328,12 @@ extension IAPService {
|
||||
("wand.and.stars",
|
||||
String(localized: "paywall_benefit_forecasts_title"),
|
||||
String(localized: "paywall_benefit_forecasts_subtitle")),
|
||||
("person.2",
|
||||
String(localized: "paywall_benefit_accounts_title"),
|
||||
String(localized: "paywall_benefit_accounts_subtitle")),
|
||||
("person.3",
|
||||
String(localized: "paywall_benefit_family_title"),
|
||||
String(localized: "paywall_benefit_family_subtitle")),
|
||||
("xmark.circle",
|
||||
String(localized: "paywall_benefit_noads_title"),
|
||||
String(localized: "paywall_benefit_noads_subtitle"))
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -8,8 +8,11 @@ final class ReviewPromptService {
|
||||
private let checkInCountKey = "reviewPromptCheckinCount"
|
||||
private let hasCompletedStoreReviewKey = "reviewPromptHasCompletedStoreReview"
|
||||
private let promptedAchievementKeysKey = "reviewPromptedAchievementKeys"
|
||||
private let minCheckInsBetweenPrompts = 3
|
||||
private let minDaysBetweenPrompts = 90
|
||||
// Rating velocity: with a monthly-cadence app, 3 check-ins + 90 days meant 3+ months
|
||||
// before the first review could even be requested. 2 check-ins + 30 days keeps the
|
||||
// prompt tied to a good moment (a completed check-in) while building ratings sooner.
|
||||
private let minCheckInsBetweenPrompts = 2
|
||||
private let minDaysBetweenPrompts = 30
|
||||
private let userDefaults: UserDefaults
|
||||
private let dateProvider: () -> Date
|
||||
private let reviewRequestHandler: () -> Void
|
||||
|
||||
@@ -48,6 +48,7 @@ class ShareService {
|
||||
|
||||
@MainActor
|
||||
func shareMonthlyCheckIn(summary: MonthlySummary, appName: String) {
|
||||
FirebaseService.shared.logShare(type: "monthly_checkin")
|
||||
let text = Self.buildMonthlyCheckInShareText(summary: summary, appName: appName)
|
||||
shareCard(
|
||||
cardTitle: "\(summary.periodLabel) Check-in",
|
||||
@@ -69,6 +70,7 @@ class ShareService {
|
||||
yearChange: String?,
|
||||
sinceInceptionChange: String?
|
||||
) {
|
||||
FirebaseService.shared.logShare(type: "portfolio_value")
|
||||
let appName = Self.appDisplayName
|
||||
let text = Self.buildPortfolioValueShareText(
|
||||
totalValue: totalValue,
|
||||
|
||||
@@ -54,6 +54,27 @@ class FreemiumValidator: ObservableObject {
|
||||
return snapshots.filter { $0.date >= cutoffDate }
|
||||
}
|
||||
|
||||
/// Number of distinct months of data that exist BEYOND the free history window.
|
||||
/// Used to make the locked value concrete ("8 more months with Premium") instead
|
||||
/// of silently truncating — users can't want what they can't see.
|
||||
func hiddenHistoryMonths(in snapshots: [Snapshot]) -> Int {
|
||||
if iapService.isPremium { return 0 }
|
||||
|
||||
let cutoffDate = Calendar.current.date(
|
||||
byAdding: .month,
|
||||
value: -FreemiumLimits.maxHistoricalMonths,
|
||||
to: Date()
|
||||
) ?? Date()
|
||||
|
||||
let calendar = Calendar.current
|
||||
let hiddenMonths = Set(
|
||||
snapshots
|
||||
.filter { $0.date < cutoffDate }
|
||||
.map { calendar.dateComponents([.year, .month], from: $0.date) }
|
||||
)
|
||||
return hiddenMonths.count
|
||||
}
|
||||
|
||||
func isSnapshotAccessible(_ snapshot: Snapshot) -> Bool {
|
||||
if iapService.isPremium { return true }
|
||||
|
||||
|
||||
@@ -121,6 +121,9 @@ class ChartsViewModel: ObservableObject {
|
||||
@Published var predictionData: [Prediction] = []
|
||||
@Published var allocationEvolutionData: [(date: Date, category: String, percentage: Double, color: String)] = []
|
||||
@Published var yearOverYearData: [YearSeries] = []
|
||||
/// Months of data hidden by the free-tier 12-month history limit (0 for premium).
|
||||
/// Drives the "unlock X more months" teaser in the charts screen.
|
||||
@Published var hiddenHistoryMonths: Int = 0
|
||||
@Published var yoySelectedYears: Set<Int> = []
|
||||
|
||||
struct YearSeries: Identifiable {
|
||||
@@ -456,6 +459,7 @@ class ChartsViewModel: ObservableObject {
|
||||
for: sourceIds,
|
||||
months: monthsLimit
|
||||
)
|
||||
hiddenHistoryMonths = freemiumValidator.hiddenHistoryMonths(in: snapshots)
|
||||
snapshots = freemiumValidator.filterSnapshots(snapshots)
|
||||
cachedSnapshots = snapshots
|
||||
}
|
||||
|
||||
@@ -538,6 +538,7 @@ class SettingsViewModel: ObservableObject {
|
||||
guard isPremium else {
|
||||
backupsEnabled = false
|
||||
UserDefaults.standard.set(false, forKey: backupsEnabledKey)
|
||||
FirebaseService.shared.logPaywallShown(trigger: "backups")
|
||||
showingPaywall = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -141,6 +141,9 @@ struct ChartsContainerView: View {
|
||||
}
|
||||
}
|
||||
chartContent
|
||||
if viewModel.hiddenHistoryMonths > 0 {
|
||||
lockedHistoryTeaser
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -150,6 +153,30 @@ struct ChartsContainerView: View {
|
||||
.sheet(isPresented: $viewModel.showingPaywall) { PaywallView() }
|
||||
}
|
||||
|
||||
/// Concrete loss-framing upsell: the user has real data older than the free
|
||||
/// 12-month window; tell them exactly how much is locked instead of hiding it.
|
||||
private var lockedHistoryTeaser: some View {
|
||||
Button {
|
||||
FirebaseService.shared.logPaywallShown(trigger: "history_teaser")
|
||||
viewModel.showingPaywall = true
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "lock.fill")
|
||||
Text(String(format: String(localized: "chart_history_locked"), viewModel.hiddenHistoryMonths))
|
||||
.multilineTextAlignment(.leading)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
}
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(.appPrimary)
|
||||
.padding(12)
|
||||
.background(Color.appPrimary.opacity(0.08))
|
||||
.cornerRadius(AppConstants.UI.smallCornerRadius)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// MARK: - iPhone Layout
|
||||
|
||||
private var iPhoneChartsLayout: some View {
|
||||
@@ -166,6 +193,9 @@ struct ChartsContainerView: View {
|
||||
}
|
||||
filtersSection
|
||||
chartContent
|
||||
if viewModel.hiddenHistoryMonths > 0 {
|
||||
lockedHistoryTeaser
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ struct OnboardingView: View {
|
||||
|
||||
@State private var currentPage = 0
|
||||
@State private var selectedCurrency = Locale.current.currency?.identifier ?? "EUR"
|
||||
@State private var useSampleData = false
|
||||
// Default ON: a user who skips onboarding lands on a populated dashboard (instant
|
||||
// aha moment) instead of an empty screen. Sample data is clearly labeled and removable.
|
||||
@State private var useSampleData = true
|
||||
@AppStorage("calmModeEnabled") private var calmModeEnabled = true
|
||||
@AppStorage("cloudSyncEnabled") private var cloudSyncEnabled = false
|
||||
@State private var showingImportSheet = false
|
||||
@@ -90,6 +92,7 @@ struct OnboardingView: View {
|
||||
}
|
||||
|
||||
Button {
|
||||
FirebaseService.shared.logOnboardingSkipped(atStep: currentPage)
|
||||
completeOnboarding()
|
||||
} label: {
|
||||
Text("Skip")
|
||||
@@ -117,6 +120,9 @@ struct OnboardingView: View {
|
||||
.onAppear {
|
||||
selectedCurrency = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency
|
||||
}
|
||||
.onChange(of: currentPage) { _, newPage in
|
||||
FirebaseService.shared.logOnboardingStep(step: newPage)
|
||||
}
|
||||
.sheet(isPresented: $showingImportSheet) {
|
||||
ImportDataView(importContext: .onboarding)
|
||||
}
|
||||
@@ -220,6 +226,12 @@ struct OnboardingQuickStartView: View {
|
||||
let onImport: () -> Void
|
||||
let onAddSource: () -> Void
|
||||
|
||||
/// URL to the app's page in the system Settings app. Exposed as a static helper
|
||||
/// for testability (see OnboardingViewTests).
|
||||
static func appSettingsURL() -> URL? {
|
||||
URL(string: UIApplication.openSettingsURLString)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 28) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
|
||||
// MARK: - Chart Preview (decorative, no real data)
|
||||
|
||||
@@ -96,6 +97,11 @@ struct PaywallView: View {
|
||||
@State private var errorMessage: String?
|
||||
@State private var showingError = false
|
||||
|
||||
/// Which plan the user has highlighted. Defaults to annual (primary offer) when
|
||||
/// the subscription exists in App Store Connect; falls back to lifetime otherwise.
|
||||
enum Plan { case annual, lifetime }
|
||||
@State private var selectedPlan: Plan = .lifetime
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
AppBackground()
|
||||
@@ -116,7 +122,11 @@ struct PaywallView: View {
|
||||
|
||||
// Bottom actions
|
||||
VStack(spacing: 12) {
|
||||
priceLabel
|
||||
if iapService.annualProduct != nil {
|
||||
planSelector
|
||||
} else {
|
||||
priceLabel
|
||||
}
|
||||
purchaseButton
|
||||
restoreButton
|
||||
legalSection
|
||||
@@ -197,6 +207,72 @@ struct PaywallView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Plan Selector
|
||||
|
||||
/// Two-option selector: annual subscription (primary, with free trial when
|
||||
/// configured) and lifetime unlock as the "best value" anchor.
|
||||
private var planSelector: some View {
|
||||
VStack(spacing: 8) {
|
||||
if let annualPrice = iapService.formattedAnnualPrice {
|
||||
planRow(
|
||||
plan: .annual,
|
||||
title: String(localized: "paywall_plan_annual"),
|
||||
price: String(format: String(localized: "paywall_plan_annual_per_year"), annualPrice),
|
||||
badge: iapService.annualTrialDescription
|
||||
)
|
||||
}
|
||||
planRow(
|
||||
plan: .lifetime,
|
||||
title: String(localized: "paywall_plan_lifetime"),
|
||||
price: iapService.formattedPrice,
|
||||
badge: String(localized: "paywall_plan_lifetime_badge")
|
||||
)
|
||||
}
|
||||
.onAppear {
|
||||
if iapService.annualProduct != nil { selectedPlan = .annual }
|
||||
}
|
||||
}
|
||||
|
||||
private func planRow(plan: Plan, title: String, price: String, badge: String?) -> some View {
|
||||
Button {
|
||||
selectedPlan = plan
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 6) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.primary)
|
||||
if let badge, !badge.isEmpty {
|
||||
Text(badge)
|
||||
.font(.caption2.weight(.bold))
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.appSecondary.opacity(0.15))
|
||||
.foregroundColor(.appSecondary)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
Text(price)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: selectedPlan == plan ? "checkmark.circle.fill" : "circle")
|
||||
.foregroundColor(selectedPlan == plan ? .appPrimary : .secondary)
|
||||
}
|
||||
.padding(12)
|
||||
.background(Color(.systemBackground))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: AppConstants.UI.smallCornerRadius)
|
||||
.stroke(selectedPlan == plan ? Color.appPrimary : Color.gray.opacity(0.25),
|
||||
lineWidth: selectedPlan == plan ? 2 : 1)
|
||||
)
|
||||
.cornerRadius(AppConstants.UI.smallCornerRadius)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// MARK: - Purchase Button
|
||||
|
||||
private var purchaseButton: some View {
|
||||
@@ -259,11 +335,18 @@ struct PaywallView: View {
|
||||
|
||||
private func purchase() {
|
||||
isPurchasing = true
|
||||
FirebaseService.shared.logPurchaseAttempt(productId: IAPService.premiumProductID)
|
||||
let product = (selectedPlan == .annual ? iapService.annualProduct : iapService.premiumProduct)
|
||||
FirebaseService.shared.logPurchaseAttempt(
|
||||
productId: product?.id ?? IAPService.premiumProductID
|
||||
)
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await iapService.purchase()
|
||||
if let product {
|
||||
try await iapService.purchase(product)
|
||||
} else {
|
||||
try await iapService.purchase()
|
||||
}
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
showingError = true
|
||||
|
||||
@@ -194,7 +194,7 @@ struct SettingsView: View {
|
||||
|
||||
private var updateAvailableSection: some View {
|
||||
Section {
|
||||
Link(destination: URL(string: "https://apps.apple.com/app/id6741412965")!) {
|
||||
Link(destination: GoalShareService.appStoreURL) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.down.circle.fill")
|
||||
.foregroundColor(.appPrimary)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
Portfolio Journal is the investment tracker built for long-term investors who value simplicity and privacy. No brokerage login. No server. Your data stays on your device — or syncs privately via iCloud if you choose.
|
||||
|
||||
Just open the app once a month, enter your portfolio value, and let Portfolio Journal do the rest.
|
||||
|
||||
---
|
||||
|
||||
TRACK YOUR ENTIRE PORTFOLIO
|
||||
• Stocks, ETFs, bonds, crypto, real estate — any asset class
|
||||
• Multiple accounts: brokerage, retirement, savings, and more
|
||||
• Monthly check-in approach: no daily noise, just long-term perspective
|
||||
• Net worth evolution over months and years
|
||||
|
||||
VISUALIZE YOUR WEALTH
|
||||
• Evolution chart: see your portfolio grow over time
|
||||
• Allocation chart: know exactly where your money is
|
||||
• Drawdown analysis: understand your worst drops and recoveries
|
||||
• Interactive charts: Compare accounts, run What-If scenarios, and see Period vs Period
|
||||
• Year vs Year: see how each year stacks up, with a forecast for the year in progress
|
||||
• Calm Mode: a distraction-free view for peace of mind
|
||||
|
||||
SET AND TRACK GOALS
|
||||
• Create financial goals with target amounts and deadlines
|
||||
• Track progress with visual indicators
|
||||
• Stay motivated with a clear picture of your journey
|
||||
|
||||
KEEP AN INVESTMENT JOURNAL
|
||||
• Log thoughts, decisions, and lessons learned
|
||||
• Build a personal record of your investment mindset over time
|
||||
• Reflect on past decisions to improve future ones
|
||||
|
||||
PRIVACY BY DESIGN
|
||||
• No account required. No login. No sign-up.
|
||||
• No analytics, no tracking, no data selling — ever.
|
||||
• iCloud sync is optional: you control it, and it's encrypted end-to-end.
|
||||
• Everything works 100% offline.
|
||||
|
||||
BUILT FOR PASSIVE INVESTORS
|
||||
Portfolio Journal is designed for Bogleheads, ETF investors, index fund fans, and anyone following a buy-and-hold strategy. It's not a trading tool — it's a long-term companion.
|
||||
|
||||
Whether you're tracking a three-fund portfolio, building toward FIRE, or simply monitoring your net worth over time, Portfolio Journal gives you clarity without complexity.
|
||||
|
||||
EXPORT YOUR DATA
|
||||
• CSV export: take your data anywhere, anytime.
|
||||
• Full ownership of your financial history.
|
||||
|
||||
---
|
||||
|
||||
WHO IS PORTFOLIO JOURNAL FOR?
|
||||
- Long-term and passive investors who don't need daily updates
|
||||
- ETF and index fund enthusiasts
|
||||
- Bogleheads and FIRE community members
|
||||
- Anyone tracking their net worth or wealth-building journey
|
||||
- Privacy-conscious users who don't want their financial data on someone else's servers
|
||||
|
||||
---
|
||||
|
||||
Download Portfolio Journal today and start building a clear, private, long-term view of your financial future.
|
||||
@@ -0,0 +1 @@
|
||||
investing,dividend,crypto,fund,index,retirement,assets,money,passive,FIRE,compound,boglehead,savings
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app
|
||||
@@ -0,0 +1 @@
|
||||
Portfolio Journal: Net Worth
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/privacy.html
|
||||
@@ -0,0 +1 @@
|
||||
No brokerage login. No servers. No tracking. Update your portfolio monthly, watch your net worth grow with clear charts, and reach your FIRE goals. Optional iCloud sync.
|
||||
@@ -0,0 +1,9 @@
|
||||
New charts, quick updates and portfolio insights:
|
||||
|
||||
• Quick Update — add snapshots to all your sources at once from the Dashboard
|
||||
• Portfolio Insights — smart observations about your portfolio (streak, gains, milestones, forecasts)
|
||||
• 3 new Premium charts: Compare Sources, What If Simulator, Period vs Period
|
||||
• Monthly contribution — set a default contribution per source, apply retroactively
|
||||
• Streak badge — see your consecutive months tracking streak on the Dashboard
|
||||
• Smarter notifications — check-in reminders skip months you have already completed; milestone alerts when your portfolio crosses round numbers
|
||||
• Bug fixes and performance improvements
|
||||
@@ -0,0 +1 @@
|
||||
Stocks, ETF & Wealth Tracker
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/support.html
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
Portfolio Journal is the investment tracker built for long-term investors who value simplicity and privacy. No brokerage login. No server. Your data stays on your device — or syncs privately via iCloud if you choose.
|
||||
|
||||
Just open the app once a month, enter your portfolio value, and let Portfolio Journal do the rest.
|
||||
|
||||
---
|
||||
|
||||
TRACK YOUR ENTIRE PORTFOLIO
|
||||
• Stocks, ETFs, bonds, crypto, real estate — any asset class
|
||||
• Multiple accounts: brokerage, retirement, savings, and more
|
||||
• Monthly check-in approach: no daily noise, just long-term perspective
|
||||
• Net worth evolution over months and years
|
||||
|
||||
VISUALIZE YOUR WEALTH
|
||||
• Evolution chart: see your portfolio grow over time
|
||||
• Allocation chart: know exactly where your money is
|
||||
• Drawdown analysis: understand your worst drops and recoveries
|
||||
• Interactive charts: Compare accounts, run What-If scenarios, and see Period vs Period
|
||||
• Year vs Year: see how each year stacks up, with a forecast for the year in progress
|
||||
• Calm Mode: a distraction-free view for peace of mind
|
||||
|
||||
SET AND TRACK GOALS
|
||||
• Create financial goals with target amounts and deadlines
|
||||
• Track progress with visual indicators
|
||||
• Stay motivated with a clear picture of your journey
|
||||
|
||||
KEEP AN INVESTMENT JOURNAL
|
||||
• Log thoughts, decisions, and lessons learned
|
||||
• Build a personal record of your investment mindset over time
|
||||
• Reflect on past decisions to improve future ones
|
||||
|
||||
PRIVACY BY DESIGN
|
||||
• No account required. No login. No sign-up.
|
||||
• No analytics, no tracking, no data selling — ever.
|
||||
• iCloud sync is optional: you control it, and it's encrypted end-to-end.
|
||||
• Everything works 100% offline.
|
||||
|
||||
BUILT FOR PASSIVE INVESTORS
|
||||
Portfolio Journal is designed for Bogleheads, ETF investors, index fund fans, and anyone following a buy-and-hold strategy. It's not a trading tool — it's a long-term companion.
|
||||
|
||||
Whether you're tracking a three-fund portfolio, building toward FIRE, or simply monitoring your net worth over time, Portfolio Journal gives you clarity without complexity.
|
||||
|
||||
EXPORT YOUR DATA
|
||||
• CSV export: take your data anywhere, anytime.
|
||||
• Full ownership of your financial history.
|
||||
|
||||
---
|
||||
|
||||
WHO IS PORTFOLIO JOURNAL FOR?
|
||||
- Long-term and passive investors who don't need daily updates
|
||||
- ETF and index fund enthusiasts
|
||||
- Bogleheads and FIRE community members
|
||||
- Anyone tracking their net worth or wealth-building journey
|
||||
- Privacy-conscious users who don't want their financial data on someone else's servers
|
||||
|
||||
---
|
||||
|
||||
Download Portfolio Journal today and start building a clear, private, long-term view of your financial future.
|
||||
@@ -0,0 +1 @@
|
||||
investing,dividend,crypto,fund,index,retirement,assets,money,passive,FIRE,compound,boglehead,savings
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app
|
||||
@@ -0,0 +1 @@
|
||||
Portfolio Journal: Net Worth
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/privacy.html
|
||||
@@ -0,0 +1 @@
|
||||
No brokerage login. No servers. No tracking. Update your portfolio monthly, watch your net worth grow with clear charts, and reach your FIRE goals. Optional iCloud sync.
|
||||
@@ -0,0 +1,9 @@
|
||||
New charts, quick updates and portfolio insights:
|
||||
|
||||
• Quick Update — add snapshots to all your sources at once from the Dashboard
|
||||
• Portfolio Insights — smart observations about your portfolio (streak, gains, milestones, forecasts)
|
||||
• 3 new Premium charts: Compare Sources, What If Simulator, Period vs Period
|
||||
• Monthly contribution — set a default contribution per source, apply retroactively
|
||||
• Streak badge — see your consecutive months tracking streak on the Dashboard
|
||||
• Smarter notifications — check-in reminders skip months you have already completed; milestone alerts when your portfolio crosses round numbers
|
||||
• Bug fixes and performance improvements
|
||||
@@ -0,0 +1 @@
|
||||
Stocks, ETF & Wealth Tracker
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/support.html
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
Portfolio Journal is the investment tracker built for long-term investors who value simplicity and privacy. No brokerage login. No server. Your data stays on your device — or syncs privately via iCloud if you choose.
|
||||
|
||||
Just open the app once a month, enter your portfolio value, and let Portfolio Journal do the rest.
|
||||
|
||||
---
|
||||
|
||||
TRACK YOUR ENTIRE PORTFOLIO
|
||||
• Stocks, ETFs, bonds, crypto, real estate — any asset class
|
||||
• Multiple accounts: brokerage, retirement, savings, and more
|
||||
• Monthly check-in approach: no daily noise, just long-term perspective
|
||||
• Net worth evolution over months and years
|
||||
|
||||
VISUALIZE YOUR WEALTH
|
||||
• Evolution chart: see your portfolio grow over time
|
||||
• Allocation chart: know exactly where your money is
|
||||
• Drawdown analysis: understand your worst drops and recoveries
|
||||
• Interactive charts: Compare accounts, run What-If scenarios, and see Period vs Period
|
||||
• Year vs Year: see how each year stacks up, with a forecast for the year in progress
|
||||
• Calm Mode: a distraction-free view for peace of mind
|
||||
|
||||
SET AND TRACK GOALS
|
||||
• Create financial goals with target amounts and deadlines
|
||||
• Track progress with visual indicators
|
||||
• Stay motivated with a clear picture of your journey
|
||||
|
||||
KEEP AN INVESTMENT JOURNAL
|
||||
• Log thoughts, decisions, and lessons learned
|
||||
• Build a personal record of your investment mindset over time
|
||||
• Reflect on past decisions to improve future ones
|
||||
|
||||
PRIVACY BY DESIGN
|
||||
• No account required. No login. No sign-up.
|
||||
• No analytics, no tracking, no data selling — ever.
|
||||
• iCloud sync is optional: you control it, and it's encrypted end-to-end.
|
||||
• Everything works 100% offline.
|
||||
|
||||
BUILT FOR PASSIVE INVESTORS
|
||||
Portfolio Journal is designed for Bogleheads, ETF investors, index fund fans, and anyone following a buy-and-hold strategy. It's not a trading tool — it's a long-term companion.
|
||||
|
||||
Whether you're tracking a three-fund portfolio, building toward FIRE, or simply monitoring your net worth over time, Portfolio Journal gives you clarity without complexity.
|
||||
|
||||
EXPORT YOUR DATA
|
||||
• CSV export: take your data anywhere, anytime.
|
||||
• Full ownership of your financial history.
|
||||
|
||||
---
|
||||
|
||||
WHO IS PORTFOLIO JOURNAL FOR?
|
||||
- Long-term and passive investors who don't need daily updates
|
||||
- ETF and index fund enthusiasts
|
||||
- Bogleheads and FIRE community members
|
||||
- Anyone tracking their net worth or wealth-building journey
|
||||
- Privacy-conscious users who don't want their financial data on someone else's servers
|
||||
|
||||
---
|
||||
|
||||
Download Portfolio Journal today and start building a clear, private, long-term view of your financial future.
|
||||
@@ -0,0 +1 @@
|
||||
investing,dividend,crypto,fund,index,retirement,assets,money,passive,FIRE,compound,boglehead,savings
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app
|
||||
@@ -0,0 +1 @@
|
||||
Portfolio Journal: Net Worth
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/privacy.html
|
||||
@@ -0,0 +1 @@
|
||||
No brokerage login. No servers. No tracking. Update your portfolio monthly, watch your net worth grow with clear charts, and reach your FIRE goals. Optional iCloud sync.
|
||||
@@ -0,0 +1,9 @@
|
||||
New charts, quick updates and portfolio insights:
|
||||
|
||||
• Quick Update — add snapshots to all your sources at once from the Dashboard
|
||||
• Portfolio Insights — smart observations about your portfolio (streak, gains, milestones, forecasts)
|
||||
• 3 new Premium charts: Compare Sources, What If Simulator, Period vs Period
|
||||
• Monthly contribution — set a default contribution per source, apply retroactively
|
||||
• Streak badge — see your consecutive months tracking streak on the Dashboard
|
||||
• Smarter notifications — check-in reminders skip months you have already completed; milestone alerts when your portfolio crosses round numbers
|
||||
• Bug fixes and performance improvements
|
||||
@@ -0,0 +1 @@
|
||||
Stocks, ETF & Wealth Tracker
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/support.html
|
||||
@@ -0,0 +1,55 @@
|
||||
Portfolio Journal es el diario de inversión para quienes invierten a largo plazo y valoran la privacidad. Sin acceso a brokers. Sin servidores. Tus datos permanecen en tu dispositivo — o se sincronizan de forma privada con iCloud si así lo decides.
|
||||
|
||||
Abre la app una vez al mes, introduce el valor de tu cartera y deja que Portfolio Journal haga el resto.
|
||||
|
||||
---
|
||||
|
||||
REGISTRA TODA TU CARTERA
|
||||
• Acciones, ETF, bonos, cripto, inmuebles — cualquier tipo de activo
|
||||
• Múltiples cuentas: broker, pensiones, ahorros y más
|
||||
• Enfoque de check-in mensual: sin ruido diario, solo perspectiva a largo plazo
|
||||
• Evolución de tu patrimonio a lo largo de meses y años
|
||||
|
||||
VISUALIZA TU RIQUEZA
|
||||
• Gráfico de evolución: observa cómo crece tu cartera con el tiempo
|
||||
• Gráfico de asignación: sabe exactamente dónde está tu dinero
|
||||
• Análisis de drawdown: comprende tus peores caídas y recuperaciones
|
||||
• Modo Calma: una vista sin distracciones para tu tranquilidad
|
||||
|
||||
ESTABLECE Y ALCANZA METAS
|
||||
• Crea objetivos financieros con importe y fecha límite
|
||||
• Sigue tu progreso con indicadores visuales
|
||||
• Mantén la motivación con una visión clara de tu camino
|
||||
|
||||
LLEVA UN DIARIO DE INVERSIÓN
|
||||
• Anota pensamientos, decisiones y lecciones aprendidas
|
||||
• Construye un registro personal de tu mentalidad inversora
|
||||
• Reflexiona sobre decisiones pasadas para mejorar las futuras
|
||||
|
||||
PRIVACIDAD POR DISEÑO
|
||||
• Sin cuenta, sin login, sin registro.
|
||||
• Sin analíticas, sin rastreo, sin venta de datos — nunca.
|
||||
• La sincronización con iCloud es opcional: tú la controlas, y está cifrada de extremo a extremo.
|
||||
• Funciona al 100% sin conexión.
|
||||
|
||||
DISEÑADO PARA INVERSORES PASIVOS
|
||||
Portfolio Journal está pensado para seguidores de Boglehead, inversores en ETF e índices, y cualquiera que siga una estrategia de compra y mantenimiento. No es una herramienta de trading — es un compañero a largo plazo.
|
||||
|
||||
Ya sea que gestiones una cartera de tres fondos, avances hacia la independencia financiera (FIRE) o simplemente controles tu patrimonio neto, Portfolio Journal te da claridad sin complejidad.
|
||||
|
||||
EXPORTA TUS DATOS
|
||||
• Exportación a CSV: lleva tus datos donde quieras, cuando quieras.
|
||||
• Plena propiedad de tu historial financiero.
|
||||
|
||||
---
|
||||
|
||||
¿PARA QUIÉN ES PORTFOLIO JOURNAL?
|
||||
- Inversores pasivos y a largo plazo que no necesitan actualizaciones diarias
|
||||
- Aficionados a ETF y fondos indexados
|
||||
- Comunidad Boglehead y FIRE
|
||||
- Cualquiera que controle su patrimonio neto o su camino hacia la libertad financiera
|
||||
- Usuarios que valoran su privacidad y no quieren sus datos financieros en servidores ajenos
|
||||
|
||||
---
|
||||
|
||||
Descarga Portfolio Journal hoy y empieza a construir una visión clara, privada y a largo plazo de tu futuro financiero.
|
||||
@@ -0,0 +1 @@
|
||||
dividendos,cripto,fondo,indexado,jubilación,ahorro,dinero,finanzas,FIRE,cartera,bolsa,interés,metas
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app
|
||||
@@ -0,0 +1 @@
|
||||
Portfolio Journal: Patrimonio
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/privacy.html
|
||||
@@ -0,0 +1 @@
|
||||
Sin acceso a brokers, sin servidores, sin rastreo. Actualiza tu cartera cada mes, ve crecer tu patrimonio con gráficos claros y alcanza tu meta FIRE. Con iCloud opcional.
|
||||
@@ -0,0 +1,9 @@
|
||||
Nuevos gráficos, actualizaciones rápidas e insights de cartera:
|
||||
|
||||
• Actualización rápida — añade snapshots a todas tus fuentes de una vez desde el Dashboard
|
||||
• Insights de cartera — observaciones inteligentes sobre tu cartera (racha, ganancias, hitos, previsiones)
|
||||
• 3 nuevos gráficos Premium: Comparar Fuentes, Simulador ¿Y si...?, Período vs Período
|
||||
• Contribución mensual — configura una contribución predeterminada por fuente, aplícala retroactivamente
|
||||
• Racha — visualiza tu racha de meses consecutivos de seguimiento en el Dashboard
|
||||
• Notificaciones más inteligentes — los recordatorios saltan meses ya completados; alertas al cruzar hitos redondos
|
||||
• Correcciones de errores y mejoras de rendimiento
|
||||
@@ -0,0 +1 @@
|
||||
Inversiones, Acciones y ETF
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/support.html
|
||||
@@ -0,0 +1,55 @@
|
||||
Portfolio Journal est l'application de suivi d'investissements conçue pour les investisseurs à long terme qui privilégient la simplicité et la confidentialité. Aucune connexion à votre courtier. Aucun serveur. Vos données restent sur votre appareil — ou se synchronisent en privé via iCloud si vous le souhaitez.
|
||||
|
||||
Ouvrez l'application une fois par mois, saisissez la valeur de votre portefeuille et laissez Portfolio Journal faire le reste.
|
||||
|
||||
---
|
||||
|
||||
SUIVEZ L'INTÉGRALITÉ DE VOTRE PORTEFEUILLE
|
||||
• Actions, ETF, obligations, crypto, immobilier — toutes les classes d'actifs
|
||||
• Plusieurs comptes : courtier, retraite, épargne et plus encore
|
||||
• Approche par bilan mensuel : sans bruit quotidien, juste une perspective à long terme
|
||||
• Évolution de votre patrimoine sur des mois et des années
|
||||
|
||||
VISUALISEZ VOTRE RICHESSE
|
||||
• Graphique d'évolution : observez la croissance de votre portefeuille dans le temps
|
||||
• Graphique d'allocation : sachez exactement où se trouve votre argent
|
||||
• Analyse du drawdown : comprenez vos pires baisses et vos récupérations
|
||||
• Mode Calme : une vue sans distraction pour votre tranquillité d'esprit
|
||||
|
||||
FIXEZ ET SUIVEZ VOS OBJECTIFS
|
||||
• Créez des objectifs financiers avec des montants cibles et des échéances
|
||||
• Suivez votre progression avec des indicateurs visuels
|
||||
• Restez motivé grâce à une vision claire de votre parcours
|
||||
|
||||
TENEZ UN JOURNAL D'INVESTISSEMENT
|
||||
• Notez vos réflexions, décisions et leçons apprises
|
||||
• Construisez un registre personnel de votre état d'esprit d'investisseur
|
||||
• Réfléchissez aux décisions passées pour améliorer les futures
|
||||
|
||||
CONFIDENTIALITÉ PAR CONCEPTION
|
||||
• Aucun compte requis. Aucune connexion. Aucune inscription.
|
||||
• Aucune analyse, aucun suivi, aucune vente de données — jamais.
|
||||
• La synchronisation iCloud est facultative : vous la contrôlez, et elle est chiffrée de bout en bout.
|
||||
• Fonctionne à 100% hors ligne.
|
||||
|
||||
CONÇU POUR LES INVESTISSEURS PASSIFS
|
||||
Portfolio Journal est pensé pour les adeptes de Boglehead, les investisseurs en ETF et fonds indiciels, et tous ceux qui suivent une stratégie d'achat et de conservation. Ce n'est pas un outil de trading — c'est un compagnon à long terme.
|
||||
|
||||
Que vous gériez un portefeuille à trois fonds, que vous visiez l'indépendance financière (FIRE) ou que vous suiviez simplement l'évolution de votre patrimoine net, Portfolio Journal vous apporte de la clarté sans complexité.
|
||||
|
||||
EXPORTEZ VOS DONNÉES
|
||||
• Export CSV : emportez vos données partout, à tout moment.
|
||||
• Propriété totale de votre historique financier.
|
||||
|
||||
---
|
||||
|
||||
POUR QUI EST PORTFOLIO JOURNAL ?
|
||||
- Investisseurs passifs et à long terme qui n'ont pas besoin de mises à jour quotidiennes
|
||||
- Fans d'ETF et de fonds indiciels
|
||||
- Membres de la communauté Boglehead et FIRE
|
||||
- Toute personne qui suit son patrimoine net ou son parcours de création de richesse
|
||||
- Utilisateurs soucieux de leur vie privée qui ne veulent pas de leurs données financières sur les serveurs d'autrui
|
||||
|
||||
---
|
||||
|
||||
Téléchargez Portfolio Journal dès aujourd'hui et commencez à construire une vision claire, privée et à long terme de votre avenir financier.
|
||||
@@ -0,0 +1 @@
|
||||
investir,dividende,crypto,fonds,indice,retraite,épargne,argent,passif,FIRE,bourse,rendement,objectif
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app
|
||||
@@ -0,0 +1 @@
|
||||
Portfolio Journal: Patrimoine
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/privacy.html
|
||||
@@ -0,0 +1 @@
|
||||
Sans login broker, sans serveur. Mettez à jour votre portefeuille chaque mois, voyez votre patrimoine grandir avec de beaux graphiques et atteignez vos objectifs FIRE.
|
||||
@@ -0,0 +1,9 @@
|
||||
Nouveaux graphiques, mises à jour rapides et insights de portefeuille :
|
||||
|
||||
• Mise à jour rapide — ajoutez des instantanés à toutes vos sources depuis le Tableau de bord
|
||||
• Insights de portefeuille — observations intelligentes sur votre portefeuille (série, gains, jalons, prévisions)
|
||||
• 3 nouveaux graphiques Premium : Comparer les Sources, Simulateur Et si…, Période vs Période
|
||||
• Contribution mensuelle — définissez une contribution par défaut par source, applicable rétroactivement
|
||||
• Badge de série — consultez votre série de mois consécutifs de suivi sur le Tableau de bord
|
||||
• Notifications plus intelligentes — rappels de bilan sautant les mois déjà complétés ; alertes de jalons
|
||||
• Corrections de bugs et améliorations des performances
|
||||
@@ -0,0 +1 @@
|
||||
Actions, ETF & Investissement
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/support.html
|
||||
@@ -0,0 +1,55 @@
|
||||
Portfolio Journal é o aplicativo de acompanhamento de investimentos feito para investidores de longo prazo que valorizam simplicidade e privacidade. Sem login em corretoras. Sem servidor. Seus dados ficam no seu dispositivo — ou sincronizam de forma privada pelo iCloud se você preferir.
|
||||
|
||||
Abra o app uma vez por mês, insira o valor da sua carteira e deixe o Portfolio Journal fazer o resto.
|
||||
|
||||
---
|
||||
|
||||
ACOMPANHE TODA A SUA CARTEIRA
|
||||
• Ações, ETFs, renda fixa, cripto, imóveis — qualquer classe de ativo
|
||||
• Múltiplas contas: corretora, previdência, poupança e mais
|
||||
• Abordagem por check-in mensal: sem ruído diário, só perspectiva de longo prazo
|
||||
• Evolução do patrimônio ao longo de meses e anos
|
||||
|
||||
VISUALIZE SUA RIQUEZA
|
||||
• Gráfico de evolução: acompanhe o crescimento da sua carteira ao longo do tempo
|
||||
• Gráfico de alocação: saiba exatamente onde está o seu dinheiro
|
||||
• Análise de drawdown: entenda suas piores quedas e recuperações
|
||||
• Modo Calmo: uma visão sem distrações para sua tranquilidade
|
||||
|
||||
DEFINA E ACOMPANHE SEUS OBJETIVOS
|
||||
• Crie metas financeiras com valor-alvo e prazo
|
||||
• Acompanhe o progresso com indicadores visuais
|
||||
• Mantenha-se motivado com uma visão clara do seu caminho
|
||||
|
||||
MANTENHA UM DIÁRIO DE INVESTIMENTOS
|
||||
• Registre pensamentos, decisões e lições aprendidas
|
||||
• Construa um histórico pessoal da sua mentalidade como investidor
|
||||
• Reflita sobre decisões passadas para melhorar as futuras
|
||||
|
||||
PRIVACIDADE POR DESIGN
|
||||
• Sem conta, sem login, sem cadastro.
|
||||
• Sem análises, sem rastreamento, sem venda de dados — nunca.
|
||||
• A sincronização com iCloud é opcional: você controla, com criptografia de ponta a ponta.
|
||||
• Funciona 100% offline.
|
||||
|
||||
FEITO PARA INVESTIDORES PASSIVOS
|
||||
Portfolio Journal foi desenvolvido para seguidores da filosofia Boglehead, investidores em ETFs e fundos de índice, e todos que adotam uma estratégia de comprar e manter. Não é uma ferramenta de trading — é um companheiro de longo prazo.
|
||||
|
||||
Seja gerenciando uma carteira de três fundos, buscando a independência financeira (FIRE) ou simplesmente monitorando seu patrimônio líquido, Portfolio Journal oferece clareza sem complexidade.
|
||||
|
||||
EXPORTE SEUS DADOS
|
||||
• Exportação em CSV: leve seus dados para onde quiser, quando quiser.
|
||||
• Propriedade total do seu histórico financeiro.
|
||||
|
||||
---
|
||||
|
||||
PARA QUEM É O PORTFOLIO JOURNAL?
|
||||
- Investidores passivos e de longo prazo que não precisam de atualizações diárias
|
||||
- Entusiastas de ETFs e fundos de índice
|
||||
- Membros da comunidade Boglehead e FIRE
|
||||
- Quem acompanha seu patrimônio líquido ou jornada de construção de riqueza
|
||||
- Usuários preocupados com privacidade que não querem seus dados financeiros em servidores de terceiros
|
||||
|
||||
---
|
||||
|
||||
Baixe Portfolio Journal hoje e comece a construir uma visão clara, privada e de longo prazo do seu futuro financeiro.
|
||||
@@ -0,0 +1 @@
|
||||
investir,dividendos,cripto,fundo,índice,reforma,poupança,dinheiro,passivo,FIRE,bolsa,juros
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app
|
||||
@@ -0,0 +1 @@
|
||||
Portfolio Journal: Patrimônio
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/privacy.html
|
||||
@@ -0,0 +1 @@
|
||||
Sem login em corretoras, sem servidores. Atualize sua carteira todo mês, veja seu patrimônio crescer com gráficos claros e alcance suas metas FIRE. iCloud opcional.
|
||||
@@ -0,0 +1,9 @@
|
||||
Novos gráficos, atualizações rápidas e insights de portfólio:
|
||||
|
||||
• Atualização rápida — adicione snapshots de todas as fontes de uma vez pelo Dashboard
|
||||
• Insights de portfólio — observações inteligentes sobre seu portfólio (sequência, ganhos, marcos, previsões)
|
||||
• 3 novos gráficos Premium: Comparar Fontes, Simulador E se…, Período vs Período
|
||||
• Contribuição mensal — defina uma contribuição padrão por fonte, aplicável retroativamente
|
||||
• Sequência — veja sua sequência de meses consecutivos de acompanhamento no Dashboard
|
||||
• Notificações mais inteligentes — lembretes ignoram meses já concluídos; alertas ao atingir marcos
|
||||
• Correções de bugs e melhorias de desempenho
|
||||
@@ -0,0 +1 @@
|
||||
Ações, ETF e Investimentos
|
||||
@@ -0,0 +1 @@
|
||||
https://portfoliojournal.app/support.html
|
||||
@@ -1 +1 @@
|
||||
|
||||
PRODUCTIVITY
|
||||
|
||||
Reference in New Issue
Block a user