Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
170 lines
4.9 KiB
Swift
170 lines
4.9 KiB
Swift
import AffinePaywall
|
|
import AffineResources
|
|
import SwiftUI
|
|
|
|
struct AppPaywallRootView: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
@ObservedObject var bridge: NativePaywallBridge
|
|
let initialPlan: AppPaywallPlan
|
|
let onPurchase: (AppPaywallPlan) -> Void
|
|
let onRestorePurchases: () -> Void
|
|
let onClose: () -> Void
|
|
|
|
private var palette: AppPaywallPalette {
|
|
AppPaywallPalette(colorScheme: colorScheme)
|
|
}
|
|
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
let layout = AppPaywallLayout(
|
|
size: geometry.size,
|
|
safeAreaInsets: geometry.safeAreaInsets
|
|
)
|
|
|
|
ZStack {
|
|
OnboardingBackground(isIntroPage: false)
|
|
palette.backgroundOverlay.ignoresSafeArea()
|
|
|
|
ScrollView(.vertical, showsIndicators: false) {
|
|
AppPaywallPage(
|
|
bridge: bridge,
|
|
initialPlan: initialPlan,
|
|
layout: layout,
|
|
palette: palette,
|
|
onPurchase: onPurchase,
|
|
onRestorePurchases: onRestorePurchases,
|
|
onClose: onClose
|
|
)
|
|
.frame(minHeight: layout.pageMinHeight, alignment: .top)
|
|
}
|
|
.scrollBounceBehavior(.basedOnSize)
|
|
}
|
|
.frame(width: geometry.size.width, height: geometry.size.height)
|
|
.foregroundStyle(palette.primaryText)
|
|
}
|
|
.alert(
|
|
"Paywall",
|
|
isPresented: Binding(
|
|
get: { bridge.errorMessage != nil },
|
|
set: { isPresented in
|
|
if !isPresented {
|
|
bridge.errorMessage = nil
|
|
}
|
|
}
|
|
)
|
|
) {
|
|
Button("OK", role: .cancel) {}
|
|
} message: {
|
|
Text(bridge.errorMessage ?? "")
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct AppPaywallPage: View {
|
|
@Environment(\.openURL) private var openURL
|
|
|
|
@ObservedObject var bridge: NativePaywallBridge
|
|
let initialPlan: AppPaywallPlan
|
|
let layout: AppPaywallLayout
|
|
let palette: AppPaywallPalette
|
|
let onPurchase: (AppPaywallPlan) -> Void
|
|
let onRestorePurchases: () -> Void
|
|
let onClose: () -> Void
|
|
|
|
private let plans = AppPaywallPlan.allCases
|
|
@State private var selectedPlan: AppPaywallPlan
|
|
|
|
init(
|
|
bridge: NativePaywallBridge,
|
|
initialPlan: AppPaywallPlan,
|
|
layout: AppPaywallLayout,
|
|
palette: AppPaywallPalette,
|
|
onPurchase: @escaping (AppPaywallPlan) -> Void,
|
|
onRestorePurchases: @escaping () -> Void,
|
|
onClose: @escaping () -> Void
|
|
) {
|
|
self.bridge = bridge
|
|
self.initialPlan = initialPlan
|
|
self.layout = layout
|
|
self.palette = palette
|
|
self.onPurchase = onPurchase
|
|
self.onRestorePurchases = onRestorePurchases
|
|
self.onClose = onClose
|
|
_selectedPlan = State(initialValue: initialPlan)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: layout.sectionSpacing) {
|
|
Text("Choose your plan")
|
|
.font(.system(size: layout.titleFontSize, weight: .bold))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
Picker("Plan", selection: $selectedPlan) {
|
|
ForEach(plans, id: \.self) { plan in
|
|
Text(LocalizedStringKey(plan.headerName)).tag(plan)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
|
|
AppPaywallCard(
|
|
plan: selectedPlan,
|
|
priceInfo: bridge.priceInfo(for: selectedPlan.planKind),
|
|
layout: layout,
|
|
palette: palette
|
|
)
|
|
|
|
Text("Subscriptions renew automatically until canceled.")
|
|
.font(.footnote)
|
|
.foregroundStyle(palette.secondaryText)
|
|
.multilineTextAlignment(.center)
|
|
|
|
PrimaryButton(
|
|
title: selectedPlan.buttonTitle,
|
|
isLoading: bridge.isLoading || bridge.isProcessing,
|
|
isEnabled: bridge.isReady,
|
|
fontSize: layout.buttonFontSize,
|
|
height: layout.buttonHeight
|
|
) {
|
|
triggerPaywallHaptic()
|
|
onPurchase(selectedPlan)
|
|
}
|
|
|
|
Button("Not now", action: onClose)
|
|
.font(.system(size: 17, weight: .medium))
|
|
.foregroundStyle(palette.secondaryText)
|
|
.frame(maxWidth: .infinity, minHeight: 44)
|
|
.buttonStyle(.plain)
|
|
|
|
AppPaywallLegalLinks(
|
|
palette: palette,
|
|
onOpenTerms: { openLegalURL("https://affine.pro/terms") },
|
|
onOpenPrivacy: { openLegalURL("https://affine.pro/privacy") },
|
|
onOpenSubscriptionTerms: { openLegalURL("https://affine.pro/terms/#subscription") },
|
|
onRestore: onRestorePurchases
|
|
)
|
|
}
|
|
.padding(.horizontal, layout.horizontalPadding)
|
|
.padding(.top, layout.topPadding)
|
|
.padding(.bottom, layout.bottomPadding)
|
|
.frame(maxWidth: .infinity, minHeight: layout.pageMinHeight, alignment: .top)
|
|
.onAppear {
|
|
bridge.selectPlan(initialPlan.planKind)
|
|
}
|
|
.onChange(of: selectedPlan) { plan in
|
|
bridge.selectPlan(plan.planKind)
|
|
}
|
|
}
|
|
|
|
private func openLegalURL(_ string: String) {
|
|
guard let url = URL(string: string) else { return }
|
|
openURL(url)
|
|
}
|
|
|
|
private func triggerPaywallHaptic() {
|
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
|
generator.prepare()
|
|
generator.impactOccurred()
|
|
}
|
|
}
|