diff --git a/PortfolioJournal.xcodeproj/project.pbxproj b/PortfolioJournal.xcodeproj/project.pbxproj index abb7cde..7510920 100644 --- a/PortfolioJournal.xcodeproj/project.pbxproj +++ b/PortfolioJournal.xcodeproj/project.pbxproj @@ -541,7 +541,7 @@ CODE_SIGN_ENTITLEMENTS = PortfolioJournal/PortfolioJournalDebug.entitlements; ENABLE_USER_SCRIPT_SANDBOXING = NO; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; ENABLE_PREVIEWS = YES; @@ -582,7 +582,7 @@ CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; PROVISIONING_PROFILE_SPECIFIER = "porfoliojournal"; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; ENABLE_PREVIEWS = YES; @@ -739,7 +739,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = PortfolioJournalWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; @@ -774,7 +774,7 @@ CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; PROVISIONING_PROFILE_SPECIFIER = "Portfolio Journalwidget"; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; @@ -805,7 +805,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -829,7 +829,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -852,7 +852,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -875,7 +875,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -899,7 +899,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = PortfolioJournalQuickUpdateExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = PortfolioJournalQuickUpdate/Info.plist; @@ -927,7 +927,7 @@ CODE_SIGN_ENTITLEMENTS = PortfolioJournalQuickUpdateExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 54; + CURRENT_PROJECT_VERSION = 55; DEVELOPMENT_TEAM = 2825Q76T7H; PROVISIONING_PROFILE_SPECIFIER = "PortfolioJournal QuickUpdate AppStore"; GENERATE_INFOPLIST_FILE = NO; diff --git a/PortfolioJournal/App/ContentView.swift b/PortfolioJournal/App/ContentView.swift index 44c1ed6..a193ec9 100644 --- a/PortfolioJournal/App/ContentView.swift +++ b/PortfolioJournal/App/ContentView.swift @@ -128,6 +128,11 @@ struct ContentView: View { // Fresh install: nothing is "new" for a user who just onboarded. if completed && lastSeenWhatsNewVersion.isEmpty { lastSeenWhatsNewVersion = currentVersion + // Straight to the aha moment: open Add Source instead of + // dropping the new user on an empty dashboard. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + NotificationCenter.default.post(name: .openAddFirstSource, object: nil) + } } } .sheet(isPresented: $showingWhatsNew, onDismiss: { diff --git a/PortfolioJournal/App/PortfolioJournalApp.swift b/PortfolioJournal/App/PortfolioJournalApp.swift index 69633aa..30f5150 100644 --- a/PortfolioJournal/App/PortfolioJournalApp.swift +++ b/PortfolioJournal/App/PortfolioJournalApp.swift @@ -1,5 +1,6 @@ import SwiftUI import CoreData +import TipKit /// 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 @@ -73,6 +74,9 @@ struct PortfolioJournalApp: App { // Clean up any duplicate objects from previous bugs before initializing stores CoreDataStack.shared.cleanupDuplicateObjects() + // Feature-discovery tips (Quick Update, OCR, Siri, chart sharing) + try? Tips.configure([.displayFrequency(.daily)]) + let iap = IAPService() _iapService = StateObject(wrappedValue: iap) _adMobService = StateObject(wrappedValue: AdMobService()) diff --git a/PortfolioJournal/Resources/de.lproj/Localizable.strings b/PortfolioJournal/Resources/de.lproj/Localizable.strings index e2ab451..1fff102 100644 --- a/PortfolioJournal/Resources/de.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/de.lproj/Localizable.strings @@ -511,3 +511,13 @@ "whats_new_141_icloud_title" = "Mehr Sync"; "whats_new_141_icloud_body" = "Monatliche Beiträge und Allokationsziele werden jetzt über iCloud synchronisiert."; "whats_new_title_format" = "Neu in %@"; + +// TipKit (1.5.0 Fase 4) +"tip_quick_update_title" = "Alles in Sekunden aktualisieren"; +"tip_quick_update_body" = "Quick Update zeigt alle Quellen auf einem Bildschirm — Werte aus der Zwischenablage einfügen, fertig."; +"tip_ocr_title" = "Oder teile einfach einen Screenshot"; +"tip_ocr_body" = "Aus deiner Bank-App: Screenshot → Teilen → Portfolio Journal. Der Saldo wird auf dem Gerät gelesen."; +"tip_siri_title" = "Probier es nächstes Mal per Sprache"; +"tip_siri_body" = "Sag Siri \"Erfasse den Saldo meines Kontos in Portfolio Journal\" — ohne die App zu öffnen."; +"tip_chart_share_title" = "Teile dieses Diagramm"; +"tip_chart_share_body" = "Erzeugt ein schönes Bild mit deinen Daten, bereit für X oder WhatsApp."; diff --git a/PortfolioJournal/Resources/en.lproj/Localizable.strings b/PortfolioJournal/Resources/en.lproj/Localizable.strings index 8eff90e..a62a541 100644 --- a/PortfolioJournal/Resources/en.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/en.lproj/Localizable.strings @@ -512,3 +512,13 @@ "whats_new_141_icloud_title" = "More in sync"; "whats_new_141_icloud_body" = "Monthly contributions and allocation targets now sync across your devices via iCloud."; "whats_new_title_format" = "What's New in %@"; + +// TipKit (1.5.0 Fase 4) +"tip_quick_update_title" = "Update everything in seconds"; +"tip_quick_update_body" = "Quick Update lists all your sources in one screen — paste values from your clipboard and go."; +"tip_ocr_title" = "Or just share a screenshot"; +"tip_ocr_body" = "From your bank app: screenshot → Share → Portfolio Journal. The balance is read on-device."; +"tip_siri_title" = "Next time, try your voice"; +"tip_siri_body" = "Say \"Log my account balance in Portfolio Journal\" to Siri — no need to open the app."; +"tip_chart_share_title" = "Share this chart"; +"tip_chart_share_body" = "Renders a beautiful image with your data, ready for X or WhatsApp."; diff --git a/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings b/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings index 0f6085e..8a8fd48 100644 --- a/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings @@ -463,3 +463,13 @@ "whats_new_141_icloud_title" = "Más sincronizado"; "whats_new_141_icloud_body" = "Las aportaciones mensuales y los objetivos de asignación ahora se sincronizan vía iCloud."; "whats_new_title_format" = "Novedades de la %@"; + +// TipKit (1.5.0 Fase 4) +"tip_quick_update_title" = "Actualiza todo en segundos"; +"tip_quick_update_body" = "Quick Update lista todas tus fuentes en una pantalla — pega valores del portapapeles y listo."; +"tip_ocr_title" = "O comparte un pantallazo"; +"tip_ocr_body" = "Desde tu app del banco: captura → Compartir → Portfolio Journal. El saldo se lee en el dispositivo."; +"tip_siri_title" = "La próxima vez, prueba con la voz"; +"tip_siri_body" = "Dile a Siri \"Registra el saldo de mi cuenta en Portfolio Journal\" — sin abrir la app."; +"tip_chart_share_title" = "Comparte esta gráfica"; +"tip_chart_share_body" = "Genera una imagen bonita con tus datos, lista para X o WhatsApp."; diff --git a/PortfolioJournal/Resources/fr.lproj/Localizable.strings b/PortfolioJournal/Resources/fr.lproj/Localizable.strings index c50d2d2..23edd14 100644 --- a/PortfolioJournal/Resources/fr.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/fr.lproj/Localizable.strings @@ -511,3 +511,13 @@ "whats_new_141_icloud_title" = "Plus synchronisé"; "whats_new_141_icloud_body" = "Les contributions mensuelles et les objectifs d'allocation se synchronisent désormais via iCloud."; "whats_new_title_format" = "Nouveautés de la %@"; + +// TipKit (1.5.0 Fase 4) +"tip_quick_update_title" = "Tout mettre à jour en quelques secondes"; +"tip_quick_update_body" = "Quick Update liste toutes vos sources sur un écran — collez les valeurs du presse-papiers et c'est fait."; +"tip_ocr_title" = "Ou partagez une capture d'écran"; +"tip_ocr_body" = "Depuis votre app bancaire : capture → Partager → Portfolio Journal. Le solde est lu sur l'appareil."; +"tip_siri_title" = "La prochaine fois, essayez la voix"; +"tip_siri_body" = "Dites à Siri « Enregistre le solde de mon compte dans Portfolio Journal » — sans ouvrir l'app."; +"tip_chart_share_title" = "Partagez ce graphique"; +"tip_chart_share_body" = "Génère une belle image avec vos données, prête pour X ou WhatsApp."; diff --git a/PortfolioJournal/Resources/it.lproj/Localizable.strings b/PortfolioJournal/Resources/it.lproj/Localizable.strings index 1f32007..74a5f50 100644 --- a/PortfolioJournal/Resources/it.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/it.lproj/Localizable.strings @@ -511,3 +511,13 @@ "whats_new_141_icloud_title" = "Più sincronizzato"; "whats_new_141_icloud_body" = "I contributi mensili e gli obiettivi di allocazione ora si sincronizzano via iCloud."; "whats_new_title_format" = "Novità della %@"; + +// TipKit (1.5.0 Fase 4) +"tip_quick_update_title" = "Aggiorna tutto in pochi secondi"; +"tip_quick_update_body" = "Quick Update elenca tutte le tue fonti in una schermata — incolla i valori dagli appunti e via."; +"tip_ocr_title" = "Oppure condividi uno screenshot"; +"tip_ocr_body" = "Dall'app della tua banca: screenshot → Condividi → Portfolio Journal. Il saldo viene letto sul dispositivo."; +"tip_siri_title" = "La prossima volta prova con la voce"; +"tip_siri_body" = "Di' a Siri \"Registra il saldo del mio conto in Portfolio Journal\" — senza aprire l'app."; +"tip_chart_share_title" = "Condividi questo grafico"; +"tip_chart_share_body" = "Genera una bella immagine con i tuoi dati, pronta per X o WhatsApp."; diff --git a/PortfolioJournal/Resources/ja.lproj/Localizable.strings b/PortfolioJournal/Resources/ja.lproj/Localizable.strings index 7b71a3f..ca1f221 100644 --- a/PortfolioJournal/Resources/ja.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/ja.lproj/Localizable.strings @@ -511,3 +511,13 @@ "whats_new_141_icloud_title" = "さらに同期"; "whats_new_141_icloud_body" = "毎月の積立額と配分目標がiCloudでデバイス間同期されるようになりました。"; "whats_new_title_format" = "バージョン%@の新機能"; + +// TipKit (1.5.0 Fase 4) +"tip_quick_update_title" = "数秒ですべて更新"; +"tip_quick_update_body" = "クイック更新なら全ソースを1画面で。クリップボードから値を貼り付けるだけ。"; +"tip_ocr_title" = "スクリーンショットの共有でもOK"; +"tip_ocr_body" = "銀行アプリでスクショ → 共有 → Portfolio Journal。残高はデバイス上で読み取られます。"; +"tip_siri_title" = "次は声で試してみて"; +"tip_siri_body" = "Siriに「Portfolio Journalで口座の残高を記録」と話しかけるだけ。アプリを開く必要はありません。"; +"tip_chart_share_title" = "このチャートを共有"; +"tip_chart_share_body" = "あなたのデータで美しい画像を生成。XやWhatsAppにそのまま共有できます。"; diff --git a/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings b/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings index 6b09798..08b1f54 100644 --- a/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings @@ -511,3 +511,13 @@ "whats_new_141_icloud_title" = "Mais sincronizado"; "whats_new_141_icloud_body" = "Aportes mensais e metas de alocação agora sincronizam via iCloud."; "whats_new_title_format" = "Novidades da %@"; + +// TipKit (1.5.0 Fase 4) +"tip_quick_update_title" = "Atualize tudo em segundos"; +"tip_quick_update_body" = "O Quick Update lista todas as suas fontes em uma tela — cole valores da área de transferência e pronto."; +"tip_ocr_title" = "Ou compartilhe uma captura de tela"; +"tip_ocr_body" = "Do app do seu banco: captura → Compartilhar → Portfolio Journal. O saldo é lido no aparelho."; +"tip_siri_title" = "Na próxima, experimente por voz"; +"tip_siri_body" = "Diga à Siri \"Registre o saldo da minha conta no Portfolio Journal\" — sem abrir o app."; +"tip_chart_share_title" = "Compartilhe este gráfico"; +"tip_chart_share_body" = "Gera uma bela imagem com seus dados, pronta para o X ou WhatsApp."; diff --git a/PortfolioJournal/Services/AppTips.swift b/PortfolioJournal/Services/AppTips.swift new file mode 100644 index 0000000..56b715d --- /dev/null +++ b/PortfolioJournal/Services/AppTips.swift @@ -0,0 +1,37 @@ +import SwiftUI +import TipKit + +// MARK: - Feature discovery (TipKit, Calm 2.0 Fase 4) +// +// The app's most differentiating features (quick update, screenshot OCR, Siri, +// chart sharing) were invisible unless you stumbled on them. One contextual tip +// each, shown at most once a day (see Tips.configure in PortfolioJournalApp), +// auto-invalidated after display or when the action is performed. + +struct QuickUpdateTip: Tip { + var title: Text { Text(String(localized: "tip_quick_update_title")) } + var message: Text? { Text(String(localized: "tip_quick_update_body")) } + var image: Image? { Image(systemName: "bolt.circle.fill") } + var options: [any TipOption] { [MaxDisplayCount(2)] } +} + +struct ScreenshotOCRTip: Tip { + var title: Text { Text(String(localized: "tip_ocr_title")) } + var message: Text? { Text(String(localized: "tip_ocr_body")) } + var image: Image? { Image(systemName: "text.viewfinder") } + var options: [any TipOption] { [MaxDisplayCount(2)] } +} + +struct SiriTip: Tip { + var title: Text { Text(String(localized: "tip_siri_title")) } + var message: Text? { Text(String(localized: "tip_siri_body")) } + var image: Image? { Image(systemName: "mic.fill") } + var options: [any TipOption] { [MaxDisplayCount(1)] } +} + +struct ChartShareTip: Tip { + var title: Text { Text(String(localized: "tip_chart_share_title")) } + var message: Text? { Text(String(localized: "tip_chart_share_body")) } + var image: Image? { Image(systemName: "square.and.arrow.up") } + var options: [any TipOption] { [MaxDisplayCount(2)] } +} diff --git a/PortfolioJournal/Services/NotificationService.swift b/PortfolioJournal/Services/NotificationService.swift index aa36e78..f4d34b4 100644 --- a/PortfolioJournal/Services/NotificationService.swift +++ b/PortfolioJournal/Services/NotificationService.swift @@ -192,6 +192,7 @@ extension Notification.Name { static let openBatchUpdate = Notification.Name("openBatchUpdate") static let openDashboard = Notification.Name("openDashboard") static let openQuickUpdate = Notification.Name("openQuickUpdate") + static let openAddFirstSource = Notification.Name("openAddFirstSource") } // MARK: - Re-engagement Notifications diff --git a/PortfolioJournal/Views/Charts/ChartsContainerView.swift b/PortfolioJournal/Views/Charts/ChartsContainerView.swift index c0a4b38..b8b33e5 100644 --- a/PortfolioJournal/Views/Charts/ChartsContainerView.swift +++ b/PortfolioJournal/Views/Charts/ChartsContainerView.swift @@ -1,5 +1,6 @@ import SwiftUI import Charts +import TipKit struct ChartsContainerView: View { @EnvironmentObject var accountStore: AccountStore @@ -466,10 +467,12 @@ struct ChartsContainerView: View { private var shareButton: some View { Button { + ChartShareTip().invalidate(reason: .actionPerformed) shareCurrentChart() } label: { Image(systemName: "square.and.arrow.up") } + .popoverTip(ChartShareTip()) .disabled(!viewModel.hasData || viewModel.isLoading || selectedChartLocked) .accessibilityLabel(String(localized: "chart_share_button")) } diff --git a/PortfolioJournal/Views/Dashboard/DashboardView.swift b/PortfolioJournal/Views/Dashboard/DashboardView.swift index 3312ff7..1bc6201 100644 --- a/PortfolioJournal/Views/Dashboard/DashboardView.swift +++ b/PortfolioJournal/Views/Dashboard/DashboardView.swift @@ -1,6 +1,7 @@ import SwiftUI import Charts import CoreData +import TipKit struct DashboardView: View { @EnvironmentObject var iapService: IAPService @@ -64,10 +65,12 @@ struct DashboardView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { + QuickUpdateTip().invalidate(reason: .actionPerformed) showingQuickUpdate = true } label: { Image(systemName: "plus.circle.fill") } + .popoverTip(QuickUpdateTip()) } ToolbarItem(placement: .navigationBarTrailing) { accountFilterMenu @@ -120,6 +123,9 @@ struct DashboardView: View { .onReceive(NotificationCenter.default.publisher(for: .openQuickUpdate)) { _ in showingQuickUpdate = true } + .onReceive(NotificationCenter.default.publisher(for: .openAddFirstSource)) { _ in + if !viewModel.hasData { showingAddSource = true } + } } } diff --git a/PortfolioJournal/Views/Dashboard/GuidedCheckInView.swift b/PortfolioJournal/Views/Dashboard/GuidedCheckInView.swift index d4a8a0e..1996c1b 100644 --- a/PortfolioJournal/Views/Dashboard/GuidedCheckInView.swift +++ b/PortfolioJournal/Views/Dashboard/GuidedCheckInView.swift @@ -1,5 +1,6 @@ import SwiftUI import CoreData +import TipKit import UIKit // MARK: - Guided monthly check-in (Calm 2.0, Fase 2) @@ -355,6 +356,9 @@ struct GuidedCheckInView: View { Spacer() + TipView(SiriTip()) + .padding(.horizontal, 32) + VStack(spacing: 10) { if let totals = savedTotals { Button { diff --git a/PortfolioJournal/Views/Dashboard/QuickUpdateView.swift b/PortfolioJournal/Views/Dashboard/QuickUpdateView.swift index 894b1e1..30e43c8 100644 --- a/PortfolioJournal/Views/Dashboard/QuickUpdateView.swift +++ b/PortfolioJournal/Views/Dashboard/QuickUpdateView.swift @@ -1,5 +1,6 @@ import SwiftUI import CoreData +import TipKit import UIKit struct QuickUpdateView: View { @@ -43,6 +44,13 @@ struct QuickUpdateView: View { ) } else { List { + Section { + TipView(ScreenshotOCRTip()) + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + } + .listSectionSeparator(.hidden) + if let amount = clipboardAmount, let target = nextEmptySource { Section { Button {