Skip to content

[iOS] FreemiumDBPUserStateManager implementation#4471

Open
jozsef-vesza wants to merge 20 commits intojozsef/freemium-pir-auth-gatefrom
jozsef/freemium-dbp-user-state-manager
Open

[iOS] FreemiumDBPUserStateManager implementation#4471
jozsef-vesza wants to merge 20 commits intojozsef/freemium-pir-auth-gatefrom
jozsef/freemium-dbp-user-state-manager

Conversation

@jozsef-vesza
Copy link
Copy Markdown
Contributor

@jozsef-vesza jozsef-vesza commented Apr 17, 2026

Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1210938375848294
Tech Design URL:
CC: @THISISDINOSAUR

Description

Adds the concrete DefaultFreemiumDBPUserStateManager on iOS and wires it into the PIR flow. Completing this subtask activates the auth-gate relaxation in #4461: didActivate now flips to true at profile-save, so freemium users can actually run scans.

Production changes (stacked on top of jozsef/freemium-pir-auth-gate):

  • Protocol expansion. FreemiumDBPUserStateManaging adds read/write methods.
  • DefaultFreemiumDBPUserStateManager UserDefaults-backed default implementation.
  • BrokerProfileJobEventsHandler will now update the state manager when profile is saved.
  • handleScanCompletion() helper on DataBrokerProtectionIOSManager. Deduplicates the scan-completion logic across both scan paths and records the freemium scan result. Skips the write if the DB read fails, so a transient error can't permanently stamp the wrong result.

Out of scope: recordSubscriptionUpgradeIfNeeded() exists on the protocol and manager but has no production caller yet. This will come later when working on pixels.

Testing Steps

Green CI, there are no entry points yet.

Impact and Risks

Low: No entry points + guarded by feature flag.

What could go wrong?

  • DB read error on the first scan. Freemium result is recorded only once, so a throw during the very first scan could lock in a wrong "no matches" result forever. Handled by skipping the write on error; the next successful scan still gets to record.
  • Concurrent scan completions across both paths. Two scan completions could race to record the result. The state manager serializes all reads and writes through a lock, so the first one wins.

Quality Considerations

  • Persistence. UserDefaults .dbp suite, key namespace ios.browser.freemium.dbp.* — iOS-scoped so there can be no collision with macOS's macos.browser.freemium.dbp.* keys.
  • Concurrency. All reads and writes pass through one NSLock. Lock acquisition happens after the async auth check, so the lock is never held across a suspension point — avoids the Swift 6 concurrency checker error.
  • Cross-platform. iOS intentionally diverges from macOS/LocalPackages/Freemium/FreemiumDBPUserStateManager.swift: simpler enum (FreemiumFirstScanResult) instead of macOS's FreemiumDBPMatchResults struct; drops macOS-only notification/dismissal flags. Spec call: RMF on iOS can't template counts, so the struct adds no value there.
  • Testability. 34 new tests: 20 state-manager unit tests, 5 handler integration tests, 9 scan-completion integration tests including cross-path and DB-error cases.

Notes to Reviewer

  • Stacked on [iOS] Relax PIR auth gates for freemium users #4461. This PR targets jozsef/freemium-pir-auth-gate. Review that one first; I'll retarget this to main once it merges.
  • Subscription-upgrade caller is intentionally deferred to the attribution-pixel subtask.

Note

Medium Risk
Introduces new UserDefaults-backed freemium state that affects whether unauthenticated users can run scans and records first-scan results; mistakes could permanently persist incorrect state or change scan behavior across sessions.

Overview
Adds a concrete DefaultFreemiumDBPUserStateManager (UserDefaults + lock) and expands FreemiumDBPUserStateManaging to persist freemium activation, timestamps, and a first-scan result (noMatches vs matchesFound).

Wires this state into production by instantiating it in DBPService, passing it through DataBrokerProtectionIOSManagerProvider, and updating BrokerProfileJobEventsHandler to record activation on .profileSaved.

Refactors iOS scan completion into a shared handleScanCompletion() used by both immediate scans and continued-processing scans; it triggers the existing matches-found event and records the first-scan result, but skips recording if database.hasMatches() throws to avoid permanently storing a wrong outcome. Adds extensive unit/integration tests and enhances repository mocks to control hasMatches() return/throw behavior.

Reviewed by Cursor Bugbot for commit 424b9f2. Bugbot is set up for automated code reviews on this repo. Configure here.

jozsef-vesza and others added 19 commits April 17, 2026 12:04
…erStateManager

Extracts lock/unlock calls from async methods into private synchronous
helpers to silence Swift 6 unavailability warnings on NSLock in async contexts.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Also make DisabledFreemiumDBPUserStateManager public (with explicit init
and public member/method declarations) so it can be used as a default
argument value in the public BrokerProfileJobEventsHandler initializer.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
A transient database read error must not permanently record `.noMatches`:
`firstScanResult` is first-write-wins, so a bogus value would stick forever.
`handleScanCompletion` now returns early on error; a later successful scan
records the correct outcome.

Tests: strengthened authenticated cases to prove the completion callback
actually ran (via `firstScanCompletedAndMatchesFoundFired`) rather than
relying on a time-based wait against `firstScanResult`, which the auth
case would satisfy by default. Added a test for the new DB-error path.
@jozsef-vesza jozsef-vesza requested a review from quanganhdo April 17, 2026 14:06
@jozsef-vesza jozsef-vesza marked this pull request as ready for review April 17, 2026 14:06
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 17, 2026

Warnings
⚠️ PR has 848 lines of added code (excluding Xcode projects and assets). Consider splitting into smaller PRs if possible.

Generated by 🚫 dangerJS against 424b9f2

…est files

- Restores SharedPackages/DataBrokerProtectionCore/Package.resolved to the
  auth-gate baseline (the content-scope-scripts / privacy-dashboard bumps
  already live in the earlier CSS-bump commit on main; they don't belong here).
- Drops the stray '//  DuckDuckGo' line from three new test file headers;
  SwiftLint's file_header rule flagged them for not matching the neighbor
  pattern.
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 424b9f2. Configure here.

Task {
await freemiumUserStateManager.recordProfileSavedIfNeeded()
onComplete(nil)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fire-and-forget Task may delay didActivate write

Medium Severity

The .profileSaved handler wraps recordProfileSavedIfNeeded() in an unstructured fire-and-forget Task. In production, eventsHandler.fire(.profileSaved) is called without an onComplete handler, so the Task is never awaited. The caller (saveProfileAndPrepareForInitialScans) proceeds immediately to refreshFreeScanState() and then startImmediateScanOperations(). Since didActivate is set inside the unawaited Task, there's a race window where canRunFreemiumScans (which reads didActivate) evaluates to false even though the profile was just saved. The PR description states this flag "activates the auth-gate relaxation" so freemium users can run scans — if the flag isn't set in time, the scan could be blocked.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 424b9f2. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant