Share cards 2.0: dark panorama + privacidad + Story + atribución (build 82) #34
Rediseño de las cards de compartir con el sistema visual de las screenshots ASO: fondo navy con glows y grid, sparkline de evolución con gradiente azul→cian y punto final, chip de delta, footer con marca + QR. Aplica a Portfolio Snapshot, Monthly Check-in y Monthly Summary. Potencia referral: sheet de opciones previo con preview en vivo (PortfolioShareOptionsView) — toggle «Solo porcentajes» (sin importes absolutos, la fricción #1 para compartir) y formato Post/Story 9:16. QR y link con ?ct=snapshot_share para atribución en App Analytics. Analytics: share type portfolio_value_percent. L10n ×7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
This commit is contained in:
@@ -6,8 +6,55 @@ import LinkPresentation
|
||||
class ShareService {
|
||||
static let shared = ShareService()
|
||||
|
||||
/// Attributed App Store link for shared cards (ct= shows up in App
|
||||
/// Analytics as a campaign, so referral installs become measurable).
|
||||
static let snapshotShareURL = URL(string: "https://apps.apple.com/app/portfolio-journal-tracker/id6757678318?ct=snapshot_share")!
|
||||
|
||||
private init() {}
|
||||
|
||||
/// Extracts the "(±X.XX%)" part of a combined "±1.234 € (±X.XX%)" string.
|
||||
static func percentPart(_ s: String?) -> String? {
|
||||
guard let s else { return nil }
|
||||
guard let open = s.lastIndex(of: "("), let close = s.lastIndex(of: ")"), open < close else {
|
||||
return s
|
||||
}
|
||||
return String(s[s.index(after: open)..<close])
|
||||
}
|
||||
|
||||
struct PortfolioCardStrings {
|
||||
let totalValue: String
|
||||
let changeText: String
|
||||
let yearChange: String?
|
||||
let sinceInceptionChange: String?
|
||||
}
|
||||
|
||||
/// Privacy transform: with `percentOnly` the card leads with the all-time
|
||||
/// return and every row drops absolute amounts. Nobody shares their net
|
||||
/// worth in euros; everybody shares +42%.
|
||||
static func portfolioCardStrings(
|
||||
totalValue: String,
|
||||
changeText: String,
|
||||
yearChange: String?,
|
||||
sinceInceptionChange: String?,
|
||||
percentOnly: Bool
|
||||
) -> PortfolioCardStrings {
|
||||
guard percentOnly else {
|
||||
return PortfolioCardStrings(
|
||||
totalValue: totalValue, changeText: changeText,
|
||||
yearChange: yearChange, sinceInceptionChange: sinceInceptionChange
|
||||
)
|
||||
}
|
||||
let heroPercent = percentPart(sinceInceptionChange)
|
||||
?? percentPart(changeText)
|
||||
?? totalValue
|
||||
return PortfolioCardStrings(
|
||||
totalValue: heroPercent,
|
||||
changeText: percentPart(changeText) ?? changeText,
|
||||
yearChange: percentPart(yearChange),
|
||||
sinceInceptionChange: percentPart(sinceInceptionChange)
|
||||
)
|
||||
}
|
||||
|
||||
static func buildMonthlyCheckInShareText(summary: MonthlySummary, appName: String) -> String {
|
||||
"""
|
||||
\(summary.periodLabel) Check-in
|
||||
@@ -118,31 +165,47 @@ class ShareService {
|
||||
changeText: String,
|
||||
changeLabel: String,
|
||||
yearChange: String?,
|
||||
sinceInceptionChange: String?
|
||||
sinceInceptionChange: String?,
|
||||
sparkline: [Double] = [],
|
||||
isPositive: Bool = true,
|
||||
percentOnly: Bool = false,
|
||||
storyFormat: Bool = false
|
||||
) {
|
||||
FirebaseService.shared.logShare(type: "portfolio_value")
|
||||
FirebaseService.shared.logShare(type: percentOnly ? "portfolio_value_percent" : "portfolio_value")
|
||||
let appName = Self.appDisplayName
|
||||
let text = Self.buildPortfolioValueShareText(
|
||||
let display = Self.portfolioCardStrings(
|
||||
totalValue: totalValue,
|
||||
changeText: changeText,
|
||||
changeLabel: changeLabel,
|
||||
yearChange: yearChange,
|
||||
sinceInceptionChange: sinceInceptionChange,
|
||||
percentOnly: percentOnly
|
||||
)
|
||||
// Fallback text honors the privacy choice too.
|
||||
let text = Self.buildPortfolioValueShareText(
|
||||
totalValue: display.totalValue,
|
||||
changeText: display.changeText,
|
||||
changeLabel: changeLabel,
|
||||
yearChange: display.yearChange,
|
||||
sinceInceptionChange: display.sinceInceptionChange,
|
||||
appName: appName
|
||||
)
|
||||
|
||||
shareCard(
|
||||
cardTitle: "Portfolio Snapshot",
|
||||
fallbackText: text
|
||||
fallbackText: text,
|
||||
shareURL: Self.snapshotShareURL
|
||||
) {
|
||||
PortfolioValueShareCardView(
|
||||
totalValue: totalValue,
|
||||
changeText: changeText,
|
||||
totalValue: display.totalValue,
|
||||
changeText: display.changeText,
|
||||
changeLabel: changeLabel,
|
||||
yearChange: yearChange,
|
||||
sinceInceptionChange: sinceInceptionChange,
|
||||
yearChange: display.yearChange,
|
||||
sinceInceptionChange: display.sinceInceptionChange,
|
||||
appName: appName,
|
||||
qrCodeImage: GoalShareService.generateQRCode(for: GoalShareService.appStoreURL, size: 200)
|
||||
qrCodeImage: GoalShareService.generateQRCode(for: Self.snapshotShareURL, size: 200),
|
||||
sparkline: sparkline,
|
||||
isPositive: isPositive,
|
||||
storyFormat: storyFormat
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -174,10 +237,10 @@ class ShareService {
|
||||
private func shareCard<Content: View>(
|
||||
cardTitle: String,
|
||||
fallbackText: String,
|
||||
shareURL: URL = GoalShareService.appStoreURL,
|
||||
@ViewBuilder card: () -> Content
|
||||
) {
|
||||
guard let viewController = ShareService.topViewController() else { return }
|
||||
let shareURL = GoalShareService.appStoreURL
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
let renderer = ImageRenderer(content: card())
|
||||
|
||||
Reference in New Issue
Block a user