Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions Clipy/Sources/Managers/MenuManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -885,8 +885,7 @@ final class PinnedSnippetStore {
func add(_ identifier: String) -> Bool {
var values = identifiers.filter { !$0.isEmpty }
guard !values.contains(identifier) else { return true }
let snapshot = EntitlementGate.currentSnapshot()
guard snapshot.isProEntitled || values.count < snapshot.limits.maxPinnedItems else {
guard EntitlementGate.canPinItem(currentPinnedCount: values.count) else {
return false
}
values.append(identifier)
Expand Down Expand Up @@ -3549,8 +3548,12 @@ class BoardManPanel: NSPanel {
}

@objc private func addSnippetCategoryFromPanel(_ sender: Any?) {
guard let title = promptForCategoryTitle(title: "Add Group", initialTitle: "") else { return }
let realm = try! Realm()
guard canAddSnippetFolder(in: realm) else {
showProLockedAlert(message: "Free plan includes 1 snippet folder. Upgrade to Pro to add more.")
return
}
guard let title = promptForCategoryTitle(title: "Add Group", initialTitle: "") else { return }
let folder = CPYFolder()
folder.title = title
folder.enable = true
Expand Down Expand Up @@ -3645,8 +3648,11 @@ class BoardManPanel: NSPanel {
}

private func canAddSnippet(in realm: Realm) -> Bool {
let snapshot = EntitlementGate.currentSnapshot()
return snapshot.isProEntitled || realm.objects(CPYSnippet.self).count < snapshot.limits.maxSnippets
return EntitlementGate.canCreateSnippet(currentSnippetCount: realm.objects(CPYSnippet.self).count)
}

private func canAddSnippetFolder(in realm: Realm) -> Bool {
return EntitlementGate.canCreateSnippetFolder(currentFolderCount: realm.objects(CPYFolder.self).count)
}

private func showProLockedAlert(message: String) {
Expand Down
27 changes: 21 additions & 6 deletions Clipy/Sources/Services/ClipService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,6 @@ extension ClipService {
// Don't save empty string history
if data.isOnlyStringType && data.stringValue.isEmpty { return }

let snapshot = EntitlementGate.currentSnapshot()
if !snapshot.isProEntitled,
realm.objects(CPYClip.self).count >= snapshot.limits.maxHistoryItems {
return
}

// Overwrite same history
let isOverwriteHistory = AppEnvironment.current.defaults.bool(forKey: Constants.UserDefaults.overwriteSameHistory)
let savedHash = isOverwriteHistory ? data.hash : Int.random(in: 0..<1_000_000)
Expand Down Expand Up @@ -181,11 +175,32 @@ extension ClipService {
dispatchRealm.transaction {
dispatchRealm.add(clip, update: .all)
}
self.trimHistoryIfNeeded(in: dispatchRealm)
}
}
}
}

private func trimHistoryIfNeeded(in realm: Realm) {
guard let limit = EntitlementGate.historyRetentionLimit(),
limit > 0 else {
return
}

let clips = realm.objects(CPYClip.self).sorted(byKeyPath: #keyPath(CPYClip.updateTime), ascending: false)
guard clips.count > limit else { return }

let overflowingClips = Array(clips.dropFirst(limit))
overflowingClips
.filter { !$0.isInvalidated && !$0.thumbnailPath.isEmpty }
.map { $0.thumbnailPath }
.forEach { PINCache.shared.removeObject(forKey: $0) }

realm.transaction {
realm.delete(overflowingClips.filter { !$0.isInvalidated })
}
}

private func types(with pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
let types = pasteboard.types?.compactMap { storableType(for: $0, pasteboard: pasteboard) } ?? []
return NSOrderedSet(array: types).array as? [NSPasteboard.PasteboardType] ?? []
Expand Down
4 changes: 4 additions & 0 deletions Clipy/Sources/Services/Entitlement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ enum EntitlementGate {
return canAdd(currentCount: currentCount, limit: .historyItems, service: service)
}

static func historyRetentionLimit(service: EntitlementService = .shared) -> Int? {
return limit(for: .historyItems, service: service)
}

static func canPinItem(currentPinnedCount: Int,
service: EntitlementService = .shared) -> Bool {
return canAdd(currentCount: currentPinnedCount, limit: .pinnedItems, service: service)
Expand Down
12 changes: 9 additions & 3 deletions Clipy/Sources/Snippets/CPYSnippetsEditorWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ extension CPYSnippetsEditorWindowController {
}

@IBAction private func addFolderButtonTapped(_ sender: AnyObject) {
guard canAddSnippetFolder() else {
showProLockedAlert(message: "Free plan includes 1 snippet folder. Upgrade to Pro to add more.")
return
}
let folder = CPYFolder.create()
folders.append(folder)
folder.merge()
Expand Down Expand Up @@ -562,10 +566,12 @@ private extension CPYSnippetsEditorWindowController {
private extension CPYSnippetsEditorWindowController {

func canAddSnippet() -> Bool {
let snapshot = EntitlementGate.currentSnapshot()
guard !snapshot.isProEntitled else { return true }
let realm = try! Realm()
return realm.objects(CPYSnippet.self).count < snapshot.limits.maxSnippets
return EntitlementGate.canCreateSnippet(currentSnippetCount: realm.objects(CPYSnippet.self).count)
}

func canAddSnippetFolder() -> Bool {
return EntitlementGate.canCreateSnippetFolder(currentFolderCount: folders.count)
}

func showProLockedAlert(message: String) {
Expand Down
45 changes: 45 additions & 0 deletions ClipyTests/EntitlementGateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ final class EntitlementGateTests {

#expect(EntitlementGate.canAddHistoryItem(currentCount: 99, service: service))
#expect(!EntitlementGate.canAddHistoryItem(currentCount: 100, service: service))
#expect(EntitlementGate.historyRetentionLimit(service: service) == 100)
}

@Test
func freeHistoryUsesRetentionInsteadOfCreationBlocking() {
let service = EntitlementService(snapshot: .freeDefault)
var storedHistoryCount = 100

storedHistoryCount += 1
if let limit = EntitlementGate.historyRetentionLimit(service: service),
storedHistoryCount > limit {
storedHistoryCount = limit
}

#expect(storedHistoryCount == 100)
}

@Test
Expand All @@ -60,6 +75,36 @@ final class EntitlementGateTests {
#expect(!EntitlementGate.canCreateSnippet(currentSnippetCount: 5, service: service))
}

@Test
func freeSnippetFolderLimitIsOne() {
let service = EntitlementService(snapshot: .freeDefault)

#expect(EntitlementGate.canCreateSnippetFolder(currentFolderCount: 0, service: service))
#expect(!EntitlementGate.canCreateSnippetFolder(currentFolderCount: 1, service: service))
}

@Test
func overLimitExistingCountsAreNotMutatedByGate() {
let service = EntitlementService(snapshot: .freeDefault)
let existingPinnedCount = 10
let existingSnippetCount = 12

#expect(!EntitlementGate.canPinItem(currentPinnedCount: existingPinnedCount, service: service))
#expect(!EntitlementGate.canCreateSnippet(currentSnippetCount: existingSnippetCount, service: service))
#expect(existingPinnedCount == 10)
#expect(existingSnippetCount == 12)
}

@Test
func proAllowsRuntimeActions() {
let service = EntitlementService(snapshot: .proActive())

#expect(EntitlementGate.canAddHistoryItem(currentCount: 10_000, service: service))
#expect(EntitlementGate.canPinItem(currentPinnedCount: 10_000, service: service))
#expect(EntitlementGate.canCreateSnippet(currentSnippetCount: 10_000, service: service))
#expect(EntitlementGate.canCreateSnippetFolder(currentFolderCount: 10_000, service: service))
}

@Test
func inactiveStatesDoNotBehaveAsActivePro() {
let inactiveStates: [LicenseState] = [.locked, .invalid, .proExpired]
Expand Down
Loading