import Foundation import UIKit import SwiftUI import LinkPresentation class ShareService { static let shared = ShareService() private init() {} static func buildMonthlyCheckInShareText(summary: MonthlySummary, appName: String) -> String { """ \(summary.periodLabel) Check-in Starting: \(summary.formattedStartingValue) Ending: \(summary.formattedEndingValue) Contributions: \(summary.formattedContributions) Net performance: \(summary.formattedNetPerformance) (\(summary.formattedNetPerformancePercentage)) Shared from \(appName) """ } static func buildPortfolioValueShareText( totalValue: String, changeText: String, changeLabel: String, yearChange: String?, sinceInceptionChange: String?, appName: String ) -> String { var lines = [ "Total Portfolio Value", totalValue, "\(changeText) \(changeLabel)" ] if let yearChange { lines.append("YoY: \(yearChange)") } if let sinceInceptionChange { lines.append("Since inception: \(sinceInceptionChange)") } lines.append("") lines.append("Shared from \(appName)") return lines.joined(separator: "\n") } @MainActor func shareMonthlyCheckIn(summary: MonthlySummary, appName: String) { let text = Self.buildMonthlyCheckInShareText(summary: summary, appName: appName) shareCard( cardTitle: "\(summary.periodLabel) Check-in", fallbackText: text ) { MonthlyCheckInShareCardView( summary: summary, appName: appName, qrCodeImage: GoalShareService.generateQRCode(for: GoalShareService.appStoreURL, size: 200) ) } } @MainActor func sharePortfolioValue( totalValue: String, changeText: String, changeLabel: String, yearChange: String?, sinceInceptionChange: String? ) { let appName = Self.appDisplayName let text = Self.buildPortfolioValueShareText( totalValue: totalValue, changeText: changeText, changeLabel: changeLabel, yearChange: yearChange, sinceInceptionChange: sinceInceptionChange, appName: appName ) shareCard( cardTitle: "Portfolio Snapshot", fallbackText: text ) { PortfolioValueShareCardView( totalValue: totalValue, changeText: changeText, changeLabel: changeLabel, yearChange: yearChange, sinceInceptionChange: sinceInceptionChange, appName: appName, qrCodeImage: GoalShareService.generateQRCode(for: GoalShareService.appStoreURL, size: 200) ) } } func shareText(_ content: String) { guard let viewController = ShareService.topViewController() else { return } let activityVC = UIActivityViewController( activityItems: [content], applicationActivities: nil ) if let popover = activityVC.popoverPresentationController { popover.sourceView = viewController.view popover.sourceRect = CGRect( x: viewController.view.bounds.midX, y: viewController.view.bounds.midY, width: 0, height: 0 ) } DispatchQueue.main.async { viewController.present(activityVC, animated: true) } } @MainActor private func shareCard( cardTitle: String, fallbackText: String, @ViewBuilder card: () -> Content ) { guard let viewController = ShareService.topViewController() else { return } let shareURL = GoalShareService.appStoreURL if #available(iOS 16.0, *) { let renderer = ImageRenderer(content: card()) let scale = viewController.view.window?.windowScene?.screen.scale ?? viewController.traitCollection.displayScale renderer.scale = scale if let image = renderer.uiImage { let item = CardShareItem( image: image, title: cardTitle, text: fallbackText, url: shareURL ) presentShareSheet(items: [item], from: viewController) return } } presentShareSheet(items: [fallbackText], from: viewController) } func shareTextFile(content: String, fileName: String) { guard let viewController = ShareService.topViewController() else { return } let tempURL = FileManager.default.temporaryDirectory .appendingPathComponent(fileName) do { try content.write(to: tempURL, atomically: true, encoding: .utf8) let activityVC = UIActivityViewController( activityItems: [tempURL], applicationActivities: nil ) if let popover = activityVC.popoverPresentationController { popover.sourceView = viewController.view popover.sourceRect = CGRect( x: viewController.view.bounds.midX, y: viewController.view.bounds.midY, width: 0, height: 0 ) } DispatchQueue.main.async { viewController.present(activityVC, animated: true) } } catch { print("Share file error: \(error)") } } func shareCalendarEvent( title: String, notes: String, startDate: Date, durationMinutes: Int = 60 ) { let endDate = startDate.addingTimeInterval(TimeInterval(durationMinutes * 60)) let icsContent = calendarICS( title: title, notes: notes, startDate: startDate, endDate: endDate ) shareTextFile(content: icsContent, fileName: "PortfolioJournal-CheckIn.ics") } private static func topViewController( base: UIViewController? = UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .flatMap { $0.windows } .first(where: { $0.isKeyWindow })?.rootViewController ) -> UIViewController? { if let nav = base as? UINavigationController { return topViewController(base: nav.visibleViewController) } if let tab = base as? UITabBarController { return topViewController(base: tab.selectedViewController) } if let presented = base?.presentedViewController { return topViewController(base: presented) } return base } private func calendarICS( title: String, notes: String, startDate: Date, endDate: Date ) -> String { let uid = UUID().uuidString let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" formatter.timeZone = TimeZone(secondsFromGMT: 0) let stamp = formatter.string(from: Date()) let start = formatter.string(from: startDate) let end = formatter.string(from: endDate) return """ BEGIN:VCALENDAR VERSION:2.0 PRODID:-//PortfolioJournal//MonthlyCheckIn//EN BEGIN:VEVENT UID:\(uid) DTSTAMP:\(stamp) DTSTART:\(start) DTEND:\(end) SUMMARY:\(escapeICS(title)) DESCRIPTION:\(escapeICS(notes)) END:VEVENT END:VCALENDAR """ } private func escapeICS(_ value: String) -> String { value .replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "\n", with: "\\n") .replacingOccurrences(of: ";", with: "\\;") .replacingOccurrences(of: ",", with: "\\,") } private func presentShareSheet(items: [Any], from viewController: UIViewController) { let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil) if let popover = activityVC.popoverPresentationController { popover.sourceView = viewController.view popover.sourceRect = CGRect( x: viewController.view.bounds.midX, y: viewController.view.bounds.midY, width: 0, height: 0 ) popover.permittedArrowDirections = [] } viewController.present(activityVC, animated: true) } private static var appDisplayName: String { if let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String { return name } if let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String { return name } return "Portfolio Journal" } } private class CardShareItem: NSObject, UIActivityItemSource { let image: UIImage let title: String let text: String let url: URL init(image: UIImage, title: String, text: String, url: URL) { self.image = image self.title = title self.text = text self.url = url } func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { image } func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { image } func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String { title } func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { let metadata = LPLinkMetadata() metadata.title = title metadata.originalURL = url metadata.url = url metadata.imageProvider = NSItemProvider(object: image) if let appIcon = UIImage(named: "BrandMark") { metadata.iconProvider = NSItemProvider(object: appIcon) } return metadata } }