Files
upstream-affine/packages/frontend/apps/ios/App/App/Onboarding/AppPaywallViewController.swift
upstream-mirror 702cc698a7
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
snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
2026-09-16 04:01:20 +05:00

237 lines
6.5 KiB
Swift

import AffinePaywall
import AffineResources
import SwiftUI
import UIKit
import WebKit
final class AppPaywallViewController: UIViewController {
var initialPlan: AppPaywallPlan = .pro
weak var bindWebView: WKWebView?
var onClose: (() -> Void)?
var onPurchaseCompleted: (() -> Void)?
private lazy var bridge = NativePaywallBridge(initialPlan: initialPlan.planKind)
private var hostingController: UIHostingController<AppPaywallRootView>?
private var prepareTask: Task<Void, Never>?
private var actionTask: Task<Void, Never>?
private static var cachedUserInterfaceStyle: UIUserInterfaceStyle {
switch UserDefaults.standard.string(forKey: AffineThemeStorage.modeKey) {
case "dark":
return .dark
case "light":
return .light
default:
return .unspecified
}
}
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = Self.cachedUserInterfaceStyle
updateBackgroundColor()
bridge.bind(webView: bindWebView)
let rootView = AppPaywallRootView(
bridge: bridge,
initialPlan: initialPlan,
onPurchase: { [weak self] plan in
self?.purchase(plan: plan)
},
onRestorePurchases: { [weak self] in
self?.restorePurchases()
},
onClose: { [weak self] in
self?.closePaywall()
}
)
let hostingController = UIHostingController(rootView: rootView)
hostingController.view.backgroundColor = .clear
self.hostingController = hostingController
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
hostingController.didMove(toParent: self)
prepareTask = Task { @MainActor [weak self] in
await self?.prepareBridge()
}
}
deinit {
prepareTask?.cancel()
actionTask?.cancel()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) else { return }
updateBackgroundColor()
}
private func updateBackgroundColor() {
view.backgroundColor = UIColor(named: "OnboardingBackground") ?? .systemBackground
}
@MainActor
private func prepareBridge() async {
do {
try await bridge.prepare()
} catch {
bridge.errorMessage = error.localizedDescription
}
}
private func purchase(plan: AppPaywallPlan) {
actionTask?.cancel()
actionTask = Task { @MainActor [weak self] in
guard let self else { return }
bridge.selectPlan(plan.planKind)
do {
let completed = try await bridge.purchaseSelectedPlan()
guard completed else { return }
guard try await hasActiveSubscription(for: plan) else {
bridge.errorMessage = String(localized: "Your purchase is still being processed. Please try again in a moment.")
return
}
dismiss(animated: true) { [weak self] in
self?.onPurchaseCompleted?()
}
} catch is CancellationError {
return
} catch {
bridge.errorMessage = error.localizedDescription
}
}
}
private func restorePurchases() {
actionTask?.cancel()
actionTask = Task { @MainActor [weak self] in
guard let self else { return }
do {
try await bridge.restorePurchases()
guard try await hasActiveSubscription(for: initialPlan) else {
bridge.errorMessage = String(localized: "No active purchases were found to restore.")
return
}
dismiss(animated: true) { [weak self] in
self?.onPurchaseCompleted?()
}
} catch is CancellationError {
return
} catch {
bridge.errorMessage = error.localizedDescription
}
}
}
private func hasActiveSubscription(for plan: AppPaywallPlan) async throws -> Bool {
guard let webView = bindWebView else {
throw NSError(
domain: "AppPaywallViewController",
code: -1,
userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing required information")]
)
}
switch plan {
case .ai:
return try await PaywallAuthGuard.hasAISubscription(in: webView)
case .pro, .lite:
return try await PaywallAuthGuard.hasProSubscription(in: webView)
}
}
private func closePaywall() {
actionTask?.cancel()
dismiss(animated: true) { [weak self] in
self?.onClose?()
}
}
}
enum AppPaywallPlan: String, CaseIterable {
case pro
case lite
case ai
var planKind: NativePaywallPlanKind {
switch self {
case .pro:
.pro
case .lite:
.lite
case .ai:
.ai
}
}
var headerName: String {
switch self {
case .pro: "Pro"
case .lite: "LITE"
case .ai: "AFFINE AI"
}
}
var description: String {
switch self {
case .pro: "Keep your knowledge available everywhere."
case .lite: "For people who want their workspace available everywhere."
case .ai: "For people who want to create, organize faster with AI."
}
}
var buttonTitle: String {
switch self {
case .pro: "Continue with Pro"
case .lite: "Continue with Lite"
case .ai: "Continue with AI"
}
}
var features: [String] {
switch self {
case .pro:
[
"Upload files larger than 10 MB",
"Sync docs and boards across all devices",
"Access your workspace on Mac, Windows, Linux, Web, iPhone, and Android",
"Secure cloud backup for your content",
"Everything stays up to date, wherever you work",
]
case .lite:
[
"Sync docs and boards across all devices",
"Access AFFiNE on Mac, Windows, Linux, Web, iPhone, and Android",
"Upload files larger than 10 MB",
"Secure cloud backup for your content",
"Pick up where you left off, anytime",
]
case .ai:
[
"Generate articles, notes, and content in seconds",
"Rewrite, improve, and translate your writing",
"Turn ideas into visuals, mind maps, and presentations",
"Chat with your documents and knowledge",
"AI-powered organization and insights",
]
}
}
}