import UIKit import SwiftUI import UniformTypeIdentifiers // MARK: - Quick Update share extension // // Appears in the share sheet when the user shares/selects text in ANY app (their // bank, broker…). Shows a compact form floating over the host app: pick a source // (the next one pending this month is preselected), amount prefilled from the // shared text, save — and the user never leaves the bank app. The main app turns // these captures into real snapshots on its next activation. @objc(ShareViewController) final class ShareViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear extractSharedText { [weak self] text in DispatchQueue.main.async { self?.presentForm(sharedText: text) } } } private func extractSharedText(completion: @escaping (String?) -> Void) { let providers = (extensionContext?.inputItems as? [NSExtensionItem])? .compactMap { $0.attachments } .flatMap { $0 } ?? [] guard let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) }) else { completion(nil) return } provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, _ in completion(item as? String) } } private func presentForm(sharedText: String?) { let form = QuickUpdateShareView( sharedText: sharedText, onDone: { [weak self] in self?.extensionContext?.completeRequest(returningItems: nil) }, onCancel: { [weak self] in self?.extensionContext?.cancelRequest(withError: NSError( domain: "com.alexandrevazquez.PortfolioJournal.QuickUpdate", code: NSUserCancelledError )) } ) let host = UIHostingController(rootView: form) host.modalPresentationStyle = .formSheet if let sheet = host.sheetPresentationController { sheet.detents = [.medium(), .large()] sheet.prefersGrabberVisible = true } host.isModalInPresentation = true present(host, animated: true) } } // MARK: - Form struct QuickUpdateShareView: View { let sharedText: String? let onDone: () -> Void let onCancel: () -> Void @State private var sources: [ExtSourceInfo] = [] @State private var selectedSourceId: UUID? @State private var amountText: String = "" @State private var saved = false @FocusState private var amountFocused: Bool private var parsedAmount: Decimal? { ExtAmountParser.parse(amountText) } var body: some View { NavigationStack { Group { if sources.isEmpty { ContentUnavailableView( String(localized: "ext_no_sources_title"), systemImage: "list.bullet", description: Text(String(localized: "ext_no_sources_body")) ) } else if saved { savedConfirmation } else { form } } .navigationTitle(String(localized: "ext_title")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(String(localized: "ext_cancel")) { onCancel() } } if !sources.isEmpty && !saved { ToolbarItem(placement: .confirmationAction) { Button(String(localized: "ext_save")) { save() } .disabled(parsedAmount == nil || selectedSourceId == nil) .fontWeight(.semibold) } } } } .onAppear(perform: load) } private var form: some View { Form { Section(String(localized: "ext_amount_section")) { TextField(String(localized: "ext_amount_placeholder"), text: $amountText) .keyboardType(.decimalPad) .font(.title3.weight(.semibold)) .focused($amountFocused) } Section(String(localized: "ext_source_section")) { ForEach(sources) { source in Button { selectedSourceId = source.id } label: { HStack { VStack(alignment: .leading, spacing: 2) { Text(source.name) .foregroundColor(.primary) .font(.subheadline.weight(.medium)) if source.updatedThisMonth { Text(String(localized: "ext_already_updated")) .font(.caption2) .foregroundColor(.secondary) } } Spacer() if selectedSourceId == source.id { Image(systemName: "checkmark.circle.fill") .foregroundColor(.blue) } } } } } } } private var savedConfirmation: some View { VStack(spacing: 14) { Image(systemName: "checkmark.circle.fill") .font(.system(size: 52)) .foregroundColor(.green) Text(String(localized: "ext_saved_title")) .font(.headline) Text(String(localized: "ext_saved_body")) .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .padding(32) .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { onDone() } } } private func load() { var mirror = ExtSharedStore.readMirror() // Pending-first ordering: the sources still waiting this month come on top. mirror.sort { a, b in if a.updatedThisMonth != b.updatedThisMonth { return !a.updatedThisMonth } return a.name.localizedCaseInsensitiveCompare(b.name) == .orderedAscending } sources = mirror selectedSourceId = mirror.first(where: { !$0.updatedThisMonth })?.id ?? mirror.first?.id if let sharedText, let parsed = ExtAmountParser.parse(sharedText) { amountText = "\(parsed)" } else { amountFocused = true } } private func save() { guard let amount = parsedAmount, let sourceId = selectedSourceId else { return } ExtSharedStore.appendPending(ExtPendingUpdate( sourceId: sourceId, amount: NSDecimalNumber(decimal: amount).doubleValue, capturedAt: Date() )) withAnimation { saved = true } } }