Update paywall (#441)

Group features and drop platform purchases.
This commit is contained in:
Davide De Rosa 2023-12-23 12:10:34 +01:00 committed by GitHub
parent 239d3e6853
commit 7d7aaa8b0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 58 additions and 77 deletions

View File

@ -372,6 +372,14 @@ extension View {
.foregroundColor(themeSecondaryColor) .foregroundColor(themeSecondaryColor)
} }
func themeCellTitleStyle() -> some View {
font(.headline)
}
func themeCellSubtitleStyle() -> some View {
font(.subheadline)
}
func themeDebugLogStyle() -> some View { func themeDebugLogStyle() -> some View {
font(.system(size: 13, weight: .medium, design: .monospaced)) font(.system(size: 13, weight: .medium, design: .monospaced))
} }

View File

@ -45,11 +45,11 @@ extension OrganizerView {
return HStack { return HStack {
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text(profile.header.name) Text(profile.header.name)
.font(.headline) .themeCellTitleStyle()
.themeLongTextStyle() .themeLongTextStyle()
VPNStatusText(isActiveProfile: isActiveProfile) VPNStatusText(isActiveProfile: isActiveProfile)
.font(.subheadline) .themeCellSubtitleStyle()
.themeSecondaryTextStyle() .themeSecondaryTextStyle()
} }
Spacer() Spacer()

View File

@ -52,7 +52,10 @@ extension PaywallView {
var body: some View { var body: some View {
List { List {
productsSection featuresSection
purchaseSection
.disabled(purchaseState != nil)
restoreSection
.disabled(purchaseState != nil) .disabled(purchaseState != nil)
}.navigationTitle(Unlocalized.appName) }.navigationTitle(Unlocalized.appName)
@ -77,38 +80,41 @@ private struct PurchaseRow: View {
let title: String let title: String
let extra: String?
let action: () -> Void let action: () -> Void
let purchaseState: PaywallView.PurchaseView.PurchaseState? let purchaseState: PaywallView.PurchaseView.PurchaseState?
var body: some View { var body: some View {
VStack(alignment: .leading) { actionButton
actionButton
.padding(.bottom, 5)
extra.map {
Text($0)
.frame(maxHeight: .infinity)
}
}.padding([.top, .bottom])
} }
} }
private typealias RowModel = (product: InAppProduct, extra: String?)
// MARK: - // MARK: -
private extension PaywallView.PurchaseView { private extension PaywallView.PurchaseView {
var productsSection: some View { var featuresSection: some View {
Section {
ForEach(features, id: \.productIdentifier) { product in
VStack(alignment: .leading) {
Text(product.localizedTitle)
.themeCellTitleStyle()
if product.localizedDescription != product.localizedTitle {
Text(product.localizedDescription)
.themeCellSubtitleStyle()
.themeSecondaryTextStyle()
}
}
}
}
}
var purchaseSection: some View {
Section { Section {
if !productManager.isRefreshingProducts { if !productManager.isRefreshingProducts {
ForEach(productRowModels, id: \.product.productIdentifier, content: productRow) ForEach(productRowModels, id: \.productIdentifier, content: productRow)
} else { } else {
ProgressView() ProgressView()
} }
restoreRow
} header: { } header: {
Text(L10n.Paywall.title) Text(L10n.Paywall.title)
} footer: { } footer: {
@ -116,13 +122,20 @@ private extension PaywallView.PurchaseView {
} }
} }
func productRow(_ model: RowModel) -> some View { var restoreSection: some View {
Section {
restoreRow
} footer: {
Text(L10n.Paywall.Items.Restore.description)
}
}
func productRow(_ product: InAppProduct) -> some View {
PurchaseRow( PurchaseRow(
product: model.product, product: product,
title: model.product.localizedTitle, title: product.localizedTitle,
extra: model.extra,
action: { action: {
purchaseProduct(model.product) purchaseProduct(product)
}, },
purchaseState: purchaseState purchaseState: purchaseState
) )
@ -131,7 +144,6 @@ private extension PaywallView.PurchaseView {
var restoreRow: some View { var restoreRow: some View {
PurchaseRow( PurchaseRow(
title: L10n.Paywall.Items.Restore.title, title: L10n.Paywall.Items.Restore.title,
extra: L10n.Paywall.Items.Restore.description,
action: restorePurchases, action: restorePurchases,
purchaseState: purchaseState purchaseState: purchaseState
) )
@ -139,20 +151,6 @@ private extension PaywallView.PurchaseView {
} }
private extension PaywallView.PurchaseView { private extension PaywallView.PurchaseView {
var skFeature: InAppProduct? {
guard let feature = feature else {
return nil
}
return productManager.product(withIdentifier: feature)
}
var skPlatformVersion: InAppProduct? {
#if targetEnvironment(macCatalyst)
productManager.product(withIdentifier: .fullVersion_macOS)
#else
productManager.product(withIdentifier: .fullVersion_iOS)
#endif
}
// hide full version if already bought the other platform version // hide full version if already bought the other platform version
var skFullVersion: InAppProduct? { var skFullVersion: InAppProduct? {
@ -168,43 +166,18 @@ private extension PaywallView.PurchaseView {
return productManager.product(withIdentifier: .fullVersion) return productManager.product(withIdentifier: .fullVersion)
} }
var platformVersionExtra: [String] { var features: [InAppProduct] {
productManager.featureProducts(excluding: [ productManager.featureProducts(excluding: {
.fullVersion, $0 == .fullVersion || $0.isPlatformVersion
.fullVersion_iOS, })
.fullVersion_macOS .sorted {
]).map { $0.localizedTitle.lowercased() < $1.localizedTitle.lowercased()
$0.localizedTitle
}.sorted {
$0.lowercased() < $1.lowercased()
} }
} }
var fullVersionExtra: [String] { var productRowModels: [InAppProduct] {
productManager.featureProducts(including: [ [skFullVersion]
.fullVersion_iOS, .compactMap { $0 }
.fullVersion_macOS
]).map {
$0.localizedTitle
}.sorted {
$0.lowercased() < $1.lowercased()
}
}
var productRowModels: [RowModel] {
var models: [RowModel] = []
skPlatformVersion.map {
let extra = platformVersionExtra.joined(separator: "\n")
models.append(($0, extra))
}
skFullVersion.map {
let extra = fullVersionExtra.joined(separator: "\n")
models.append(($0, extra))
}
skFeature.map {
models.append(($0, nil))
}
return models
} }
} }

View File

@ -132,12 +132,12 @@ public final class ProductManager: NSObject, ObservableObject {
inApp.product(withIdentifier: identifier) inApp.product(withIdentifier: identifier)
} }
public func featureProducts(including: [LocalProduct]) -> [InAppProduct] { public func featureProducts(including: (LocalProduct) -> Bool) -> [InAppProduct] {
inApp.products().filter { inApp.products().filter {
guard let p = LocalProduct(rawValue: $0.productIdentifier) else { guard let p = LocalProduct(rawValue: $0.productIdentifier) else {
return false return false
} }
guard including.contains(p) else { guard including(p) else {
return false return false
} }
guard p.isFeature else { guard p.isFeature else {
@ -147,12 +147,12 @@ public final class ProductManager: NSObject, ObservableObject {
} }
} }
public func featureProducts(excluding: [LocalProduct]) -> [InAppProduct] { public func featureProducts(excluding: (LocalProduct) -> Bool) -> [InAppProduct] {
inApp.products().filter { inApp.products().filter {
guard let p = LocalProduct(rawValue: $0.productIdentifier) else { guard let p = LocalProduct(rawValue: $0.productIdentifier) else {
return false return false
} }
guard !excluding.contains(p) else { guard !excluding(p) else {
return false return false
} }
guard p.isFeature else { guard p.isFeature else {