Update version
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var iapService: IAPService
|
||||
@EnvironmentObject var adMobService: AdMobService
|
||||
@EnvironmentObject var tabSelection: TabSelectionStore
|
||||
@EnvironmentObject var coreDataStack: CoreDataStack
|
||||
@AppStorage("onboardingCompleted") private var onboardingCompleted = false
|
||||
@AppStorage("faceIdEnabled") private var faceIdEnabled = false
|
||||
@AppStorage("pinEnabled") private var pinEnabled = false
|
||||
@@ -11,6 +13,7 @@ struct ContentView: View {
|
||||
@AppStorage("lockOnBackground") private var lockOnBackground = false
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@State private var isUnlocked = false
|
||||
@State private var resolvedOnboardingCompleted: Bool?
|
||||
|
||||
private var lockEnabled: Bool {
|
||||
faceIdEnabled || pinEnabled
|
||||
@@ -19,7 +22,9 @@ struct ContentView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Group {
|
||||
if !onboardingCompleted {
|
||||
if !isReadyForContent {
|
||||
AppLaunchLoadingView(messageKey: "loading_data")
|
||||
} else if resolvedOnboardingCompleted == false {
|
||||
OnboardingView(onboardingCompleted: $onboardingCompleted)
|
||||
} else {
|
||||
mainContent
|
||||
@@ -55,6 +60,62 @@ struct ContentView: View {
|
||||
isUnlocked = false
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if coreDataStack.isLoaded {
|
||||
syncOnboardingState()
|
||||
}
|
||||
}
|
||||
.onChange(of: coreDataStack.isLoaded) { _, loaded in
|
||||
if loaded {
|
||||
syncOnboardingState()
|
||||
}
|
||||
}
|
||||
.onChange(of: onboardingCompleted) { _, completed in
|
||||
resolvedOnboardingCompleted = completed
|
||||
}
|
||||
}
|
||||
|
||||
private var isReadyForContent: Bool {
|
||||
coreDataStack.isLoaded && resolvedOnboardingCompleted != nil
|
||||
}
|
||||
|
||||
private func syncOnboardingState() {
|
||||
let settings = AppSettings.getOrCreate(in: coreDataStack.viewContext)
|
||||
var resolved = settings.onboardingCompleted || onboardingCompleted
|
||||
if !resolved && hasExistingData() {
|
||||
resolved = true
|
||||
}
|
||||
if settings.onboardingCompleted != resolved {
|
||||
settings.onboardingCompleted = resolved
|
||||
CoreDataStack.shared.save()
|
||||
}
|
||||
if onboardingCompleted != resolved {
|
||||
onboardingCompleted = resolved
|
||||
}
|
||||
resolvedOnboardingCompleted = resolved
|
||||
}
|
||||
|
||||
private func hasExistingData() -> Bool {
|
||||
let context = coreDataStack.viewContext
|
||||
|
||||
let sourceRequest: NSFetchRequest<InvestmentSource> = InvestmentSource.fetchRequest()
|
||||
sourceRequest.fetchLimit = 1
|
||||
sourceRequest.resultType = .countResultType
|
||||
if (try? context.count(for: sourceRequest)) ?? 0 > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
let snapshotRequest: NSFetchRequest<Snapshot> = Snapshot.fetchRequest()
|
||||
snapshotRequest.fetchLimit = 1
|
||||
snapshotRequest.resultType = .countResultType
|
||||
if (try? context.count(for: snapshotRequest)) ?? 0 > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
let accountRequest: NSFetchRequest<Account> = Account.fetchRequest()
|
||||
accountRequest.fetchLimit = 1
|
||||
accountRequest.resultType = .countResultType
|
||||
return ((try? context.count(for: accountRequest)) ?? 0) > 0
|
||||
}
|
||||
|
||||
private var mainContent: some View {
|
||||
@@ -84,7 +145,7 @@ struct ContentView: View {
|
||||
}
|
||||
.tag(3)
|
||||
|
||||
SettingsView()
|
||||
SettingsView(iapService: iapService)
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gearshape.fill")
|
||||
}
|
||||
@@ -105,6 +166,7 @@ struct ContentView: View {
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
.environmentObject(CoreDataStack.shared)
|
||||
.environmentObject(IAPService())
|
||||
.environmentObject(AdMobService())
|
||||
.environmentObject(AccountStore(iapService: IAPService()))
|
||||
|
||||
@@ -22,6 +22,7 @@ struct PortfolioJournalApp: App {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environment(\.managedObjectContext, coreDataStack.viewContext)
|
||||
.environmentObject(coreDataStack)
|
||||
.environmentObject(iapService)
|
||||
.environmentObject(adMobService)
|
||||
.environmentObject(accountStore)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"identifier" : "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
|
||||
"nonRenewingSubscriptions" : [
|
||||
|
||||
],
|
||||
"products" : [
|
||||
{
|
||||
"displayPrice" : "4.69",
|
||||
"familyShareable" : true,
|
||||
"internalID" : "premium_001",
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Unlock all premium features forever",
|
||||
"displayName" : "Portfolio Journal Premium",
|
||||
"locale" : "en_US"
|
||||
},
|
||||
{
|
||||
"description" : "Desbloquea todas las funciones premium para siempre",
|
||||
"displayName" : "Portfolio Journal Premium",
|
||||
"locale" : "es_ES"
|
||||
}
|
||||
],
|
||||
"productID" : "com.portfoliojournal.premium",
|
||||
"referenceName" : "Premium Lifetime",
|
||||
"type" : "NonConsumable"
|
||||
}
|
||||
],
|
||||
"settings" : {
|
||||
"_applicationInternalID" : "1234567890",
|
||||
"_developerTeamID" : "XXXXXXXXXX",
|
||||
"_failTransactionsEnabled" : false,
|
||||
"_lastSynchronizedDate" : 758307388.12345,
|
||||
"_locale" : "en_US",
|
||||
"_storefront" : "USA",
|
||||
"_storeKitErrors" : [
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Load Products"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Purchase"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Verification"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "App Store Sync"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Subscription Status"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "App Transaction"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Manage Subscriptions Sheet"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Refund Request Sheet"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Offer Code Redeem Sheet"
|
||||
}
|
||||
]
|
||||
},
|
||||
"subscriptionGroups" : [
|
||||
|
||||
],
|
||||
"version" : {
|
||||
"major" : 3,
|
||||
"minor" : 0
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,8 @@ class CoreDataStack: ObservableObject {
|
||||
return appGroupURL
|
||||
}
|
||||
|
||||
@Published private(set) var isLoaded = false
|
||||
|
||||
lazy var persistentContainer: NSPersistentContainer = {
|
||||
let container: NSPersistentContainer
|
||||
if Self.cloudKitEnabled {
|
||||
@@ -139,13 +141,16 @@ class CoreDataStack: ObservableObject {
|
||||
|
||||
container.persistentStoreDescriptions = [description]
|
||||
|
||||
container.loadPersistentStores { description, error in
|
||||
container.loadPersistentStores { [weak self] description, error in
|
||||
if let error = error as NSError? {
|
||||
// In production, handle this error appropriately
|
||||
print("Core Data failed to load: \(error), \(error.userInfo)")
|
||||
} else {
|
||||
print("Core Data loaded successfully at: \(description.url?.path ?? "unknown")")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self?.isLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
// Merge policy - remote changes win
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<string>production</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.alexandrevazquez.portfoliojournal</string>
|
||||
|
||||
@@ -39,10 +39,15 @@ class CategoryRepository: ObservableObject {
|
||||
]
|
||||
|
||||
do {
|
||||
self.categories = try self.context.fetch(request)
|
||||
let fetched = try self.context.fetch(request)
|
||||
DispatchQueue.main.async {
|
||||
self.categories = fetched
|
||||
}
|
||||
} catch {
|
||||
print("Failed to fetch categories: \(error)")
|
||||
self.categories = []
|
||||
DispatchQueue.main.async {
|
||||
self.categories = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,15 @@ class GoalRepository: ObservableObject {
|
||||
]
|
||||
|
||||
do {
|
||||
self.goals = try self.context.fetch(request)
|
||||
let fetched = try self.context.fetch(request)
|
||||
DispatchQueue.main.async {
|
||||
self.goals = fetched
|
||||
}
|
||||
} catch {
|
||||
print("Failed to fetch goals: \(error)")
|
||||
self.goals = []
|
||||
DispatchQueue.main.async {
|
||||
self.goals = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,15 @@ class InvestmentSourceRepository: ObservableObject {
|
||||
]
|
||||
|
||||
do {
|
||||
self.sources = try self.context.fetch(request)
|
||||
let fetched = try self.context.fetch(request)
|
||||
DispatchQueue.main.async {
|
||||
self.sources = fetched
|
||||
}
|
||||
} catch {
|
||||
print("Failed to fetch sources: \(error)")
|
||||
self.sources = []
|
||||
DispatchQueue.main.async {
|
||||
self.sources = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<key>PLIST_VERSION</key>
|
||||
<string>1</string>
|
||||
<key>BUNDLE_ID</key>
|
||||
<string>com.alexandrevazquez.portfoliojournal</string>
|
||||
<string>com.alexandrevazquez.PortfolioJournal</string>
|
||||
<key>PROJECT_ID</key>
|
||||
<string>portfoliojournal-ef2d7</string>
|
||||
<key>STORAGE_BUCKET</key>
|
||||
@@ -25,6 +25,6 @@
|
||||
<key>IS_SIGNIN_ENABLED</key>
|
||||
<true></true>
|
||||
<key>GOOGLE_APP_ID</key>
|
||||
<string>1:334225114072:ios:81bad412ffe1c6df3d28ad</string>
|
||||
<string>1:334225114072:ios:a0a50b5835ac042e3d28ad</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -35,6 +35,269 @@
|
||||
<string>ca-app-pub-1549720748100858~9632507420</string>
|
||||
<key>GADDelayAppMeasurementInit</key>
|
||||
<true/>
|
||||
<key>SKAdNetworkItems</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>cstr6suwn9.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>4fzdc2evr5.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>4pfyvq9l8r.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>2fnua5tdw4.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>ydx93a7ass.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>5a6flpkh64.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>p78axxw29g.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>v72qych5uu.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>ludvb6z3bs.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>cp8zw746q7.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>3sh42y64q3.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>c6k4g5qg8m.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>s39g8k73mm.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>3qy4746246.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>f38h382jlk.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>hs6bdukanm.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>v4nxqhlyqp.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>wzmmz9fp6w.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>yclnxrl5pm.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>t38b2kh725.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>7ug5zh24hu.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>gta9lk7p23.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>vutu7akeur.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>y5ghdn5j9k.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>n6fk4nfna4.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>v9wttpbfk9.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>n38lu8286q.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>47vhws6wlr.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>kbd757ber7.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>9t245vhmpl.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>eh6m2bh4zr.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>a2p9lx4jpn.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>22mmun2rn5.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>4468km3ulz.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>2u9pt9hc89.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>8s468mfl3y.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>klf5c3l5u5.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>ppxm28t8ap.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>ecpz2srf59.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>uw77j35x4d.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>pwa73g5rt2.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>mlmmfzh3r3.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>578prtvx9j.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>4dzt52r2t5.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>e5fvkxwrpn.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>8c4e2ghe7u.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>zq492l623r.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>3rd42ekr43.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>3qcr597p9d.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>su67r6k2v3.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>kbd757ywx3.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>44n7hlldy6.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>k674qkevps.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>2rq3zucswp.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>5tjdwbrq8w.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>97r2b46745.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>b9bk5wbcq9.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>44jx6755aq.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>kbmxgpxpgc.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>tl55sbb4fm.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>9g2aggbj52.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>g2y4y55b64.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>c3frkrj4fj.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>r26jy69rpl.skadnetwork</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>SKAdNetworkIdentifier</key>
|
||||
<string>275upjj5gd.skadnetwork</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"error" = "Error";
|
||||
"success" = "Success";
|
||||
"loading" = "Loading...";
|
||||
"loading_data" = "Loading your data...";
|
||||
|
||||
// MARK: - Tab Bar
|
||||
"tab_dashboard" = "Home";
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"Open Full Note" = "Ver nota completa";
|
||||
"Duplicate Previous" = "Duplicar anterior";
|
||||
"Save" = "Guardar";
|
||||
"loading" = "Cargando...";
|
||||
"loading_data" = "Cargando tus datos...";
|
||||
|
||||
// MARK: - Monthly Check-in
|
||||
"Monthly Check-in" = "Chequeo mensual";
|
||||
|
||||
@@ -73,7 +73,11 @@ class ImportService {
|
||||
)
|
||||
return applyImport(parsed, context: CoreDataStack.shared.viewContext)
|
||||
case .json:
|
||||
let parsed = parseJSON(content, allowMultipleAccounts: allowMultipleAccounts)
|
||||
let parsed = parseJSON(
|
||||
content,
|
||||
allowMultipleAccounts: allowMultipleAccounts,
|
||||
defaultAccountName: defaultAccountName
|
||||
)
|
||||
return applyImport(parsed, context: CoreDataStack.shared.viewContext)
|
||||
}
|
||||
}
|
||||
@@ -96,7 +100,11 @@ class ImportService {
|
||||
defaultAccountName: defaultAccountName
|
||||
)
|
||||
case .json:
|
||||
parsed = self.parseJSON(content, allowMultipleAccounts: allowMultipleAccounts)
|
||||
parsed = self.parseJSON(
|
||||
content,
|
||||
allowMultipleAccounts: allowMultipleAccounts,
|
||||
defaultAccountName: defaultAccountName
|
||||
)
|
||||
}
|
||||
|
||||
let totalSnapshots = parsed.reduce(0) { total, account in
|
||||
@@ -189,7 +197,7 @@ Personal,Real Estate,Rental Property,2024-01-01,82000,80000,Estimated value
|
||||
let normalizedAccount = (providedAccount ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let rawAccountName = normalizedAccount.isEmpty ? fallbackAccount : normalizedAccount
|
||||
let accountName = allowMultipleAccounts ? rawAccountName : "Personal"
|
||||
let accountName = allowMultipleAccounts ? rawAccountName : fallbackAccount
|
||||
|
||||
guard let categoryName = indexOfCategory.flatMap({ row.safeValue(at: $0) }), !categoryName.isEmpty,
|
||||
let sourceName = indexOfSource.flatMap({ row.safeValue(at: $0) }), !sourceName.isEmpty,
|
||||
@@ -237,7 +245,11 @@ Personal,Real Estate,Rental Property,2024-01-01,82000,80000,Estimated value
|
||||
}
|
||||
}
|
||||
|
||||
private func parseJSON(_ content: String, allowMultipleAccounts: Bool) -> [ImportedAccount] {
|
||||
private func parseJSON(
|
||||
_ content: String,
|
||||
allowMultipleAccounts: Bool,
|
||||
defaultAccountName: String?
|
||||
) -> [ImportedAccount] {
|
||||
guard let data = content.data(using: .utf8),
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
return []
|
||||
@@ -246,7 +258,8 @@ Personal,Real Estate,Rental Property,2024-01-01,82000,80000,Estimated value
|
||||
if let accountsArray = json["accounts"] as? [[String: Any]] {
|
||||
return accountsArray.compactMap { accountDict in
|
||||
let rawName = accountDict["name"] as? String ?? "Personal"
|
||||
let name = allowMultipleAccounts ? rawName : "Personal"
|
||||
let fallbackName = defaultAccountName ?? "Personal"
|
||||
let name = allowMultipleAccounts ? rawName : fallbackName
|
||||
|
||||
let currency = accountDict["currency"] as? String
|
||||
let inputMode = InputMode(rawValue: accountDict["inputMode"] as? String ?? "") ?? .simple
|
||||
@@ -341,9 +354,10 @@ Personal,Real Estate,Rental Property,2024-01-01,82000,80000,Estimated value
|
||||
)
|
||||
}
|
||||
|
||||
let fallbackName = allowMultipleAccounts ? "Personal" : (defaultAccountName ?? "Personal")
|
||||
return [
|
||||
ImportedAccount(
|
||||
name: "Personal",
|
||||
name: fallbackName,
|
||||
currency: json["currency"] as? String,
|
||||
inputMode: .simple,
|
||||
notificationFrequency: .monthly,
|
||||
|
||||
@@ -16,7 +16,6 @@ enum AppConstants {
|
||||
// MARK: - StoreKit
|
||||
|
||||
static let premiumProductID = "com.portfoliojournal.premium"
|
||||
static let premiumPrice = "€4.69"
|
||||
|
||||
// MARK: - AdMob
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ class SourceDetailViewModel: ObservableObject {
|
||||
private let calculationService: CalculationService
|
||||
private let predictionEngine: PredictionEngine
|
||||
private let freemiumValidator: FreemiumValidator
|
||||
private let iapService: IAPService
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var isRefreshing = false
|
||||
private var refreshQueued = false
|
||||
@@ -54,6 +55,7 @@ class SourceDetailViewModel: ObservableObject {
|
||||
self.calculationService = calculationService ?? .shared
|
||||
self.predictionEngine = predictionEngine ?? .shared
|
||||
self.freemiumValidator = FreemiumValidator(iapService: iapService)
|
||||
self.iapService = iapService
|
||||
|
||||
loadData()
|
||||
setupObservers()
|
||||
@@ -70,6 +72,14 @@ class SourceDetailViewModel: ObservableObject {
|
||||
self.refreshData()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
iapService.$isPremium
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.refreshData()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Data Loading
|
||||
|
||||
@@ -10,6 +10,7 @@ struct AccountEditorView: View {
|
||||
@State private var inputMode: InputMode = .simple
|
||||
@State private var notificationFrequency: NotificationFrequency = .monthly
|
||||
@State private var customFrequencyMonths = 1
|
||||
@State private var errorMessage: String?
|
||||
|
||||
private let accountRepository = AccountRepository()
|
||||
|
||||
@@ -25,6 +26,11 @@ struct AccountEditorView: View {
|
||||
}
|
||||
} header: {
|
||||
Text("Account")
|
||||
} footer: {
|
||||
if let errorMessage {
|
||||
Text(errorMessage)
|
||||
.foregroundColor(.negativeRed)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -39,25 +45,6 @@ struct AccountEditorView: View {
|
||||
Text(inputMode.description)
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Reminder Frequency", selection: $notificationFrequency) {
|
||||
ForEach(NotificationFrequency.allCases) { frequency in
|
||||
Text(frequency.displayName).tag(frequency)
|
||||
}
|
||||
}
|
||||
|
||||
if notificationFrequency == .custom {
|
||||
Stepper(
|
||||
"Every \(customFrequencyMonths) month\(customFrequencyMonths > 1 ? "s" : "")",
|
||||
value: $customFrequencyMonths,
|
||||
in: 1...24
|
||||
)
|
||||
}
|
||||
} header: {
|
||||
Text("Account Reminders")
|
||||
} footer: {
|
||||
Text("Reminders apply to the whole account.")
|
||||
}
|
||||
}
|
||||
.navigationTitle(account == nil ? "New Account" : "Edit Account")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -73,12 +60,35 @@ struct AccountEditorView: View {
|
||||
.onAppear {
|
||||
loadAccount()
|
||||
}
|
||||
.onChange(of: name) { _, _ in
|
||||
validateName()
|
||||
}
|
||||
}
|
||||
.presentationDetents([.large])
|
||||
}
|
||||
|
||||
private var isValid: Bool {
|
||||
!name.trimmingCharacters(in: .whitespaces).isEmpty
|
||||
let trimmed = name.trimmingCharacters(in: .whitespaces)
|
||||
return !trimmed.isEmpty && !isDuplicateName(trimmed)
|
||||
}
|
||||
|
||||
private func validateName() {
|
||||
let trimmed = name.trimmingCharacters(in: .whitespaces)
|
||||
if trimmed.isEmpty {
|
||||
errorMessage = nil
|
||||
return
|
||||
}
|
||||
errorMessage = isDuplicateName(trimmed) ? "An account with this name already exists." : nil
|
||||
}
|
||||
|
||||
private func isDuplicateName(_ trimmed: String) -> Bool {
|
||||
let normalized = trimmed.lowercased()
|
||||
return accountRepository.accounts.contains { existing in
|
||||
if let account, existing.id == account.id {
|
||||
return false
|
||||
}
|
||||
return (existing.name).trimmingCharacters(in: .whitespaces).lowercased() == normalized
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAccount() {
|
||||
@@ -86,27 +96,33 @@ struct AccountEditorView: View {
|
||||
name = account.name
|
||||
currencyCode = account.currencyCode ?? currencyCode
|
||||
inputMode = InputMode(rawValue: account.inputMode) ?? .simple
|
||||
notificationFrequency = account.frequency
|
||||
customFrequencyMonths = Int(account.customFrequencyMonths)
|
||||
notificationFrequency = .monthly
|
||||
customFrequencyMonths = 1
|
||||
validateName()
|
||||
}
|
||||
|
||||
private func saveAccount() {
|
||||
validateName()
|
||||
guard errorMessage == nil else { return }
|
||||
let trimmedName = name.trimmingCharacters(in: .whitespaces)
|
||||
let enforcedFrequency: NotificationFrequency = .monthly
|
||||
let enforcedCustomMonths = 1
|
||||
if let account {
|
||||
accountRepository.updateAccount(
|
||||
account,
|
||||
name: name.trimmingCharacters(in: .whitespaces),
|
||||
name: trimmedName,
|
||||
currency: currencyCode,
|
||||
inputMode: inputMode,
|
||||
notificationFrequency: notificationFrequency,
|
||||
customFrequencyMonths: customFrequencyMonths
|
||||
notificationFrequency: enforcedFrequency,
|
||||
customFrequencyMonths: enforcedCustomMonths
|
||||
)
|
||||
} else {
|
||||
_ = accountRepository.createAccount(
|
||||
name: name.trimmingCharacters(in: .whitespaces),
|
||||
name: trimmedName,
|
||||
currency: currencyCode,
|
||||
inputMode: inputMode,
|
||||
notificationFrequency: notificationFrequency,
|
||||
customFrequencyMonths: customFrequencyMonths
|
||||
notificationFrequency: enforcedFrequency,
|
||||
customFrequencyMonths: enforcedCustomMonths
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ struct AccountsView: View {
|
||||
@State private var showingAddAccount = false
|
||||
@State private var selectedAccount: Account?
|
||||
@State private var showingPaywall = false
|
||||
@State private var accountToDelete: Account?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -33,6 +34,13 @@ struct AccountsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .trailing) {
|
||||
Button(role: .destructive) {
|
||||
accountToDelete = account
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Accounts")
|
||||
@@ -43,6 +51,25 @@ struct AccountsView: View {
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
.navigationTitle("Accounts")
|
||||
.confirmationDialog(
|
||||
"Delete Account",
|
||||
isPresented: Binding(
|
||||
get: { accountToDelete != nil },
|
||||
set: { if !$0 { accountToDelete = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Delete", role: .destructive) {
|
||||
guard let accountToDelete else { return }
|
||||
if accountStore.selectedAccount?.id == accountToDelete.id {
|
||||
accountStore.selectAllAccounts()
|
||||
}
|
||||
accountRepository.deleteAccount(accountToDelete)
|
||||
self.accountToDelete = nil
|
||||
}
|
||||
} message: {
|
||||
Text("This will remove the account and unlink its sources.")
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
|
||||
@@ -17,6 +17,35 @@ struct LoadingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct AppLaunchLoadingView: View {
|
||||
var messageKey: LocalizedStringKey = "loading"
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Spacer()
|
||||
|
||||
Image("BrandMark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 140, height: 140)
|
||||
.padding(16)
|
||||
.background(Color.appPrimary.opacity(0.08))
|
||||
.cornerRadius(28)
|
||||
|
||||
ProgressView()
|
||||
.scaleEffect(1.2)
|
||||
|
||||
Text(messageKey)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(AppBackground())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Skeleton Loading
|
||||
|
||||
struct SkeletonView: View {
|
||||
|
||||
@@ -43,7 +43,7 @@ struct DashboardView: View {
|
||||
viewModel.refreshData()
|
||||
}
|
||||
.overlay {
|
||||
if viewModel.isLoading {
|
||||
if viewModel.isLoading && !viewModel.hasData {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ struct OnboardingView: View {
|
||||
selectedCurrency = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency
|
||||
}
|
||||
.sheet(isPresented: $showingImportSheet) {
|
||||
ImportDataView()
|
||||
ImportDataView(importContext: .onboarding)
|
||||
}
|
||||
.sheet(isPresented: $showingAddSource) {
|
||||
AddSourceView()
|
||||
|
||||
@@ -120,15 +120,9 @@ struct PaywallView: View {
|
||||
|
||||
private var priceCard: some View {
|
||||
VStack(spacing: 8) {
|
||||
HStack(alignment: .top, spacing: 4) {
|
||||
Text("€")
|
||||
.font(.title2.weight(.semibold))
|
||||
.foregroundColor(.appPrimary)
|
||||
|
||||
Text("4.69")
|
||||
.font(.system(size: 48, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.appPrimary)
|
||||
}
|
||||
Text(iapService.formattedPrice)
|
||||
.font(.system(size: 48, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.appPrimary)
|
||||
|
||||
Text("One-time purchase")
|
||||
.font(.subheadline)
|
||||
@@ -283,6 +277,7 @@ struct FeatureRow: View {
|
||||
// MARK: - Compact Paywall (for inline use)
|
||||
|
||||
struct CompactPaywallBanner: View {
|
||||
@EnvironmentObject var iapService: IAPService
|
||||
@Binding var showingPaywall: Bool
|
||||
|
||||
var body: some View {
|
||||
@@ -311,7 +306,7 @@ struct CompactPaywallBanner: View {
|
||||
Button {
|
||||
showingPaywall = true
|
||||
} label: {
|
||||
Text("€4.69")
|
||||
Text(iapService.formattedPrice)
|
||||
.font(.caption.weight(.semibold))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
|
||||
@@ -3,6 +3,20 @@ import UniformTypeIdentifiers
|
||||
import UIKit
|
||||
|
||||
struct ImportDataView: View {
|
||||
enum ImportContext {
|
||||
case settings
|
||||
case onboarding
|
||||
}
|
||||
|
||||
enum AccountSelection: String, CaseIterable, Identifiable {
|
||||
case existing
|
||||
case new
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
||||
let importContext: ImportContext
|
||||
|
||||
@EnvironmentObject private var iapService: IAPService
|
||||
@EnvironmentObject private var accountStore: AccountStore
|
||||
@EnvironmentObject private var tabSelection: TabSelectionStore
|
||||
@@ -15,10 +29,24 @@ struct ImportDataView: View {
|
||||
@State private var isImporting = false
|
||||
@State private var importProgress: Double = 0
|
||||
@State private var importStatus = "Preparing import"
|
||||
@State private var accountSelection: AccountSelection = .existing
|
||||
@State private var selectedAccountId: UUID?
|
||||
@State private var newAccountName = ""
|
||||
@State private var accountErrorMessage: String?
|
||||
|
||||
private let accountRepository = AccountRepository()
|
||||
|
||||
init(importContext: ImportContext = .settings) {
|
||||
self.importContext = importContext
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
if shouldShowAccountSelection {
|
||||
accountSection
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Format", selection: $selectedFormat) {
|
||||
Text("CSV").tag(ImportService.ImportFormat.csv)
|
||||
@@ -65,7 +93,7 @@ struct ImportDataView: View {
|
||||
} header: {
|
||||
Text("Format Guide")
|
||||
} footer: {
|
||||
Text(iapService.isPremium ? "Accounts are imported as provided." : "Free users import into the Personal account.")
|
||||
Text(importFooterText)
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -117,6 +145,18 @@ struct ImportDataView: View {
|
||||
} message: {
|
||||
Text(errorMessage ?? "")
|
||||
}
|
||||
.onAppear {
|
||||
if selectedAccountId == nil {
|
||||
selectedAccountId = accountStore.selectedAccount?.id ?? accountStore.accounts.first?.id
|
||||
}
|
||||
}
|
||||
.onChange(of: accountSelection) { _, _ in
|
||||
accountErrorMessage = nil
|
||||
}
|
||||
.onChange(of: newAccountName) { _, _ in
|
||||
guard accountSelection == .new else { return }
|
||||
accountErrorMessage = validateNewAccountName()
|
||||
}
|
||||
}
|
||||
.presentationDetents([.large])
|
||||
}
|
||||
@@ -178,6 +218,78 @@ Personal,Stocks,Index Fund,2024-01-01,15000,12000,Long-term
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldShowAccountSelection: Bool {
|
||||
importContext == .onboarding
|
||||
}
|
||||
|
||||
private var importFooterText: String {
|
||||
if importContext == .onboarding {
|
||||
return iapService.isPremium
|
||||
? "Import will be added to the selected account."
|
||||
: "Free users import into the existing account."
|
||||
}
|
||||
return iapService.isPremium
|
||||
? "Accounts are imported as provided."
|
||||
: "Free users import into the selected account."
|
||||
}
|
||||
|
||||
private var accountSection: some View {
|
||||
Section {
|
||||
if iapService.isPremium {
|
||||
Picker("Account", selection: $accountSelection) {
|
||||
Text("Existing").tag(AccountSelection.existing)
|
||||
Text("New").tag(AccountSelection.new)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.disabled(isImporting)
|
||||
|
||||
if accountSelection == .existing {
|
||||
Picker("Import into", selection: $selectedAccountId) {
|
||||
ForEach(accountStore.accounts) { account in
|
||||
Text(account.name).tag(Optional(account.id))
|
||||
}
|
||||
}
|
||||
.disabled(isImporting)
|
||||
} else {
|
||||
TextField("New account name", text: $newAccountName)
|
||||
.disabled(isImporting)
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
Text("Account")
|
||||
Spacer()
|
||||
Text(selectedAccountName ?? "Personal")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Account")
|
||||
} footer: {
|
||||
if let accountErrorMessage {
|
||||
Text(accountErrorMessage)
|
||||
.foregroundColor(.negativeRed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var selectedAccountName: String? {
|
||||
accountStore.accounts.first { $0.id == selectedAccountId }?.name
|
||||
?? accountStore.selectedAccount?.name
|
||||
?? accountStore.accounts.first?.name
|
||||
}
|
||||
|
||||
private func validateNewAccountName() -> String? {
|
||||
let trimmed = newAccountName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else {
|
||||
return "Enter a name for the new account."
|
||||
}
|
||||
let normalized = trimmed.lowercased()
|
||||
let exists = accountStore.accounts.contains {
|
||||
$0.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized
|
||||
}
|
||||
return exists ? "An account with this name already exists." : nil
|
||||
}
|
||||
|
||||
private func handleImport(_ result: Result<[URL], Error>) {
|
||||
do {
|
||||
let urls = try result.get()
|
||||
@@ -243,9 +355,30 @@ Personal,Stocks,Index Fund,2024-01-01,15000,12000,Long-term
|
||||
}
|
||||
|
||||
private func handleImportContent(_ content: String) {
|
||||
let allowMultipleAccounts = iapService.isPremium
|
||||
let defaultAccountName = accountStore.selectedAccount?.name
|
||||
let shouldForceSingleAccount = importContext == .onboarding
|
||||
let allowMultipleAccounts = iapService.isPremium && !shouldForceSingleAccount
|
||||
var defaultAccountName = accountStore.selectedAccount?.name
|
||||
?? accountStore.accounts.first?.name
|
||||
|
||||
if shouldForceSingleAccount && iapService.isPremium {
|
||||
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
|
||||
} else {
|
||||
defaultAccountName = selectedAccountName
|
||||
}
|
||||
}
|
||||
|
||||
isImporting = true
|
||||
importProgress = 0
|
||||
importStatus = "Parsing file"
|
||||
|
||||
@@ -2,7 +2,7 @@ import SwiftUI
|
||||
import StoreKit
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var iapService: IAPService
|
||||
private let iapService: IAPService
|
||||
@StateObject private var viewModel: SettingsViewModel
|
||||
@AppStorage("calmModeEnabled") private var calmModeEnabled = true
|
||||
@AppStorage("cloudSyncEnabled") private var cloudSyncEnabled = false
|
||||
@@ -19,8 +19,9 @@ struct SettingsView: View {
|
||||
@State private var showingRestartAlert = false
|
||||
@State private var didLoadCloudSync = false
|
||||
|
||||
init() {
|
||||
_viewModel = StateObject(wrappedValue: SettingsViewModel(iapService: IAPService()))
|
||||
init(iapService: IAPService) {
|
||||
self.iapService = iapService
|
||||
_viewModel = StateObject(wrappedValue: SettingsViewModel(iapService: iapService))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -221,7 +222,7 @@ struct SettingsView: View {
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("Unlock all features for €4.69")
|
||||
Text("Unlock all features for \(iapService.formattedPrice)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@@ -705,7 +706,6 @@ struct PinSetupView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
.environmentObject(IAPService())
|
||||
SettingsView(iapService: IAPService())
|
||||
.environmentObject(AccountStore(iapService: IAPService()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user