Skip to content

Conversation

requilence
Copy link
Contributor

Refactored membership data management to work with new middleware caching behavior for faster app startup.

Problem

Middleware now returns cached membership/tiers data immediately (with noCache: false) instead of blocking on network calls. App startup was slow because we were forcing network calls with noCache: true.

Changes

Single Source of Truth Architecture

  • MembershipStatusStorage now owns all membership data (status + tiers)
  • Added tiersPublisher and currentTiers to protocol
  • UI subscribes to storage publishers instead of calling service directly

Fast Startup

  • Changed startSubscription() to use noCache: false (uses middleware in-memory cache)
  • Added refreshMembership() with noCache: true for explicit refresh after IAP purchases

Event Handling

  • Added membershipTiersUpdate event handling
  • membershipUpdate: Updates status using event data + existing tiers
  • membershipTiersUpdate: Builds all tiers from event data
  • Simple guard skips status update if tiers not available yet (edge case)

Tier Filtering

  • Added isTest and iosProductID properties to MembershipTier model
  • Storage builds ALL tiers without filtering
  • UI filters tiers when rendering based on:
    • Test tiers (via FeatureFlags.membershipTestTiers)
    • iOS compatibility (has iosProductID OR is current user tier)

Files Modified

  • MembershipStatusStorage.swift - Core storage implementation
  • MembershipStatusStorageProtocol - Added tiers publishers
  • MembershipCoordinatorModel.swift - Subscribe to storage, filter tiers in UI
  • MembershipTier.swift - Added isTest, iosProductID properties
  • MembershipModelBuilder.swift - Pass new properties when building tiers
  • MembershipTier+Mocks.swift - Updated mocks
  • MembershipStatusStorageMock.swift - Added tiers support

refactor the membership storage and event handling
@requilence requilence requested a review from a team as a code owner October 17, 2025 14:01
Copy link

claude bot commented Oct 17, 2025

Bugs/Issues

Race condition in event handling (MembershipStatusStorage.swift:73-91, 93-102)

  • Both membershipUpdate and membershipTiersUpdate event handlers create new Task blocks inside the for-loop
  • These tasks run concurrently without synchronization, creating potential race conditions when updating _status and _tiers
  • If multiple events arrive in quick succession, the order of updates to @published properties becomes non-deterministic
  • Fix: Remove the Task wrappers since handle(events:) is already called from a @mainactor task (line 64)

Missing tiers refresh (MembershipStatusStorage.swift:55-58)

  • refreshMembership() only refreshes status with noCache: true, but doesn't refresh tiers
  • If tiers data is stale after IAP purchase, UI won't show updated tier information
  • Fix: Add tiers refresh call with noCache: true

Silent error swallowing (MembershipCoordinatorModel.swift:65-68)

  • retryLoadTiers() calls refreshMembership() but doesn't check if it succeeds
  • If refresh fails, showTiersLoadingError remains false, leaving user with no feedback
  • Fix: Catch errors from refreshMembership() and set showTiersLoadingError = true on failure

Best Practices

Debug print statements in production (MembershipStatusStorage.swift:76, 87, 89)

  • Three print() statements will appear in production logs
  • Use proper logging framework or anytypeAssertionFailure() for errors per project conventions

Missing error handling (MembershipStatusStorage.swift:55-58)

  • refreshMembership() silently falls back to old _status on error using try? fallback pattern
  • No logging or analytics tracking when refresh fails after IAP purchase
  • Consider logging failures for debugging

Summary: 🚨 Major Issues - Race conditions in event handling, incomplete refresh logic, and silent error swallowing need fixing

Comment on lines +24 to +29
var tiers: [MembershipTier] {
let currentTierId = userMembership.tier?.type.id ?? 0
return allTiers
.filter { FeatureFlags.membershipTestTiers || !$0.isTest }
.filter { !$0.iosProductID.isEmpty || $0.type.id == currentTierId }
}
Copy link
Member

Choose a reason for hiding this comment

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

If we use this on the UI level, it will really affect performance. It's better to make this a published property and discard @Published private var allTiers: [MembershipTier] = []
because seems like it's not used anymore.

Also naming is really ambiguous. We have allTiers and tiers with no significant distinction and ability to understand what is what.

}

case .membershipTiersUpdate(let update):
Task {
Copy link
Member

Choose a reason for hiding this comment

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

Task created inside main actor will still be called on the main actor. If you want to do it in the backgroind you need to call Task.detached

Comment on lines +105 to +106
public let isTest: Bool
public let iosProductID: String
Copy link
Member

Choose a reason for hiding this comment

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

We already have this properties available inside Anytype_Model_MembershipTierData I am not sure we need to introduce them here once again

)
_status.tier.flatMap { AnytypeAnalytics.instance().logChangePlan(tier: $0) }
AnytypeAnalytics.instance().setMembershipTier(tier: _status.tier)
print("[Membership] Updated membership status - tier: \(_status.tier?.name ?? "none")")
Copy link
Member

Choose a reason for hiding this comment

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

As claude mentioned - no prints in production code. It should be either assert or remove it

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.

2 participants