Release 1.3.0: onboarding UX, charts paywall banner, re-engagement notifications, Crashlytics

- Crashlytics: add FirebaseCrashlytics framework + dSYM upload build phase
- Onboarding: useSampleData off by default, Add First Investment as primary CTA
- AddSourceView: contextual placeholder and footer explaining what a source is
- Charts: CompactPaywallBanner visible to free users on every visit
- Notifications: re-engagement (7d) and monthly check-in local notifications
- Paywall: decorative chart preview replacing static crown icon
- Localization: all new strings in en + es-ES
This commit is contained in:
alexandrev-tibco
2026-05-01 09:26:37 +02:00
parent 10f6d0ca20
commit 94ed4d17eb
11 changed files with 340 additions and 131 deletions
@@ -6,7 +6,7 @@ struct OnboardingView: View {
@State private var currentPage = 0
@State private var selectedCurrency = Locale.current.currency?.identifier ?? "EUR"
@State private var useSampleData = true
@State private var useSampleData = false
@AppStorage("calmModeEnabled") private var calmModeEnabled = true
@AppStorage("cloudSyncEnabled") private var cloudSyncEnabled = false
@State private var showingImportSheet = false
@@ -221,109 +221,82 @@ struct OnboardingQuickStartView: View {
let onAddSource: () -> Void
var body: some View {
VStack(spacing: 24) {
Spacer()
ScrollView {
VStack(spacing: 28) {
Spacer().frame(height: 8)
VStack(spacing: 12) {
Image("BrandMark")
.resizable()
.scaledToFit()
.frame(width: 72, height: 72)
// Header
VStack(spacing: 10) {
Image("BrandMark")
.resizable()
.scaledToFit()
.frame(width: 64, height: 64)
Text("Quick Start")
.font(.title.weight(.bold))
Text(String(localized: "onboarding_quickstart_title"))
.font(.title.weight(.bold))
Text("Pick your currency and start with sample data or import your own.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 30)
}
VStack(spacing: 12) {
HStack {
Text("Currency")
Spacer()
Picker("Currency", selection: $selectedCurrency) {
ForEach(CurrencyPicker.commonCodes, id: \.self) { code in
Text(code).tag(code)
}
}
.pickerStyle(.menu)
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
Toggle("Load sample portfolio", isOn: $useSampleData)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
Toggle("Enable Calm Mode (recommended)", isOn: $calmModeEnabled)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
Toggle("Sync with iCloud (optional)", isOn: $cloudSyncEnabled)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
if cloudSyncEnabled {
Text("iCloud sync starts after you restart the app.")
.font(.caption)
Text(String(localized: "onboarding_quickstart_subtitle"))
.font(.subheadline)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
Button {
openAppSettings()
} label: {
Label("Open iCloud Settings", systemImage: "gear")
.frame(maxWidth: .infinity)
}
.padding()
.background(Color.appPrimary.opacity(0.1))
.cornerRadius(12)
.multilineTextAlignment(.center)
.padding(.horizontal, 30)
}
HStack(spacing: 12) {
Button {
onImport()
} label: {
Label("Import", systemImage: "square.and.arrow.down")
// Primary CTA Add first source
VStack(spacing: 10) {
Button(action: onAddSource) {
Label(String(localized: "onboarding_add_first_source"), systemImage: "plus.circle.fill")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.appPrimary)
.foregroundColor(.white)
.cornerRadius(AppConstants.UI.cornerRadius)
}
Button(action: onImport) {
Label(String(localized: "onboarding_import_data"), systemImage: "square.and.arrow.down")
.font(.subheadline)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(Color.appPrimary.opacity(0.1))
.foregroundColor(.appPrimary)
.cornerRadius(AppConstants.UI.cornerRadius)
}
}
// Secondary options
VStack(spacing: 10) {
HStack {
Text("Currency")
Spacer()
Picker("Currency", selection: $selectedCurrency) {
ForEach(CurrencyPicker.commonCodes, id: \.self) { code in
Text(code).tag(code)
}
}
.pickerStyle(.menu)
}
.padding()
.background(Color.appPrimary.opacity(0.1))
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
Button {
onAddSource()
} label: {
Label("Add Source", systemImage: "plus")
.frame(maxWidth: .infinity)
}
.padding()
.background(Color.appSecondary.opacity(0.1))
.cornerRadius(12)
Toggle("Enable Calm Mode (recommended)", isOn: $calmModeEnabled)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
Toggle("Sync with iCloud (optional)", isOn: $cloudSyncEnabled)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
}
Spacer().frame(height: 8)
}
.padding(.horizontal, 24)
Spacer()
Spacer()
}
}
private func openAppSettings() {
guard let url = Self.appSettingsURL() else { return }
UIApplication.shared.open(url)
}
static func appSettingsURL() -> URL? {
URL(string: UIApplication.openSettingsURLString)
}
}
// MARK: - First Launch Welcome