Adds the DI seam to the 5 previously singleton-coupled VMs: - VerifyEmailViewModel - RegisterViewModel - PasswordResetViewModel - AppleSignInViewModel - OnboardingTasksViewModel All now accept init(dataManager: DataManagerObservable = .shared). iOSApp.swift injects DataManagerObservable.shared at the root via .environmentObject so descendant views can reach it via @EnvironmentObject without implicit singleton reads. Dependencies.swift factories updated to pass DataManager.shared explicitly into Kotlin VM constructors — SKIE doesn't surface Kotlin default init parameters as Swift defaults, so every Kotlin VM call-site needs the explicit argument. Affects makeAuthViewModel, makeResidenceViewModel, makeTaskViewModel, makeContractorViewModel, makeDocumentViewModel. Full iOS build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
5.1 KiB
Swift
132 lines
5.1 KiB
Swift
import Foundation
|
|
import ComposeApp
|
|
|
|
/// Backs the First-Task onboarding screen. Owns the network calls for
|
|
/// personalised suggestions and the full template catalog, plus the bulk
|
|
/// task-create submission. No hardcoded suggestion rules or fallback
|
|
/// catalog — when the API fails the screen shows error+retry+skip.
|
|
///
|
|
/// Mirrors the Android `OnboardingViewModel.suggestionsState` /
|
|
/// `templatesGroupedState` / `createTasksState` flows in a purely Swift
|
|
/// shape: calling `APILayer.shared.*` directly is more idiomatic here than
|
|
/// observing Kotlin StateFlows, and matches the pattern used by
|
|
/// `TaskViewModel.swift`.
|
|
@MainActor
|
|
final class OnboardingTasksViewModel: ObservableObject {
|
|
|
|
// MARK: - Suggestions (For You tab)
|
|
@Published private(set) var suggestions: [TaskSuggestionResponse] = []
|
|
@Published private(set) var profileCompleteness: Double = 0
|
|
@Published private(set) var isLoadingSuggestions = false
|
|
@Published private(set) var suggestionsError: String?
|
|
/// True once `loadSuggestions` has returned any terminal state.
|
|
/// Used by the view to distinguish "haven't tried yet" from "tried and
|
|
/// returned empty".
|
|
@Published private(set) var suggestionsAttempted = false
|
|
|
|
// MARK: - Grouped catalog (Browse All tab)
|
|
@Published private(set) var grouped: TaskTemplatesGroupedResponse?
|
|
@Published private(set) var isLoadingGrouped = false
|
|
@Published private(set) var groupedError: String?
|
|
|
|
// MARK: - Submission
|
|
@Published private(set) var isSubmitting = false
|
|
@Published private(set) var submitError: String?
|
|
|
|
// MARK: - Private Properties
|
|
private let dataManager: DataManagerObservable
|
|
|
|
// MARK: - Initialization
|
|
init(dataManager: DataManagerObservable = .shared) {
|
|
self.dataManager = dataManager
|
|
}
|
|
|
|
// MARK: - Loads
|
|
|
|
func loadSuggestions(residenceId: Int32) async {
|
|
if isLoadingSuggestions { return }
|
|
isLoadingSuggestions = true
|
|
suggestionsError = nil
|
|
|
|
do {
|
|
let result = try await APILayer.shared.getTaskSuggestions(residenceId: residenceId)
|
|
if let success = result as? ApiResultSuccess<TaskSuggestionsResponse>,
|
|
let data = success.data {
|
|
suggestions = data.suggestions
|
|
profileCompleteness = data.profileCompleteness
|
|
AnalyticsManager.shared.track(.onboardingSuggestionsLoaded(
|
|
count: data.suggestions.count,
|
|
profileCompleteness: data.profileCompleteness
|
|
))
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
suggestionsError = ErrorMessageParser.parse(error.message)
|
|
} else {
|
|
suggestionsError = "Could not load suggestions."
|
|
}
|
|
} catch {
|
|
suggestionsError = ErrorMessageParser.parse(error.localizedDescription)
|
|
}
|
|
|
|
isLoadingSuggestions = false
|
|
suggestionsAttempted = true
|
|
}
|
|
|
|
func loadGrouped(forceRefresh: Bool = false) async {
|
|
if isLoadingGrouped { return }
|
|
isLoadingGrouped = true
|
|
groupedError = nil
|
|
|
|
do {
|
|
let result = try await APILayer.shared.getTaskTemplatesGrouped(forceRefresh: forceRefresh)
|
|
if let success = result as? ApiResultSuccess<TaskTemplatesGroupedResponse>,
|
|
let data = success.data {
|
|
grouped = data
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
groupedError = ErrorMessageParser.parse(error.message)
|
|
} else {
|
|
groupedError = "Could not load templates."
|
|
}
|
|
} catch {
|
|
groupedError = ErrorMessageParser.parse(error.localizedDescription)
|
|
}
|
|
|
|
isLoadingGrouped = false
|
|
}
|
|
|
|
// MARK: - Submit
|
|
|
|
/// Posts the picked tasks in a single transaction via the bulk endpoint.
|
|
/// Returns true on any successful server response (including empty
|
|
/// selections, which short-circuit without a network call). False is
|
|
/// terminal — the caller should show the stored `submitError`.
|
|
func submit(residenceId: Int32, requests: [TaskCreateRequest]) async -> Bool {
|
|
if requests.isEmpty {
|
|
AnalyticsManager.shared.track(.onboardingTaskStepSkipped(reason: "user_skip"))
|
|
return true
|
|
}
|
|
|
|
isSubmitting = true
|
|
submitError = nil
|
|
|
|
let request = BulkCreateTasksRequest(residenceId: residenceId, tasks: requests)
|
|
defer { isSubmitting = false }
|
|
|
|
do {
|
|
let result = try await APILayer.shared.bulkCreateTasks(request: request)
|
|
if result is ApiResultSuccess<BulkCreateTasksResponse> {
|
|
AnalyticsManager.shared.track(.onboardingTasksCreated(count: requests.count))
|
|
return true
|
|
} else if let error = ApiResultBridge.error(from: result) {
|
|
submitError = ErrorMessageParser.parse(error.message)
|
|
return false
|
|
} else {
|
|
submitError = "Could not create tasks."
|
|
return false
|
|
}
|
|
} catch {
|
|
submitError = ErrorMessageParser.parse(error.localizedDescription)
|
|
return false
|
|
}
|
|
}
|
|
}
|