Skip to content

Conversation

@iammajid
Copy link
Contributor

Adds a visual indicator for users with a lifetime license in the Settings screen. Users with an active lifetime purchase now see a "Full Version" label with a checkmark, confirming their premium status. Additionally, the "Restore Purchase" button has been removed for subscription users as they already have active premium access.

@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

Walkthrough

Reorders settings sections and adds a purchase status section when the user does not have full access. Introduces PurchaseStatusCell and PurchaseStatusCellViewModel to display an icon, title, and subtitle with Combine bindings. SettingsViewModel now conditionally builds sections (inserting purchaseStatusSection based on subscription/unlock state), exposes hasFullAccess, and provides a footer for the about section when full access exists. Localization adds keys for full-version footer, trial expiration date, and free-tier subtitle. SettingsViewController handles selection of PurchaseStatusCellViewModel by triggering the unlock flow. Project file updated to include the two new Swift sources.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review SettingsViewModel: section ordering, conditional section generation, hasFullAccess computation, getFooterTitle(for:) and aboutSectionElements changes.
  • Inspect PurchaseStatusCell and PurchaseStatusCellViewModel: Combine bindings, lifecycle/unbinding, layout constraints, cell reuse handling.
  • Verify SettingsViewController selection handling for PurchaseStatusCellViewModel.
  • Check localization keys usage and formatting for trial expiration.
  • Confirm pbxproj entries correctly register the new files in appropriate targets/groups.

Suggested reviewers

  • tobihagemann

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.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
Title check ✅ Passed The pull request title 'Show purchase status in settings' accurately summarizes the main change: adding a visual purchase status indicator to the Settings screen.
Description check ✅ Passed The pull request description is directly related to the changeset, detailing the addition of a visual indicator for lifetime license users and removal of the restore purchase button for subscription users.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/purchase-status

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e94263d and d6c38e6.

📒 Files selected for processing (3)
  • Cryptomator.xcodeproj/project.pbxproj (4 hunks)
  • Cryptomator/Settings/PurchaseStatusCell.swift (1 hunks)
  • SharedResources/en.lproj/Localizable.strings (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • SharedResources/en.lproj/Localizable.strings
  • Cryptomator/Settings/PurchaseStatusCell.swift
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: iammajid
Repo: cryptomator/ios PR: 426
File: Cryptomator/Settings/SettingsViewModel.swift:73-90
Timestamp: 2025-11-11T11:58:54.204Z
Learning: In SettingsViewModel.swift (Cryptomator iOS app), when displaying subscription/purchase status in the aboutSectionElements: subscriptions take precedence over lifetime licenses. If both hasRunningSubscription and fullVersionUnlocked are true (an edge case that shouldn't happen in practice), only the "Manage Subscriptions" button should be shown, not the "Full Version" status cell. This is intentional design behavior.
⏰ 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). (1)
  • GitHub Check: Build and test (premium)
🔇 Additional comments (2)
Cryptomator.xcodeproj/project.pbxproj (2)

443-444: New PurchaseStatus sources are correctly wired into the main app target

The PurchaseStatusCell.swift and PurchaseStatusCellViewModel.swift build file entries and their inclusion in the Cryptomator target’s Sources phase look consistent and scoped only to the main app, which is what you want for a Settings-only UI addition.

Also applies to: 2813-2814


1068-1069: PurchaseStatus files are properly referenced and grouped under Settings

File references for PurchaseStatusCell.swift and PurchaseStatusCellViewModel.swift are valid and placed under the existing Settings group, matching the rest of the Settings module structure. No issues from a project organization standpoint.

Also applies to: 2053-2054

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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

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

Copy link

@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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc04fa5 and 22a98a5.

📒 Files selected for processing (2)
  • Cryptomator/Settings/SettingsViewModel.swift (1 hunks)
  • SharedResources/en.lproj/Localizable.strings (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
Cryptomator/Settings/SettingsViewModel.swift (5)
Cryptomator/Common/Cells/ButtonCellViewModel.swift (1)
  • createDisclosureButton (19-21)
Cryptomator/Settings/SettingsCoordinator.swift (3)
  • showAbout (31-35)
  • showManageSubscriptions (85-97)
  • showUnlockFullVersion (79-83)
CryptomatorCommon/Sources/CryptomatorCommonCore/LocalizedString.swift (1)
  • getValue (12-22)
Cryptomator/Purchase/IAPViewController.swift (1)
  • restorePurchase (277-290)
Cryptomator/Purchase/PurchaseViewController.swift (1)
  • restorePurchase (55-61)
⏰ 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). (1)
  • GitHub Check: Build and test (freemium)
🔇 Additional comments (2)
SharedResources/en.lproj/Localizable.strings (1)

216-216: LGTM! Localization key follows conventions and uses consistent terminology.

The new localization string is well-formed and appropriately placed within the Settings section. The key name "fullVersionStatus" clearly indicates its purpose, and "Full Version" matches the terminology used throughout the app.

Cryptomator/Settings/SettingsViewModel.swift (1)

79-84: Status cell implementation is correct.

The localization key "settings.fullVersionStatus" exists in the English localization file with the value "Full Version". The cell implementation is appropriate, using BindableTableViewCellViewModel with selectionStyle: .none and accessoryType: .checkmark to correctly represent a non-interactive status indicator.

Copy link

@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: 1

🧹 Nitpick comments (4)
Cryptomator/Settings/SettingsViewController.swift (1)

103-141: Simplify didSelectRow logic by handling PurchaseStatusCellViewModel up front

Functionally this works (purchase status row deselects and triggers showUnlockFullVersion()), but you now:

  • Call dataSource?.itemIdentifier(for:) twice, and
  • Potentially deselect the same row twice for PurchaseStatusCellViewModel.

You can keep behavior while tightening the code by handling the purchase‑status case first and reusing the same item identifier, e.g.:

 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-    let cellViewModel = dataSource?.itemIdentifier(for: indexPath) as? ButtonCellViewModel<SettingsButtonAction>
+    let item = dataSource?.itemIdentifier(for: indexPath)
+
+    if item is PurchaseStatusCellViewModel {
+        tableView.deselectRow(at: indexPath, animated: true)
+        coordinator?.showUnlockFullVersion()
+        return
+    }
+
+    let cellViewModel = item as? ButtonCellViewModel<SettingsButtonAction>
@@
-    switch cellViewModel?.action {
+    switch cellViewModel?.action {
         // existing cases…
     }
-
-    if dataSource?.itemIdentifier(for: indexPath) is PurchaseStatusCellViewModel {
-        tableView.deselectRow(at: indexPath, animated: true)
-        coordinator?.showUnlockFullVersion()
-    }
 }

Keeps the UX identical while reducing lookups and local complexity in this already large delegate method.

Cryptomator/Settings/PurchaseStatusCellViewModel.swift (1)

19-23: Consider using non-optional Bindable types for title and subtitle.

The initializer accepts non-optional String parameters for title and subtitle (line 19), but wraps them in Bindable<String?> (lines 16-17). Since these values are always present, using Bindable<String> instead would eliminate unnecessary optionality and make the code more type-safe.

Apply this diff to use non-optional Bindable types:

-	let title: Bindable<String?>
-	let subtitle: Bindable<String?>
+	let title: Bindable<String>
+	let subtitle: Bindable<String>
Cryptomator/Settings/SettingsViewModel.swift (1)

103-118: Cache the DateFormatter to improve performance.

The DateFormatter is created on every access to purchaseStatusCellViewModel (lines 106-108). Since DateFormatter creation is relatively expensive and this computed property is accessed whenever sections are rebuilt, consider caching the formatter as a static property or lazy var.

Add a cached date formatter at the class level:

+	private static let expirationDateFormatter: DateFormatter = {
+		let formatter = DateFormatter()
+		formatter.dateStyle = .medium
+		formatter.timeStyle = .none
+		return formatter
+	}()
+
 	private var purchaseStatusCellViewModel: PurchaseStatusCellViewModel {
 		let subtitle: String
 		if let trialExpirationDate = cryptomatorSettings.trialExpirationDate, trialExpirationDate > Date() {
-			let dateFormatter = DateFormatter()
-			dateFormatter.dateStyle = .medium
-			dateFormatter.timeStyle = .none
-			subtitle = String(format: LocalizedString.getValue("settings.trial.expirationDate"), dateFormatter.string(from: trialExpirationDate))
+			subtitle = String(format: LocalizedString.getValue("settings.trial.expirationDate"), Self.expirationDateFormatter.string(from: trialExpirationDate))
 		} else {
 			subtitle = LocalizedString.getValue("settings.freeTier.subtitle")
 		}
Cryptomator/Settings/PurchaseStatusCell.swift (1)

30-66: LGTM! Layout implementation is clean and accessible.

The Auto Layout setup is well-structured with:

  • Proper use of layout margins
  • Multi-line label support for dynamic type
  • Appropriate spacing and sizing

One optional enhancement: consider setting isAccessibilityElement = true and providing a combined accessibility label that includes the icon meaning, title, and subtitle for VoiceOver users.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22a98a5 and e94263d.

📒 Files selected for processing (6)
  • Cryptomator.xcodeproj/project.pbxproj (4 hunks)
  • Cryptomator/Settings/PurchaseStatusCell.swift (1 hunks)
  • Cryptomator/Settings/PurchaseStatusCellViewModel.swift (1 hunks)
  • Cryptomator/Settings/SettingsViewController.swift (1 hunks)
  • Cryptomator/Settings/SettingsViewModel.swift (4 hunks)
  • SharedResources/en.lproj/Localizable.strings (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: iammajid
Repo: cryptomator/ios PR: 426
File: Cryptomator/Settings/SettingsViewModel.swift:73-90
Timestamp: 2025-11-11T11:58:54.204Z
Learning: In SettingsViewModel.swift (Cryptomator iOS app), when displaying subscription/purchase status in the aboutSectionElements: subscriptions take precedence over lifetime licenses. If both hasRunningSubscription and fullVersionUnlocked are true (an edge case that shouldn't happen in practice), only the "Manage Subscriptions" button should be shown, not the "Full Version" status cell. This is intentional design behavior.
📚 Learning: 2025-11-11T11:58:54.204Z
Learnt from: iammajid
Repo: cryptomator/ios PR: 426
File: Cryptomator/Settings/SettingsViewModel.swift:73-90
Timestamp: 2025-11-11T11:58:54.204Z
Learning: In SettingsViewModel.swift (Cryptomator iOS app), when displaying subscription/purchase status in the aboutSectionElements: subscriptions take precedence over lifetime licenses. If both hasRunningSubscription and fullVersionUnlocked are true (an edge case that shouldn't happen in practice), only the "Manage Subscriptions" button should be shown, not the "Full Version" status cell. This is intentional design behavior.

Applied to files:

  • Cryptomator/Settings/PurchaseStatusCellViewModel.swift
  • Cryptomator/Settings/SettingsViewController.swift
  • Cryptomator/Settings/PurchaseStatusCell.swift
  • Cryptomator/Settings/SettingsViewModel.swift
📚 Learning: 2024-10-10T15:32:49.838Z
Learnt from: tobihagemann
Repo: cryptomator/ios PR: 384
File: FileProviderExtensionUI/FileProviderCoordinator.swift:85-86
Timestamp: 2024-10-10T15:32:49.838Z
Learning: In `ReauthenticationViewController.swift`, the `coordinator` property is declared as `weak`, preventing retain cycles with `FileProviderCoordinator`.

Applied to files:

  • Cryptomator/Settings/SettingsViewController.swift
🧬 Code graph analysis (3)
Cryptomator/Settings/SettingsViewController.swift (2)
Cryptomator/Purchase/PurchaseViewController.swift (2)
  • tableView (31-41)
  • tableView (43-53)
Cryptomator/Settings/SettingsCoordinator.swift (1)
  • showUnlockFullVersion (79-83)
Cryptomator/Settings/PurchaseStatusCell.swift (1)
Cryptomator/Common/Combine/Publisher+OptionalAssign.swift (1)
  • assign (13-17)
Cryptomator/Settings/SettingsViewModel.swift (4)
Cryptomator/Common/TableViewModel.swift (1)
  • getFooterTitle (24-26)
CryptomatorCommon/Sources/CryptomatorCommonCore/LocalizedString.swift (1)
  • getValue (12-22)
Cryptomator/Purchase/PremiumManager.swift (3)
  • trialExpirationDate (50-52)
  • trialExpirationDate (119-127)
  • trialExpirationDate (168-170)
Cryptomator/Common/Cells/ButtonCellViewModel.swift (1)
  • createDisclosureButton (19-21)
⏰ 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). (1)
  • GitHub Check: Build and test (premium)
🔇 Additional comments (6)
Cryptomator.xcodeproj/project.pbxproj (1)

442-443: Purchase status cell + view model are correctly wired into the main app target

The new PurchaseStatusCell.swift and PurchaseStatusCellViewModel.swift are properly added to:

  • PBXFileReference (files exist in the Settings folder),
  • PBXBuildFile (compile sources entries),
  • the Settings group hierarchy, and
  • the Cryptomator target’s Sources build phase.

No config or target‑membership issues stand out.

Also applies to: 1063-1064, 2044-2045, 2792-2793

SharedResources/en.lproj/Localizable.strings (1)

216-218: LGTM! Clear and well-structured localization strings.

The three new localization keys follow the existing naming convention and provide clear, user-friendly messages for the purchase status feature. The date format specifier on line 217 is correctly used.

Cryptomator/Settings/SettingsViewModel.swift (4)

52-77: LGTM! Dynamic section generation logic is clean and correct.

The conditional inclusion of the purchase status section based on hasFullAccess is well-implemented and aligns with the PR objectives.


79-82: LGTM! Footer title logic is correct.

The implementation properly returns the footer text only for the about section when the user has full access.


84-86: LGTM! Clean abstraction for access checking.

The hasFullAccess computed property provides a clear, reusable check for premium access across the view model.


88-96: LGTM! Correctly removes the "Restore Purchase" button.

The about section elements have been properly refactored to remove the "Restore Purchase" button, addressing the feedback from the previous review. The conditional inclusion of "Manage Subscriptions" is appropriate.

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.

3 participants