-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
feat(ios): sync paywall with external purchased items #13681
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Added support for binding a WKWebView context to the paywall ViewModel to fetch external subscription state via JavaScript. Updated PayWallPlugin and Paywall presentation logic to pass and use the web context. Refactored purchased items handling to merge store and external sources, and improved subscription decoding logic. Removed debug menu from AFFiNEViewController.
Adds an async JavaScript call to update the subscription state in the associated web context when the dismiss function is triggered. Logs success or error for better debugging.
WalkthroughRemoves debug-only paywall UI from AffineViewController, expands Paywall.presentWall to accept and bind a WKWebView, exposes SKUnitCategory publicly, and updates the Paywall ViewModel to bind a web context, split/store external entitlements, compute their union, and call JS to update subscription state on dismiss. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Plugin as PayWallPlugin
participant Paywall as Paywall
participant VM as Paywall.ViewModel
participant WK as WKWebView
participant SK as App Store
User->>Plugin: trigger paywall
Plugin->>Paywall: presentWall(controller, bindWebContext: webView, type)
Paywall->>VM: create ViewModel
alt context provided
Paywall->>VM: bind(context: WKWebView)
Note right of VM: VM.associatedWebContext = weak WK
end
VM->>SK: fetch App Store entitlements
SK-->>VM: entitlements
VM->>VM: storePurchasedItems = result
alt has web context
VM->>WK: evaluate JS window.getSubscriptionState()
WK-->>VM: subscription info
VM->>VM: externalPurchasedItems = decode(web info)
end
VM->>VM: purchasedItems = storePurchasedItems ∪ externalPurchasedItems
User->>VM: dismiss
par update web state
VM->>WK: evaluate JS to update subscription state
and close UI
VM-->>Paywall: dismiss controller
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro Disabled knowledge base sources:
📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (52)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🧪 Early access (Sonnet 4.5): enabledWe are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience. Note:
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (3)
packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift (3)
9-11
: Consider keeping simple initializers on one line.The initializer has only one parameter and doesn't appear long enough to warrant line wrapping. Single-parameter initializers are typically kept on one line for better readability.
- init( - associatedController: UIViewController? - ) { + init(associatedController: UIViewController?) { controller = associatedController super.init() }
31-37
: Add capture list to prevent potential retain cycle.The closure captures
self
implicitly. While the plugin's lifecycle is managed by Capacitor, it's best practice to use[weak self]
in async closures to prevent potential retain cycles and clearly express intent.As per coding guidelines.
- DispatchQueue.main.async { + DispatchQueue.main.async { [weak self] in + guard let self else { return } Paywall.presentWall( toController: controller, bindWebContext: self.webView, type: type ) }
29-30
: Remove or address outdated TODO and debug print.The TODO comment appears outdated—the paywall type is already being handled in
Paywall.presentWall
(see the related snippet from Paywall.swift where "pro" and "ai" types are switched on). The print statement should use proper logging or be removed for production.- // TODO: GET TO KNOW THE PAYWALL TYPE - print("[*] showing paywall of type: \(type)")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
packages/frontend/apps/ios/App/App/AffineViewController.swift
(0 hunks)packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift
(2 hunks)packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift
(1 hunks)packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift
(3 hunks)packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift
(3 hunks)packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift
(1 hunks)
💤 Files with no reviewable changes (1)
- packages/frontend/apps/ios/App/App/AffineViewController.swift
🧰 Additional context used
📓 Path-based instructions (3)
packages/frontend/apps/ios/App/**/*.swift
📄 CodeRabbit inference engine (packages/frontend/apps/ios/AGENTS.md)
packages/frontend/apps/ios/App/**/*.swift
: Use 2-space indentation in Swift files
Place opening braces on the same line in Swift
Use a single space around operators and commas in Swift
Use PascalCase for types and camelCase for properties/methods in Swift
Prefer @observable macro over ObservableObject/@published
Prefer Swift concurrency: async/await, Task, actor, and @mainactor where appropriate
Use result builders for declarative APIs when appropriate
Break long property wrapper declarations across lines
Use opaque result types (some) for protocol-returning APIs when suitable
Favor early returns to reduce nesting
Use guard statements for optional unwrapping
Ensure single responsibility per type/extension
Prefer value types over reference types when feasible
Use Result for typed errors where appropriate
Use throws/try to propagate errors
Use optional chaining with guard let/if let for safe unwrapping
Define and use typed error enums/structures
Avoid protocol-oriented design unless necessary
Prefer dependency injection over singletons
Favor composition over inheritance
Use Factory/Repository patterns where appropriate
Use assert() for development-time invariants and assertionFailure() for unreachable code; use precondition() for fatal errors
Manage memory correctly: use weak to break cycles, unowned when guaranteed non-nil, capture lists in closures, and deinit for cleanup
Files:
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift
packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift
packages/frontend/apps/ios/App/**/[A-Z]*.swift
📄 CodeRabbit inference engine (packages/frontend/apps/ios/AGENTS.md)
Name Swift files for types in PascalCase
Files:
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift
packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift
packages/frontend/apps/ios/App/**/*+*.swift
📄 CodeRabbit inference engine (packages/frontend/apps/ios/AGENTS.md)
Use '+' in Swift filenames for extensions (e.g., Type+Feature.swift)
Files:
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift
🧬 Code graph analysis (2)
packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift (2)
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift (1)
presentWall
(25-50)packages/frontend/apps/ios/App/App/AffineViewController.swift (1)
webView
(25-27)
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift (1)
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift (2)
bind
(50-52)bind
(54-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (49)
- GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
- GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
- GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
- GitHub Check: y-octo binding test on x86_64-apple-darwin
- GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
- GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
- GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
- GitHub Check: Run native tests
- GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
- GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
- GitHub Check: E2E Mobile Test (1)
- GitHub Check: Build @affine/electron renderer
- GitHub Check: E2E Test (7)
- GitHub Check: E2E Test (9)
- GitHub Check: E2E Test (10)
- GitHub Check: E2E Test (6)
- GitHub Check: E2E Test (8)
- GitHub Check: fuzzing
- GitHub Check: E2E Test (4)
- GitHub Check: E2E Test (5)
- GitHub Check: loom thread test
- GitHub Check: E2E Test (3)
- GitHub Check: E2E Test (1)
- GitHub Check: E2E Test (2)
- GitHub Check: E2E Mobile Test (4)
- GitHub Check: E2E Mobile Test (5)
- GitHub Check: E2E Mobile Test (3)
- GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
- GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
- GitHub Check: E2E Mobile Test (2)
- GitHub Check: Build AFFiNE native (aarch64-apple-darwin)
- GitHub Check: Build Server native
- GitHub Check: Build AFFiNE native (x86_64-apple-darwin)
- GitHub Check: E2E BlockSuite Test (6)
- GitHub Check: E2E BlockSuite Test (7)
- GitHub Check: E2E BlockSuite Test (8)
- GitHub Check: E2E BlockSuite Test (10)
- GitHub Check: E2E BlockSuite Test (9)
- GitHub Check: E2E BlockSuite Test (2)
- GitHub Check: E2E BlockSuite Test (3)
- GitHub Check: E2E BlockSuite Test (5)
- GitHub Check: E2E BlockSuite Test (4)
- GitHub Check: E2E BlockSuite Test (1)
- GitHub Check: Analyze (javascript, affine)
- GitHub Check: Analyze (javascript, blocksuite)
- GitHub Check: Analyze (typescript, affine)
- GitHub Check: Analyze (typescript, blocksuite)
- GitHub Check: Lint
- GitHub Check: Typecheck
🔇 Additional comments (7)
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift (2)
79-91
: Fire-and-forget JS update is appropriate.The detached task ensures the dismiss flow isn't blocked by the web context update, which is good for UX. Error handling is adequate for this non-critical operation.
120-136
: Verify intended behavior when web context is missing.The code throws an error if
associatedWebContext
is nil (lines 122-126), but this error is caught and only logged (line 135). This results inexternalPurchasedItems
remaining empty when the web context isn't bound.Confirm whether:
- Graceful degradation (empty external items) is the intended behavior
- Or if missing web context should prevent paywall presentation entirely
If graceful degradation is intentional, consider removing the explicit throw at lines 123-126 and instead skip the fetch with an early
guard let webView = await associatedWebContext else { return }
.packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift (3)
27-35
: Clean separation of store and external entitlements.The split between
storePurchasedItems
andexternalPurchasedItems
with a computed union property follows a clear pattern. The computed property correctly omits@Published
since changes to its dependencies will trigger updates.
38-38
: Correct memory management for web context.Using
weak
for the WKWebView reference prevents retain cycles, andprivate(set)
properly encapsulates the binding mechanism.
54-56
: LGTM!The binding method follows the established pattern and is appropriately main-actor-isolated.
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift (1)
10-24
: Public API exposure aligns with new configuration.Making
SKUnitCategory
and its extensions public is appropriate for the newPaywall.Configuration
API. The enum'sIdentifiable
conformance and title extension are suitable for public use.packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift (1)
26-32
: Web context binding integrates cleanly.The optional
bindWebContext
parameter maintains backward compatibility, and the binding order (context → category selection → controller) is logical.
...ntend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift
Show resolved
Hide resolved
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift
Outdated
Show resolved
Hide resolved
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## canary #13681 +/- ##
===========================================
- Coverage 79.33% 56.75% -22.58%
===========================================
Files 454 2755 +2301
Lines 60640 136846 +76206
Branches 3902 20944 +17042
===========================================
+ Hits 48106 77672 +29566
- Misses 12488 56957 +44469
- Partials 46 2217 +2171
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
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 abindWebContext
parameter, andViewModel
supports binding and using the web context). [1] [2] [3] [4]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 computedpurchasedItems
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] [2]
Codebase Cleanup
AFFiNEViewController
, streamlining the production build.API and Model Enhancements
SKUnitCategory
and its extensions public to allow broader usage across modules, and introduced a configuration struct for the paywall. [1] [2]Other Minor Improvements
PayWallPlugin
for readability.Summary by CodeRabbit