Fix causa raíz de taps muertos en Charts iPad: AppBackground interceptaba toques (build 50)
El círculo decorativo de AppBackground (70% del ancho del panel, offset -35%) se desborda de su panel por diseño y en layouts side-by-side queda flotando SOBRE el sidebar de Charts. Las shapes de SwiftUI participan en hit-testing por defecto y el panel derecho va después en el HStack → cada tap en la zona del sidebar cubierta por el círculo moría en silencio. El patrón lo delató: fallaban Overview + Analyze (arriba, bajo el círculo) y funcionaban Risk + Forecast (abajo). Dependía de la geometría (orientación/tamaño), por eso no reproducía en el simulador en portrait. - AppBackground: .allowsHitTesting(false) — un fondo decorativo jamás debe interceptar toques (fix global: aplica también a Sources/Journal en iPad) - Panel de detalle de Charts: .clipped() para que la decoración tampoco PINTE sobre el sidebar - Selección premium nunca se bloquea: los charts premium se seleccionan y muestran teaser de desbloqueo en el área del chart (chart_locked_* ×7 idiomas); el paywall se presenta desde el botón (contexto fiable) - UITests: testChartSidebarSelection ahora corre en landscape (geometría que reproducía el bug) + testChartSelectionWithoutPremium nuevo; --no-premium en ScreenshotMode para testear la experiencia free Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
This commit is contained in:
@@ -77,7 +77,8 @@ struct ChartsContainerView: View {
|
||||
|
||||
Divider()
|
||||
|
||||
// Right panel: KPI header + period control + chart
|
||||
// Right panel: KPI header + period control + chart.
|
||||
// Clipped so the decorative background can't paint over the sidebar.
|
||||
ZStack {
|
||||
AppBackground()
|
||||
ScrollView {
|
||||
@@ -100,6 +101,7 @@ struct ChartsContainerView: View {
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.clipped()
|
||||
}
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
// Attached here (not on the layout-switching Group): sheets presented from
|
||||
@@ -257,9 +259,13 @@ struct ChartsContainerView: View {
|
||||
/// Chart-specific metrics above the chart. Each chart type surfaces the KPIs
|
||||
/// that make sense for it (the same ones its in-card stats row shows on
|
||||
/// iPhone) instead of the always-identical portfolio aggregates.
|
||||
private var selectedChartLocked: Bool {
|
||||
viewModel.selectedChartType.isPremium && !viewModel.isPremium
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var kpiHeader: some View {
|
||||
let stats = chartSpecificStats
|
||||
let stats = selectedChartLocked ? [] : chartSpecificStats
|
||||
if !stats.isEmpty {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(stats.indices, id: \.self) { i in
|
||||
@@ -464,7 +470,7 @@ struct ChartsContainerView: View {
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.disabled(!viewModel.hasData || viewModel.isLoading)
|
||||
.disabled(!viewModel.hasData || viewModel.isLoading || selectedChartLocked)
|
||||
.accessibilityLabel(String(localized: "chart_share_button"))
|
||||
}
|
||||
|
||||
@@ -575,7 +581,9 @@ struct ChartsContainerView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var chartContent: some View {
|
||||
if viewModel.isLoading {
|
||||
if viewModel.selectedChartType.isPremium && !viewModel.isPremium {
|
||||
premiumLockedView
|
||||
} else if viewModel.isLoading {
|
||||
ProgressView()
|
||||
.frame(height: 300)
|
||||
} else if !viewModel.hasData {
|
||||
@@ -634,6 +642,53 @@ struct ChartsContainerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shown in place of a premium chart when the user isn't premium. Selecting
|
||||
/// the chart always works; unlocking is an explicit button tap (a reliable
|
||||
/// context for presenting the paywall sheet).
|
||||
private var premiumLockedView: some View {
|
||||
VStack(spacing: 16) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.appPrimary.opacity(0.12))
|
||||
.frame(width: 88, height: 88)
|
||||
Image(systemName: viewModel.selectedChartType.icon)
|
||||
.font(.system(size: 34))
|
||||
.foregroundColor(.appPrimary)
|
||||
Image(systemName: "lock.circle.fill")
|
||||
.font(.system(size: 26))
|
||||
.foregroundColor(.appWarning)
|
||||
.background(Circle().fill(Color(.systemBackground)))
|
||||
.offset(x: 32, y: 30)
|
||||
}
|
||||
|
||||
Text(String(format: String(localized: "chart_locked_title"), viewModel.selectedChartType.rawValue))
|
||||
.font(.headline)
|
||||
|
||||
Text(viewModel.selectedChartType.description)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Button {
|
||||
FirebaseService.shared.logPaywallShown(trigger: "advanced_charts")
|
||||
viewModel.showingPaywall = true
|
||||
} label: {
|
||||
Text(String(localized: "chart_locked_cta"))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.appPrimary)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(24)
|
||||
}
|
||||
}
|
||||
.padding(32)
|
||||
.frame(maxWidth: .infinity, minHeight: 320)
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
||||
}
|
||||
|
||||
private var emptyStateView: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "chart.bar.xaxis")
|
||||
|
||||
Reference in New Issue
Block a user