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:
@@ -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;
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user