Files
InvestmentTrackerApp/PortfolioJournal/Services/GoalShareService.swift
T
2026-02-01 11:14:48 +01:00

191 lines
6.9 KiB
Swift

import Foundation
import SwiftUI
import UIKit
import CoreImage.CIFilterBuiltins
class GoalShareService {
static let shared = GoalShareService()
/// App Store URL - auto-redirects to user's country
static let appStoreURL = URL(string: "https://apps.apple.com/app/portfolio-journal/id6744983373")!
/// Website URL - has Open Graph tags for social media previews
static let websiteURL = URL(string: "https://portfoliojournal.app")!
/// Share page base URL - parameters will be appended
static let sharePageBaseURL = "https://portfoliojournal.app/share"
private init() {}
/// Build share URL with goal parameters for dynamic OG tags
static func buildShareURL(goalName: String, progressPercent: Int) -> URL {
var components = URLComponents(string: sharePageBaseURL)!
components.queryItems = [
URLQueryItem(name: "goal", value: goalName),
URLQueryItem(name: "progress", value: String(progressPercent))
]
return components.url ?? URL(string: sharePageBaseURL)!
}
/// Generate a QR code image for the App Store URL
static func generateQRCode(for url: URL, size: CGFloat = 60) -> UIImage? {
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(url.absoluteString.utf8)
filter.correctionLevel = "M"
guard let outputImage = filter.outputImage else { return nil }
// Scale up the QR code
let scale = size / outputImage.extent.size.width
let scaledImage = outputImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
guard let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) else {
return nil
}
return UIImage(cgImage: cgImage)
}
@MainActor
func shareGoal(
name: String,
progress: Double,
currentValue: Decimal,
targetValue: Decimal,
targetDate: Date? = nil,
estimatedCompletionDate: Date? = nil,
privacyMode: Bool = false
) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let viewController = windowScene.windows.first?.rootViewController else {
return
}
// Generate QR code for the card
let qrCodeImage = Self.generateQRCode(for: Self.appStoreURL, size: 200)
let card = GoalShareCardView(
name: name,
progress: progress,
currentValue: currentValue,
targetValue: targetValue,
targetDate: targetDate,
estimatedCompletionDate: estimatedCompletionDate,
privacyMode: privacyMode,
qrCodeImage: qrCodeImage
)
let progressPercent = Int(progress * 100)
let shareURL = Self.buildShareURL(goalName: name, progressPercent: progressPercent)
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 {
// Use combined item provider for text + image together
let shareItem = GoalShareItem(
image: image,
goalName: name,
progressPercent: progressPercent,
shareURL: shareURL
)
presentShareSheet(items: [shareItem], from: viewController)
} else {
// Fallback to text only
let text = buildShareText(goalName: name, progressPercent: progressPercent, shareURL: shareURL)
presentShareSheet(items: [text], from: viewController)
}
} else {
// iOS 15 fallback - text only
let text = buildShareText(goalName: name, progressPercent: progressPercent, shareURL: shareURL)
presentShareSheet(items: [text], from: viewController)
}
}
private func buildShareText(goalName: String, progressPercent: Int, shareURL: URL) -> String {
return """
I'm \(progressPercent)% towards my "\(goalName)" goal! 🎯
Track your investment goals with Portfolio Journal.
\(shareURL.absoluteString)
"""
}
private func presentShareSheet(items: [Any], from viewController: UIViewController) {
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil)
// Exclude some activities that don't make sense for goal sharing
activityVC.excludedActivityTypes = [
.addToReadingList,
.assignToContact,
.openInIBooks
]
// iPad support - prevent crash by setting popover source
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)
}
}
/// Combined share item that provides both image and text together
private class GoalShareItem: NSObject, UIActivityItemSource {
let image: UIImage
let goalName: String
let progressPercent: Int
let shareURL: URL
init(image: UIImage, goalName: String, progressPercent: Int, shareURL: URL) {
self.image = image
self.goalName = goalName
self.progressPercent = progressPercent
self.shareURL = shareURL
super.init()
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return image
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
// For most activities, return the image
// The text will be provided via LPLinkMetadata or as a separate item
return image
}
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
return "My Investment Goal Progress - Portfolio Journal"
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let metadata = LPLinkMetadata()
metadata.title = "I'm \(progressPercent)% towards my \"\(goalName)\" goal! 🎯"
metadata.originalURL = shareURL
metadata.url = shareURL
metadata.imageProvider = NSItemProvider(object: image)
// Set icon
if let appIcon = UIImage(named: "BrandMark") {
metadata.iconProvider = NSItemProvider(object: appIcon)
}
return metadata
}
}
import LinkPresentation