|
@@ -81,11 +81,22 @@ struct PaywallView: View {
|
|
|
if !subscriptions.hasAllProductsLoaded, !subscriptions.isLoadingProducts {
|
|
if !subscriptions.hasAllProductsLoaded, !subscriptions.isLoadingProducts {
|
|
|
await subscriptions.loadProducts(presentLoadingUI: true)
|
|
await subscriptions.loadProducts(presentLoadingUI: true)
|
|
|
}
|
|
}
|
|
|
|
|
+ await subscriptions.refreshTrialEligibility()
|
|
|
await paywallConfigService.refreshFromRemote()
|
|
await paywallConfigService.refreshFromRemote()
|
|
|
viewModel.syncSelectedPlan(with: availablePlans)
|
|
viewModel.syncSelectedPlan(with: availablePlans)
|
|
|
}
|
|
}
|
|
|
.onChange(of: subscriptions.hasEverPurchasedPremium) { _, _ in
|
|
.onChange(of: subscriptions.hasEverPurchasedPremium) { _, _ in
|
|
|
viewModel.syncSelectedPlan(with: availablePlans)
|
|
viewModel.syncSelectedPlan(with: availablePlans)
|
|
|
|
|
+ if let eligiblePlan = config.trialEligiblePlan,
|
|
|
|
|
+ subscriptions.trialDisplay(for: eligiblePlan) != nil {
|
|
|
|
|
+ viewModel.selectPlan(eligiblePlan)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .onChange(of: subscriptions.trialDisplayByPlan) { _, _ in
|
|
|
|
|
+ if let eligiblePlan = config.trialEligiblePlan,
|
|
|
|
|
+ subscriptions.trialDisplay(for: eligiblePlan) != nil {
|
|
|
|
|
+ viewModel.selectPlan(eligiblePlan)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
.onReceive(NotificationCenter.default.publisher(for: .paywallConfigDidUpdate)) { _ in }
|
|
.onReceive(NotificationCenter.default.publisher(for: .paywallConfigDidUpdate)) { _ in }
|
|
|
.alert(
|
|
.alert(
|
|
@@ -185,6 +196,7 @@ struct PaywallView: View {
|
|
|
plan: plan,
|
|
plan: plan,
|
|
|
config: config,
|
|
config: config,
|
|
|
product: subscriptions.product(for: plan),
|
|
product: subscriptions.product(for: plan),
|
|
|
|
|
+ trial: subscriptions.trialDisplay(for: plan),
|
|
|
isSelected: viewModel.selectedPlan == plan,
|
|
isSelected: viewModel.selectedPlan == plan,
|
|
|
metrics: metrics
|
|
metrics: metrics
|
|
|
) {
|
|
) {
|
|
@@ -198,13 +210,15 @@ struct PaywallView: View {
|
|
|
|
|
|
|
|
private func ctaSection(metrics: PaywallLayoutMetrics) -> some View {
|
|
private func ctaSection(metrics: PaywallLayoutMetrics) -> some View {
|
|
|
let selectedProduct = subscriptions.product(for: viewModel.selectedPlan)
|
|
let selectedProduct = subscriptions.product(for: viewModel.selectedPlan)
|
|
|
|
|
+ let selectedTrial = subscriptions.trialDisplay(for: viewModel.selectedPlan)
|
|
|
let ctaDisabled = buttonsBusy || subscriptions.isLoadingProducts || !isProductReady || hasProductLoadFailure
|
|
let ctaDisabled = buttonsBusy || subscriptions.isLoadingProducts || !isProductReady || hasProductLoadFailure
|
|
|
|
|
|
|
|
return VStack(spacing: metrics.ctaSpacing) {
|
|
return VStack(spacing: metrics.ctaSpacing) {
|
|
|
Text(
|
|
Text(
|
|
|
viewModel.selectedPlan.localizedRenewalDisclosure(
|
|
viewModel.selectedPlan.localizedRenewalDisclosure(
|
|
|
from: selectedProduct,
|
|
from: selectedProduct,
|
|
|
- config: config
|
|
|
|
|
|
|
+ config: config,
|
|
|
|
|
+ trial: selectedTrial
|
|
|
)
|
|
)
|
|
|
)
|
|
)
|
|
|
.font(.system(size: metrics.footerNoteFontSize))
|
|
.font(.system(size: metrics.footerNoteFontSize))
|
|
@@ -228,7 +242,8 @@ struct PaywallView: View {
|
|
|
Text(
|
|
Text(
|
|
|
viewModel.selectedPlan.localizedCTATitle(
|
|
viewModel.selectedPlan.localizedCTATitle(
|
|
|
from: selectedProduct,
|
|
from: selectedProduct,
|
|
|
- config: config
|
|
|
|
|
|
|
+ config: config,
|
|
|
|
|
+ trial: selectedTrial
|
|
|
)
|
|
)
|
|
|
)
|
|
)
|
|
|
.font(.system(size: metrics.ctaFontSize, weight: .bold))
|
|
.font(.system(size: metrics.ctaFontSize, weight: .bold))
|
|
@@ -398,6 +413,7 @@ private struct PaywallPricingCard: View {
|
|
|
let plan: PaywallPlan
|
|
let plan: PaywallPlan
|
|
|
let config: PaywallConfig
|
|
let config: PaywallConfig
|
|
|
let product: Product?
|
|
let product: Product?
|
|
|
|
|
+ let trial: PaywallTrialDisplay?
|
|
|
let isSelected: Bool
|
|
let isSelected: Bool
|
|
|
let metrics: PaywallLayoutMetrics
|
|
let metrics: PaywallLayoutMetrics
|
|
|
let onSelect: () -> Void
|
|
let onSelect: () -> Void
|
|
@@ -417,7 +433,7 @@ private struct PaywallPricingCard: View {
|
|
|
.font(.system(size: metrics.ctaFontSize, weight: .semibold))
|
|
.font(.system(size: metrics.ctaFontSize, weight: .semibold))
|
|
|
.foregroundStyle(AppTheme.textPrimary)
|
|
.foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
|
|
|
- Text(plan.localizedSubtitle(from: product, config: config))
|
|
|
|
|
|
|
+ Text(plan.localizedSubtitle(from: product, config: config, trial: trial))
|
|
|
.font(.system(size: metrics.featureFontSize * 0.85))
|
|
.font(.system(size: metrics.featureFontSize * 0.85))
|
|
|
.foregroundStyle(AppTheme.textTertiary)
|
|
.foregroundStyle(AppTheme.textTertiary)
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
@@ -470,22 +486,28 @@ private struct PaywallPricingCard: View {
|
|
|
)
|
|
)
|
|
|
)
|
|
)
|
|
|
.overlay(alignment: .topTrailing) {
|
|
.overlay(alignment: .topTrailing) {
|
|
|
- if let badge = planCopy.badge, plan == .lifetime {
|
|
|
|
|
- Text(badge)
|
|
|
|
|
- .font(.system(size: metrics.featureFontSize * 0.75, weight: .bold))
|
|
|
|
|
- .foregroundStyle(Color(hex: 0x0B0E14))
|
|
|
|
|
- .padding(.horizontal, 10)
|
|
|
|
|
- .padding(.vertical, 4)
|
|
|
|
|
- .background(AppTheme.accentTeal)
|
|
|
|
|
- .clipShape(Capsule())
|
|
|
|
|
- .padding(.top, metrics.cardPadding * 0.65)
|
|
|
|
|
- .padding(.trailing, metrics.cardPadding * 0.65)
|
|
|
|
|
|
|
+ if let trial {
|
|
|
|
|
+ cardBadge(trial.badgeText)
|
|
|
|
|
+ } else if let badge = planCopy.badge, plan == .lifetime {
|
|
|
|
|
+ cardBadge(badge)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
.buttonStyle(AppPlainButtonStyle())
|
|
.buttonStyle(AppPlainButtonStyle())
|
|
|
.hoverCard(cornerRadius: metrics.cornerRadius, isSelected: isSelected)
|
|
.hoverCard(cornerRadius: metrics.cornerRadius, isSelected: isSelected)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private func cardBadge(_ text: String) -> some View {
|
|
|
|
|
+ Text(text)
|
|
|
|
|
+ .font(.system(size: metrics.featureFontSize * 0.75, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(Color(hex: 0x0B0E14))
|
|
|
|
|
+ .padding(.horizontal, 10)
|
|
|
|
|
+ .padding(.vertical, 4)
|
|
|
|
|
+ .background(AppTheme.accentTeal)
|
|
|
|
|
+ .clipShape(Capsule())
|
|
|
|
|
+ .padding(.top, metrics.cardPadding * 0.65)
|
|
|
|
|
+ .padding(.trailing, metrics.cardPadding * 0.65)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#Preview {
|
|
#Preview {
|