Calm 2.0 Fase 4: TipKit para descubrimiento + aha post-onboarding (build 55)

TipKit (Tips.configure con frecuencia diaria):
- QuickUpdateTip: popover en el '+' del Home — descubre Quick Update
- ScreenshotOCRTip: inline en Quick Update — descubre el OCR por captura
- SiriTip: en el cierre del check-in guiado — descubre el intent de voz
- ChartShareTip: popover en el compartir de Charts
- Se invalidan al ejecutar la acción; máx 1-2 impresiones cada uno
- Strings ×7 idiomas

Onboarding:
- Al completar el onboarding sin datos, se abre Add Source directamente
  (notificación .openAddFirstSource) — el usuario nuevo no aterriza en un
  dashboard vacío

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
This commit is contained in:
alexandrev-tibco
2026-07-10 09:31:03 +02:00
parent 08094483ed
commit 4c0d2af564
16 changed files with 148 additions and 10 deletions
+10 -10
View File
@@ -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;
+5
View File
@@ -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: {
@@ -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())
@@ -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.";
@@ -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.";
@@ -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.";
@@ -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.";
@@ -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.";
@@ -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にそのまま共有できます。";
@@ -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.";
+37
View File
@@ -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)] }
}
@@ -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
@@ -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"))
}
@@ -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 }
}
}
}
@@ -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 {
@@ -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 {