Skip to content

Conversation

Lakr233
Copy link
Collaborator

@Lakr233 Lakr233 commented Sep 30, 2025

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] [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 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] [2]

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] [2]

Other Minor Improvements

  • Improved constructor formatting for PayWallPlugin for readability.

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.

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.
Copy link
Contributor

coderabbitai bot commented Sep 30, 2025

Walkthrough

Removes 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

Cohort / File(s) Summary
Debug UI removal
packages/frontend/apps/ios/App/App/AffineViewController.swift
Removed shake-detection override, debug menu helper, and in-controller paywall presentation utilities/extensions.
Plugin passes web context
packages/frontend/apps/ios/App/App/Plugins/PayWall/PayWallPlugin.swift
Formatting adjusted; Paywall.presentWall call now passes bindWebContext: self.webView.
Paywall API: bind WKWebView
packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift
Added WebKit import and new parameter bindWebContext: WKWebView? to presentWall; binds context to ViewModel when provided.
ViewModel: web binding & entitlement flow
.../AffinePaywall/Sources/AffinePaywall/Model/ViewModel.swift, .../AffinePaywall/Sources/AffinePaywall/Model/ViewModel+Action.swift
Added associatedWebContext (weak), bind(context:), storePurchasedItems, externalPurchasedItems, and computed purchasedItems. Fetches store entitlements into storePurchasedItems, fetches external entitlements via JS (window.getSubscriptionState) into externalPurchasedItems, decodes values, and updates web context on dismiss before closing.
Public visibility for SKUnitCategory
.../AffinePaywall/Sources/AffinePaywall/Model/SKUnitCategory.swift
Made SKUnitCategory, its id, and its title extension public.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

app:core

Suggested reviewers

  • darkskygit
  • EYHN
  • akumatus

Poem

I thump my paws—no shake-to-show today,
A web-bound wall hops in to play.
Store bits and web bits join as one,
I bind, I check, then say we're done.
Hop, bind, dismiss — a rabbit's run. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “feat(ios): sync paywall with external purchased items” accurately and concisely reflects the primary change of integrating the paywall with external subscription state, aligns with conventional commit style, and provides clear context for teammates reviewing the history.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch lakr-paywall-subscription-status

📜 Recent 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 00b0866 and c88b733.

📒 Files selected for processing (1)
  • packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/frontend/apps/ios/App/Packages/AffinePaywall/Sources/AffinePaywall/Paywall.swift
⏰ 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)
  • GitHub Check: Unit Test (5)
  • GitHub Check: Unit Test (4)
  • GitHub Check: Unit Test (2)
  • GitHub Check: Unit Test (3)
  • GitHub Check: Unit Test (1)
  • GitHub Check: Native Unit Test
  • GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
  • GitHub Check: E2E Test (9)
  • GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
  • GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
  • GitHub Check: E2E Test (7)
  • GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
  • GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
  • GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
  • GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
  • GitHub Check: Build @affine/electron renderer
  • GitHub Check: E2E Test (10)
  • GitHub Check: y-octo binding test on x86_64-apple-darwin
  • GitHub Check: E2E Test (8)
  • GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
  • GitHub Check: E2E BlockSuite Test (10)
  • GitHub Check: E2E Test (6)
  • GitHub Check: E2E Test (4)
  • GitHub Check: E2E Test (5)
  • GitHub Check: E2E Test (3)
  • GitHub Check: E2E Test (2)
  • GitHub Check: fuzzing
  • GitHub Check: E2E BlockSuite Test (8)
  • GitHub Check: E2E Test (1)
  • GitHub Check: E2E BlockSuite Test (4)
  • GitHub Check: E2E BlockSuite Test (6)
  • GitHub Check: E2E BlockSuite Test (9)
  • GitHub Check: E2E BlockSuite Test (3)
  • GitHub Check: E2E Mobile Test (2)
  • GitHub Check: E2E Mobile Test (3)
  • GitHub Check: E2E BlockSuite Test (5)
  • GitHub Check: E2E BlockSuite Test (1)
  • GitHub Check: E2E BlockSuite Test (2)
  • GitHub Check: E2E BlockSuite Test (7)
  • GitHub Check: E2E Mobile Test (1)
  • GitHub Check: E2E Mobile Test (4)
  • GitHub Check: E2E Mobile Test (5)
  • GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
  • GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
  • GitHub Check: loom thread test
  • GitHub Check: Run native tests
  • GitHub Check: Analyze (javascript, blocksuite)
  • GitHub Check: Analyze (typescript, affine)
  • GitHub Check: Analyze (typescript, blocksuite)
  • GitHub Check: Analyze (javascript, affine)
  • GitHub Check: Typecheck
  • GitHub Check: Lint

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.

❤️ Share
🧪 Early access (Sonnet 4.5): enabled

We 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:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 4b3ebd8 and 00b0866.

📒 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 in externalPurchasedItems remaining empty when the web context isn't bound.

Confirm whether:

  1. Graceful degradation (empty external items) is the intended behavior
  2. 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 and externalPurchasedItems 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, and private(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 new Paywall.Configuration API. The enum's Identifiable 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.

Copy link

codecov bot commented Sep 30, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 56.75%. Comparing base (856b69e) to head (1914ea7).
⚠️ Report is 1 commits behind head on canary.

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     
Flag Coverage Δ
server-test 77.83% <ø> (-1.19%) ⬇️
unittest 31.99% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@darkskygit darkskygit added this pull request to the merge queue Oct 2, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to no response for status checks Oct 2, 2025
@darkskygit darkskygit added this pull request to the merge queue Oct 2, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to no response for status checks Oct 2, 2025
@EYHN EYHN added this pull request to the merge queue Oct 3, 2025
Merged via the queue into canary with commit 7d0b8aa Oct 3, 2025
109 checks passed
@EYHN EYHN deleted the lakr-paywall-subscription-status branch October 3, 2025 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

3 participants