Files
AFFiNE-Mirror/packages/frontend/apps/ios/App/App/AffineViewController.swift
Lakr 7d0b8aaa81 feat(ios): sync paywall with external purchased items (#13681)
This pull request introduces significant improvements to the integration
between the paywall feature and the web context within the iOS app. The
main focus is on enabling synchronization of subscription states between
the app and the embedded web view, refactoring how purchased items are
managed, and enhancing the paywall presentation logic. Additionally,
some debug-only code has been removed for cleaner production builds.

**Paywall and Web Context Integration**

* Added support for binding a `WKWebView` context to the paywall,
allowing the paywall to communicate with the web view for subscription
state updates and retrievals (`Paywall.presentWall` now accepts a
`bindWebContext` parameter, and `ViewModel` supports binding and using
the web context).
[[1]](diffhunk://#diff-bce0a21a4e7695b7bf2430cd6b8a85fbc84124cc3be83f3288119992b7abb6cdR10-R32)
[[2]](diffhunk://#diff-cb192a424400265435cb06d86b204aa17b4e8195d9dd811580f51faeda211ff0R54-R57)
[[3]](diffhunk://#diff-cb192a424400265435cb06d86b204aa17b4e8195d9dd811580f51faeda211ff0L26-R38)
[[4]](diffhunk://#diff-1854d318d8fd8736d078f5960373ed440836263649a8193c8ee33e72a99424edL30-R36)

* On paywall dismissal, the app now triggers a JavaScript call to update
the subscription state in the web view, ensuring consistency between the
app and the web context.

**Purchased Items Refactor**

* Refactored `ViewModel` to distinguish between store-purchased items
and externally-purchased items (from the web context), and unified them
in a computed `purchasedItems` property. This improves clarity and
extensibility for handling entitlements from multiple sources.

* Added logic to fetch external entitlements by executing JavaScript in
the web view and decoding the subscription information, mapping external
plans to internal product identifiers.
[[1]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbL99-R137)
[[2]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbR169-R209)

**Codebase Cleanup**

* Removed debug-only code for shake gesture and debug menu from
`AFFiNEViewController`, streamlining the production build.

**API and Model Enhancements**

* Made `SKUnitCategory` and its extensions public to allow broader usage
across modules, and introduced a configuration struct for the paywall.
[[1]](diffhunk://#diff-742ccf0c6bafd2db6cb9795382d556fbab90b8855ff38dc340aa39318541517dL10-R17)
[[2]](diffhunk://#diff-bce0a21a4e7695b7bf2430cd6b8a85fbc84124cc3be83f3288119992b7abb6cdR10-R32)

**Other Minor Improvements**

* Improved constructor formatting for `PayWallPlugin` for readability.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- Paywall now binds to the in-app web view so web-based subscriptions
are recognized alongside App Store purchases.
- Bug Fixes
- Entitlements combine App Store and web subscription state for more
accurate display.
- Dismissing the paywall immediately updates subscription status to
reduce stale states.
  - Improved reliability when presenting the paywall.
- Chores
  - Removed debug shake menu and debug paywall options from iOS builds.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-03 07:21:41 +00:00

78 lines
2.5 KiB
Swift

import Capacitor
import Intelligents
import UIKit
class AFFiNEViewController: CAPBridgeViewController {
var intelligentsButton: IntelligentsButton?
override func viewDidLoad() {
super.viewDidLoad()
webView?.allowsBackForwardNavigationGestures = true
navigationController?.navigationBar.isHidden = true
extendedLayoutIncludesOpaqueBars = false
edgesForExtendedLayout = []
let intelligentsButton = installIntelligentsButton()
intelligentsButton.delegate = self
self.intelligentsButton = intelligentsButton
dismissIntelligentsButton()
}
override func webViewConfiguration(for instanceConfiguration: InstanceConfiguration) -> WKWebViewConfiguration {
let configuration = super.webViewConfiguration(for: instanceConfiguration)
return configuration
}
override func webView(with frame: CGRect, configuration: WKWebViewConfiguration) -> WKWebView {
super.webView(with: frame, configuration: configuration)
}
override func capacitorDidLoad() {
let plugins: [CAPPlugin] = [
AuthPlugin(),
CookiePlugin(),
HashcashPlugin(),
NavigationGesturePlugin(),
NbStorePlugin(),
PayWallPlugin(associatedController: self),
]
plugins.forEach { bridge?.registerPluginInstance($0) }
}
private var intelligentsButtonTimer: Timer?
private var isCheckingIntelligentEligibility = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
IntelligentContext.shared.webView = webView
navigationController?.setNavigationBarHidden(false, animated: animated)
let timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [weak self] _ in
self?.checkEligibilityOfIntelligent()
}
intelligentsButtonTimer = timer
RunLoop.main.add(timer, forMode: .common)
}
private func checkEligibilityOfIntelligent() {
guard !isCheckingIntelligentEligibility else { return }
assert(intelligentsButton != nil)
guard intelligentsButton?.isHidden ?? false else { return } // already eligible
isCheckingIntelligentEligibility = true
IntelligentContext.shared.webView = webView
IntelligentContext.shared.preparePresent { [self] result in
DispatchQueue.main.async {
defer { self.isCheckingIntelligentEligibility = false }
switch result {
case .failure: break
case .success:
self.presentIntelligentsButton()
}
}
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
intelligentsButtonTimer?.invalidate()
}
}