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:
alexandrev-tibco
2026-08-06 23:09:59 +02:00
parent 6e6f4c5489
commit 8084bc6a82
12 changed files with 492 additions and 121 deletions
+74 -11
View File
@@ -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())