Release 1.4.0 (build 31): retención, quick update, streak, what's new
- 1A: Notificación mensual de resumen de portfolio (día 5 de cada mes) - 1B: Badge de racha mensual en Dashboard (streak counter) - 1C: Empty states mejorados en Goals y Journal con CTAs claros - 2A: Quick Update sheet — actualiza todas las fuentes desde una pantalla - 2B: Notificación batch update redirige al Quick Update sheet - 2C: Widget deep link portfoliojournal://quickupdate abre Quick Update - 3A: App Store version check banner en Settings - 3C: What's New sheet en primer launch de versión nueva - Localización completa en 7 idiomas para todas las nuevas strings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,10 @@ struct ImportDataView: View {
|
||||
@State private var newAccountName = ""
|
||||
@State private var accountErrorMessage: String?
|
||||
|
||||
// CSV column mapping flow
|
||||
@State private var pendingCSVContent: String?
|
||||
@State private var showingCSVMapping = false
|
||||
|
||||
private let accountRepository = AccountRepository()
|
||||
|
||||
private var availableAccounts: [Account] {
|
||||
@@ -127,6 +131,13 @@ struct ImportDataView: View {
|
||||
) { result in
|
||||
handleImport(result)
|
||||
}
|
||||
.sheet(isPresented: $showingCSVMapping) {
|
||||
if let content = pendingCSVContent {
|
||||
CSVMappingView(csvContent: content) { mapping in
|
||||
performMappedCSVImport(content: content, mapping: mapping)
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(
|
||||
"Import Complete",
|
||||
isPresented: Binding(
|
||||
@@ -366,12 +377,8 @@ Personal,Stocks,Index Fund,2024-01-01,15000,12000,Long-term
|
||||
handleImportContent(content)
|
||||
}
|
||||
|
||||
private func handleImportContent(_ content: String) {
|
||||
// For non-Premium users or when only one account exists, use the Default account
|
||||
// For Premium users with multiple accounts, respect their selection
|
||||
private func resolvedAccountName() -> String {
|
||||
let useAccountSelection = shouldShowAccountSelection && iapService.isPremium
|
||||
let allowMultipleAccounts = false // Always import into a single account
|
||||
|
||||
var defaultAccountName = accountStore.accounts.first(where: { $0.isDefaultAccount })?.name
|
||||
?? accountStore.selectedAccount?.name
|
||||
?? accountStore.accounts.first?.name
|
||||
@@ -379,23 +386,64 @@ Personal,Stocks,Index Fund,2024-01-01,15000,12000,Long-term
|
||||
|
||||
if useAccountSelection {
|
||||
if accountSelection == .new {
|
||||
accountErrorMessage = validateNewAccountName()
|
||||
guard accountErrorMessage == nil else { return }
|
||||
let trimmed = newAccountName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let currency = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency
|
||||
let account = accountRepository.createAccount(
|
||||
name: trimmed,
|
||||
currency: currency,
|
||||
inputMode: .simple,
|
||||
notificationFrequency: .monthly,
|
||||
customFrequencyMonths: 1
|
||||
)
|
||||
defaultAccountName = account.name
|
||||
if !trimmed.isEmpty {
|
||||
let currency = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency
|
||||
let account = accountRepository.createAccount(
|
||||
name: trimmed,
|
||||
currency: currency,
|
||||
inputMode: .simple,
|
||||
notificationFrequency: .monthly,
|
||||
customFrequencyMonths: 1
|
||||
)
|
||||
defaultAccountName = account.name
|
||||
}
|
||||
} else {
|
||||
defaultAccountName = selectedAccountName ?? defaultAccountName
|
||||
}
|
||||
}
|
||||
return defaultAccountName
|
||||
}
|
||||
|
||||
private func handleImportContent(_ content: String) {
|
||||
let useAccountSelection = shouldShowAccountSelection && iapService.isPremium
|
||||
|
||||
if useAccountSelection && accountSelection == .new {
|
||||
accountErrorMessage = validateNewAccountName()
|
||||
guard accountErrorMessage == nil else { return }
|
||||
}
|
||||
|
||||
// CSV → show column mapping sheet first
|
||||
if selectedFormat == .csv {
|
||||
pendingCSVContent = content
|
||||
showingCSVMapping = true
|
||||
return
|
||||
}
|
||||
|
||||
// JSON → import directly
|
||||
runImport(content: content, format: .json, defaultAccountName: resolvedAccountName())
|
||||
}
|
||||
|
||||
private func performMappedCSVImport(content: String, mapping: ImportService.CSVMappingConfig) {
|
||||
let defaultAccountName = resolvedAccountName()
|
||||
isImporting = true
|
||||
importProgress = 0
|
||||
importStatus = "Parsing file"
|
||||
|
||||
Task {
|
||||
let importResult = await ImportService.shared.importCSVWithMappingAsync(
|
||||
content: content,
|
||||
mapping: mapping,
|
||||
defaultAccountName: defaultAccountName
|
||||
) { progress in
|
||||
importProgress = progress.fraction
|
||||
importStatus = progress.message
|
||||
}
|
||||
finishImport(importResult)
|
||||
}
|
||||
}
|
||||
|
||||
private func runImport(content: String, format: ImportService.ImportFormat, defaultAccountName: String) {
|
||||
isImporting = true
|
||||
importProgress = 0
|
||||
importStatus = "Parsing file"
|
||||
@@ -403,27 +451,30 @@ Personal,Stocks,Index Fund,2024-01-01,15000,12000,Long-term
|
||||
Task {
|
||||
let importResult = await ImportService.shared.importDataAsync(
|
||||
content: content,
|
||||
format: selectedFormat,
|
||||
allowMultipleAccounts: allowMultipleAccounts,
|
||||
format: format,
|
||||
allowMultipleAccounts: false,
|
||||
defaultAccountName: defaultAccountName
|
||||
) { progress in
|
||||
importProgress = progress.fraction
|
||||
importStatus = progress.message
|
||||
}
|
||||
finishImport(importResult)
|
||||
}
|
||||
}
|
||||
|
||||
isImporting = false
|
||||
private func finishImport(_ importResult: ImportService.ImportResult) {
|
||||
isImporting = false
|
||||
|
||||
if importResult.errors.isEmpty {
|
||||
var message = "Imported \(importResult.sourcesCreated) sources and \(importResult.snapshotsCreated) snapshots."
|
||||
if importResult.snapshotsUpdated > 0 {
|
||||
message += " Updated \(importResult.snapshotsUpdated) existing snapshots."
|
||||
}
|
||||
resultMessage = message
|
||||
tabSelection.selectedTab = 0
|
||||
dismiss()
|
||||
} else {
|
||||
errorMessage = importResult.errors.joined(separator: "\n")
|
||||
if importResult.errors.isEmpty {
|
||||
var message = "Imported \(importResult.sourcesCreated) sources and \(importResult.snapshotsCreated) snapshots."
|
||||
if importResult.snapshotsUpdated > 0 {
|
||||
message += " Updated \(importResult.snapshotsUpdated) existing snapshots."
|
||||
}
|
||||
resultMessage = message
|
||||
tabSelection.selectedTab = 0
|
||||
dismiss()
|
||||
} else {
|
||||
errorMessage = importResult.errors.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ struct SettingsView: View {
|
||||
@AppStorage("lockOnBackground") private var lockOnBackground = false
|
||||
|
||||
@ObservedObject private var cloudStack = CoreDataStack.shared
|
||||
@ObservedObject private var updateService = AppUpdateService.shared
|
||||
|
||||
@State private var showingPinSetup = false
|
||||
@State private var showingPinChange = false
|
||||
@@ -39,6 +40,9 @@ struct SettingsView: View {
|
||||
AppBackground()
|
||||
|
||||
List {
|
||||
if updateService.updateAvailable {
|
||||
updateAvailableSection
|
||||
}
|
||||
brandSection
|
||||
// Premium Section
|
||||
premiumSection
|
||||
@@ -181,6 +185,33 @@ struct SettingsView: View {
|
||||
if viewModel.backupsEnabled {
|
||||
viewModel.refreshBackups()
|
||||
}
|
||||
updateService.checkForUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Update Available Section
|
||||
|
||||
private var updateAvailableSection: some View {
|
||||
Section {
|
||||
Link(destination: URL(string: "https://apps.apple.com/app/id6741412965")!) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.down.circle.fill")
|
||||
.foregroundColor(.appPrimary)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(String(localized: "update_available_title"))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
if let v = updateService.latestVersion {
|
||||
Text(String(format: String(localized: "update_available_body"), v))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,6 +491,12 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
CategoriesView()
|
||||
} label: {
|
||||
Label("Categories", systemImage: "tag")
|
||||
}
|
||||
|
||||
Button {
|
||||
if viewModel.canExport {
|
||||
viewModel.showingExportOptions = true
|
||||
|
||||
Reference in New Issue
Block a user