Improve goal sharing experience
This commit is contained in:
@@ -1,44 +1,190 @@
|
||||
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
|
||||
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
|
||||
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 {
|
||||
let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil)
|
||||
viewController.present(activityVC, animated: true)
|
||||
// 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 {
|
||||
let text = "I am \(Int(progress * 100))% towards \(name) on Portfolio Journal!"
|
||||
let activityVC = UIActivityViewController(activityItems: [text], applicationActivities: nil)
|
||||
viewController.present(activityVC, animated: true)
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user