├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Shared │ └── Optional.swift ├── Trial │ ├── CancelableTimedBlock.swift │ ├── Clock.swift │ ├── Days.swift │ ├── Info.plist │ ├── Resources │ │ └── PrivacyInfo.xcprivacy │ ├── Trial.h │ ├── Trial.swift │ ├── TrialPeriod.swift │ ├── TrialProvider.swift │ ├── TrialTimer.swift │ ├── TrialWriter.swift │ └── UserDefaultsTrialPeriodReader.swift └── TrialLicense │ ├── AppLicensing.swift │ ├── Collection+safeSubscript.swift │ ├── GenericRegistrationStrategy.swift │ ├── Info.plist │ ├── License.swift │ ├── LicenseConfiguration.swift │ ├── LicenseInformation.swift │ ├── LicenseInformationProvider.swift │ ├── LicenseVerifier.swift │ ├── LicensingScheme.swift │ ├── PersonalizedLicenseRegistrationStrategy.swift │ ├── RegisterApplication.swift │ ├── RegistrationPayload.swift │ ├── Resources │ └── PrivacyInfo.xcprivacy │ ├── Sequence+mapDictionary.swift │ ├── String+replacingCharactersOfnCharacterSetWith.swift │ ├── TrialLicense.h │ ├── TrialRunner.swift │ ├── URLComponents.swift │ ├── URLQueryLicenseParser.swift │ ├── UserDefaultsLicenseProvider.swift │ └── UserDefaultsLicenseWriter.swift └── Tests ├── TrialLicenseTests ├── GenericRegistrationStrategyTests.swift ├── Helpers.swift ├── Info.plist ├── LicenseInformationProviderTests.swift ├── LicenseInformationTests.swift ├── LicenseVerifierTests.swift ├── LicensingSchemeTests.swift ├── PersonalizedLicenseRegistrationStrategyTests.swift ├── RegisterApplicationTests.swift ├── UserDefaultsLicenseProviderTests.swift └── UserDefaultsLicenseWriterTests.swift └── TrialTests ├── Helpers.swift ├── Info.plist ├── TrialPeriodTests.swift ├── TrialProviderTests.swift ├── TrialWriterTests.swift └── UserDefaultsTrialPeriodReaderTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | Carthage/Checkouts 52 | Carthage/Build 53 | 54 | # fastlane 55 | # 56 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 57 | # screenshots whenever they are needed. 58 | # For more information about the recommended setup visit: 59 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 60 | 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots 64 | fastlane/test_output 65 | 66 | .swiftpm/ 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 CleanCocoa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CocoaFob", 6 | "repositoryURL": "https://github.com/glebd/cocoafob", 7 | "state": { 8 | "branch": null, 9 | "revision": "f706f2056ebc91c4d0b646333c4fb704fcb52aee", 10 | "version": "2.4.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "TrialLicense", 7 | platforms: [ 8 | .macOS(.v10_13), 9 | ], 10 | products: [ 11 | .library( 12 | name: "TrialLicense", 13 | targets: ["TrialLicense"]), 14 | .library( 15 | name: "Trial", 16 | targets: ["Trial"]), 17 | ], 18 | dependencies: [ 19 | .package(name: "CocoaFob", url: "https://github.com/glebd/cocoafob", from: Version("2.4.0")), 20 | ], 21 | targets: [ 22 | .target( 23 | name: "Trial", 24 | dependencies: [], 25 | path: "Sources/Trial", 26 | exclude: ["Info.plist"], 27 | resources: [.process("Resources/PrivacyInfo.xcprivacy")]), 28 | .testTarget( 29 | name: "TrialTests", 30 | dependencies: ["Trial"], 31 | path: "Tests/TrialTests", 32 | exclude: ["Info.plist"]), 33 | .target( 34 | name: "TrialLicense", 35 | dependencies: ["Trial", "CocoaFob"], 36 | path: "Sources/TrialLicense", 37 | exclude: ["Info.plist"], 38 | resources: [.process("Resources/PrivacyInfo.xcprivacy")]), 39 | .testTarget( 40 | name: "TrialLicenseTests", 41 | dependencies: ["TrialLicense"], 42 | path: "Tests/TrialLicenseTests", 43 | exclude: ["Info.plist"]), 44 | ] 45 | ) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Time-Based Trial App Licensing 2 | 3 | Adds time-based trial and easy license verification using [CocoaFob](https://github.com/glebd/cocoafob) to your macOS app. 4 | 5 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | 7 | **For setup instructions of your own store and app preparation with FastSpring, have a look at my book [_Make Money Outside the Mac App Store_](https://christiantietze.de/books/make-money-outside-mac-app-store-fastspring/)!** 8 | 9 | ## Installation 10 | 11 | ### Swift Package Manager 12 | 13 | ```swift 14 | dependencies: [ 15 | .package(url: "https://github.com/CleanCocoa/TrialLicensing.git", .upToNextMajor(from: Version(3, 0, 0))), 16 | ] 17 | ``` 18 | 19 | Add package depencency via Xcode by using this URL: `https://github.com/CleanCocoa/TrialLicensing.git` 20 | 21 | CocoaFob is automatically linked statically for you, no need to do anything. 22 | 23 | ### Carthage 24 | 25 | * Include the `Trial` and `TrialLicense` libraries in your project. 26 | * Include the [CocoaFob (Swift 5)](https://github.com/glebd/cocoafob/tree/master/swift5) library in your project, too. (You have to link this in the app because a library cannot embed another library.) 27 | 28 | ## Usage 29 | 30 | * Create an `AppLicensing` instance with `licenseChangeBlock` and `invalidLicenseInformationBlock` handling change events. 31 | * Set up and start the trial. 32 | 33 | Example: 34 | 35 | ```swift 36 | import TrialLicense 37 | 38 | let publicKey = [ 39 | "-----BEGIN DSA PUBLIC KEY-----\n", 40 | // ... 41 | "-----END DSA PUBLIC KEY-----\n" 42 | ].join("") 43 | let configuration = LicenseConfiguration(appName: "AmazingApp!", publicKey: publicKey) 44 | 45 | class MyApp: AppLicensingDelegate { 46 | 47 | init() { 48 | 49 | AppLicensing.setUp( 50 | configuration: configuration, 51 | initialTrialDuration: Days(30), 52 | 53 | // Set up the callbacks: 54 | licenseChangeBlock: self.licenseDidChange(licenseInfo:), 55 | invalidLicenseInformationBlock: self.didEnterInvalidLicenseCode(name:licenseCode:), 56 | 57 | // Get notified about initial state to unlock the app immediately: 58 | fireInitialState: true) 59 | } 60 | 61 | func licenseDidChange(licenseInformation: LicenseInformation) { 62 | 63 | switch licenseInformation { 64 | case .onTrial(_): 65 | // Changing back to trial may be possible if you support unregistering 66 | // form the app (and the trial period is still good.) 67 | return 68 | 69 | case .registered(_): 70 | // For example: 71 | // displayThankYouAlert() 72 | // unlockApp() 73 | 74 | case .trialUp: 75 | // For example: 76 | // displayTrialUpAlert() 77 | // lockApp() 78 | // showRegisterApp() 79 | } 80 | } 81 | 82 | func didEnterInvalidLicenseCode(name: String, licenseCode: String) { 83 | 84 | // For example: 85 | // displayInvalidLicenseAlert() 86 | // -- or show an error label in the license window. 87 | } 88 | } 89 | 90 | let myApp = MyApp() 91 | ``` 92 | 93 | 94 | ## Privacy Manifest 95 | 96 | The package declares usage of `UserDefaults` API because it ships with these mechanisms to read/write information in principle. That's it. 97 | 98 | If you use this to store actual customer names and/or email addresses in your app, depending on the licensing scheme you use, you should check that your app has a `NSPrivacyCollectedDataTypes` entry with e.g. a value of `NSPrivacyCollectedDataTypeEmailAddress`. 99 | 100 | 101 | ## Components 102 | 103 | `LicenseInformation` reveals the state your app is in: 104 | 105 | ```swift 106 | enum LicenseInformation { 107 | case registered(License) 108 | case onTrial(TrialPeriod) 109 | case trialUp 110 | } 111 | ``` 112 | 113 | The associated types provide additional information, for example to display details in a settings window or show remaining trial days in the title bar of your app. 114 | 115 | `License` represents a valid name--license code pair: 116 | 117 | ```swift 118 | struct License { 119 | let name: String 120 | let licenseCode: String 121 | } 122 | ``` 123 | 124 | `TrialPeriod` encapsulates the duration of the trial. 125 | 126 | ```swift 127 | struct TrialPeriod { 128 | let startDate: Date 129 | let endDate: Date 130 | } 131 | ``` 132 | 133 | `TrialPeriod` also provides these convenience methods: 134 | 135 | * `ended() -> Bool` 136 | * `daysLeft() -> Days` 137 | 138 | ... where `Days` encapsulates the remainder for easy conversion to `TimeInterval` and exposing `userFacingAmount: Int` for display. 139 | 140 | ## License 141 | 142 | Copyright (c) 2016 by [Christian Tietze](https://christiantietze.de/). Distributed under the MIT License. See the LICENSE file for details. 143 | -------------------------------------------------------------------------------- /Sources/Shared/Optional.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | -------------------------------------------------------------------------------- /Sources/Trial/CancelableTimedBlock.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | typealias CancelableDispatchBlock = (_ cancel: Bool) -> Void 8 | 9 | func dispatch(cancelableBlock block: @escaping () -> Void, atDate date: Date) -> CancelableDispatchBlock? { 10 | 11 | // Use two pointers for the same block handle to make 12 | // the block reference itself. 13 | var cancelableBlock: CancelableDispatchBlock? = nil 14 | 15 | let delayBlock: CancelableDispatchBlock = { cancel in 16 | 17 | if !cancel { 18 | DispatchQueue.main.async(execute: block) 19 | } 20 | 21 | cancelableBlock = nil 22 | } 23 | 24 | cancelableBlock = delayBlock 25 | 26 | let delay = Int(date.timeIntervalSinceNow) 27 | DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + .seconds(delay)) { 28 | 29 | guard case let .some(cancelableBlock) = cancelableBlock else { return } 30 | 31 | cancelableBlock(false) 32 | } 33 | 34 | return cancelableBlock 35 | } 36 | 37 | func cancelBlock(_ block: CancelableDispatchBlock?) { 38 | 39 | guard case let .some(block) = block else { return } 40 | 41 | block(true) 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Trial/Clock.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | public protocol KnowsTimeAndDate: AnyObject { 8 | 9 | func now() -> Date 10 | } 11 | 12 | public class Clock: KnowsTimeAndDate { 13 | 14 | public init() { } 15 | 16 | public func now() -> Date { 17 | 18 | return Date() 19 | } 20 | } 21 | 22 | public class StaticClock: KnowsTimeAndDate { 23 | 24 | let date: Date 25 | 26 | public init(clockDate: Date) { 27 | 28 | date = clockDate 29 | } 30 | 31 | public func now() -> Date { 32 | 33 | return date 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Trial/Days.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | public struct Days { 8 | 9 | public static func timeInterval(amount: Double) -> TimeInterval { 10 | return amount * 60 * 60 * 24 11 | } 12 | 13 | public static func amount(timeInterval: TimeInterval) -> Double { 14 | return timeInterval / 60 / 60 / 24 15 | } 16 | 17 | public let amount: Double 18 | 19 | /// Rounded to the next integer. 20 | public var userFacingAmount: Int { 21 | 22 | return Int(ceil(amount)) 23 | } 24 | 25 | public init(timeInterval: TimeInterval) { 26 | amount = fabs(Days.amount(timeInterval: timeInterval)) 27 | } 28 | 29 | public init(_ anAmount: Double) { 30 | amount = anAmount 31 | } 32 | 33 | public var timeInterval: TimeInterval { 34 | return Days.timeInterval(amount: amount) 35 | } 36 | 37 | /// Returns true for negative values, like `Days(-5)` meaning "5 days ago". 38 | public var isPast: Bool { 39 | return amount < 0 40 | } 41 | } 42 | 43 | extension Days: CustomStringConvertible { 44 | 45 | public var description: String { 46 | return "\(amount)" 47 | } 48 | } 49 | 50 | extension Days: Equatable { } 51 | 52 | public func ==(lhs: Days, rhs: Days) -> Bool { 53 | 54 | return lhs.amount == rhs.amount 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Trial/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2016 Christian Tietze. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/Trial/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyCollectedDataTypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | NSPrivacyAccessedAPIType 15 | NSPrivacyAccessedAPICategoryUserDefaults 16 | NSPrivacyAccessedAPITypeReasons 17 | 18 | C56D.1 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Sources/Trial/Trial.h: -------------------------------------------------------------------------------- 1 | // 2 | // Trial.h 3 | // Trial 4 | // 5 | // Created by Christian Tietze on 22/09/16. 6 | // Copyright © 2016 Christian Tietze. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Trial. 12 | FOUNDATION_EXPORT double TrialVersionNumber; 13 | 14 | //! Project version string for Trial. 15 | FOUNDATION_EXPORT const unsigned char TrialVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Trial/Trial.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | public struct Trial { 8 | 9 | public let trialPeriod: TrialPeriod 10 | public let clock: KnowsTimeAndDate 11 | 12 | public init(trialPeriod: TrialPeriod, clock: KnowsTimeAndDate) { 13 | 14 | self.trialPeriod = trialPeriod 15 | self.clock = clock 16 | } 17 | 18 | public var daysLeft: Int { 19 | return trialPeriod.daysLeft(clock: clock).userFacingAmount 20 | } 21 | 22 | public var ended: Bool { 23 | return trialPeriod.ended(clock: clock) 24 | } 25 | 26 | public var isActive: Bool { 27 | return !ended 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Trial/TrialPeriod.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | public struct TrialPeriod { 8 | 9 | public let startDate: Date 10 | public let endDate: Date 11 | 12 | public init(startDate aStartDate: Date, endDate anEndDate: Date) { 13 | 14 | startDate = aStartDate 15 | endDate = anEndDate 16 | } 17 | 18 | public init(numberOfDays daysLeft: Days, clock: KnowsTimeAndDate) { 19 | 20 | startDate = clock.now() 21 | endDate = startDate.addingTimeInterval(daysLeft.timeInterval) 22 | } 23 | } 24 | 25 | extension TrialPeriod { 26 | 27 | public func ended(clock: KnowsTimeAndDate = Clock()) -> Bool { 28 | 29 | let now = clock.now() 30 | return endDate < now 31 | } 32 | 33 | public func userFacingDaysLeft(clock: KnowsTimeAndDate = Clock()) -> Int { 34 | 35 | return daysLeft(clock: clock).userFacingAmount 36 | } 37 | 38 | public func daysLeft(clock: KnowsTimeAndDate = Clock()) -> Days { 39 | 40 | let now = clock.now() 41 | let timeUntil = now.timeIntervalSince(endDate) 42 | 43 | return Days(timeInterval: timeUntil) 44 | } 45 | } 46 | 47 | extension TrialPeriod { 48 | 49 | public enum UserDefaultsKeys { 50 | 51 | public static let startDate = "trial_starting" 52 | public static let endDate = "trial_ending" 53 | } 54 | } 55 | 56 | extension TrialPeriod: Equatable { } 57 | 58 | public func ==(lhs: TrialPeriod, rhs: TrialPeriod) -> Bool { 59 | 60 | return lhs.startDate == rhs.startDate && lhs.endDate == rhs.endDate 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Trial/TrialProvider.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | public protocol ProvidesTrial { 8 | var currentTrialPeriod: TrialPeriod? { get } 9 | func currentTrial(clock: KnowsTimeAndDate) -> Trial? 10 | } 11 | 12 | extension ProvidesTrial { 13 | public func currentTrial(clock: KnowsTimeAndDate) -> Trial? { 14 | guard let trialPeriod = currentTrialPeriod else { return nil } 15 | return Trial(trialPeriod: trialPeriod, clock: clock) 16 | } 17 | } 18 | 19 | open class TrialProvider: ProvidesTrial { 20 | 21 | let trialPeriodReader: ReadsTrialPeriod 22 | 23 | public init(trialPeriodReader: ReadsTrialPeriod) { 24 | self.trialPeriodReader = trialPeriodReader 25 | } 26 | 27 | open var currentTrialPeriod: TrialPeriod? { 28 | return trialPeriodReader.currentTrialPeriod 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Trial/TrialTimer.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | public class TrialTimer { 8 | 9 | let trialEndDate: Date 10 | let trialExpirationBlock: () -> Void 11 | 12 | public convenience init(trialPeriod: TrialPeriod, trialExpirationBlock: @escaping () -> Void) { 13 | 14 | self.init(trialEndDate: trialPeriod.endDate, trialExpirationBlock: trialExpirationBlock) 15 | } 16 | 17 | public init(trialEndDate: Date, trialExpirationBlock: @escaping () -> Void) { 18 | 19 | self.trialEndDate = trialEndDate 20 | self.trialExpirationBlock = trialExpirationBlock 21 | } 22 | 23 | public var isRunning: Bool { delayedBlock != nil } 24 | 25 | var delayedBlock: CancelableDispatchBlock? 26 | 27 | public func start() { 28 | 29 | guard !isRunning else { 30 | assertionFailure("invalid re-starting of a running timer") 31 | return 32 | } 33 | 34 | guard let delayedBlock = dispatch(cancelableBlock: timerDidFire, atDate: trialEndDate) else { 35 | fatalError("Cannot create a cancellable timer.") 36 | } 37 | 38 | // NSLog("Starting trial timer for: \(trialEndDate)") 39 | self.delayedBlock = delayedBlock 40 | } 41 | 42 | fileprivate func timerDidFire() { 43 | 44 | trialExpirationBlock() 45 | } 46 | 47 | /// - note: Try to stop a non-running timer raises an assertion failure. 48 | public func stop() { 49 | 50 | guard isRunning else { 51 | assertionFailure("attempting to stop non-running timer") 52 | return 53 | } 54 | 55 | cancelBlock(delayedBlock) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Trial/TrialWriter.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | public class TrialWriter { 8 | 9 | public let userDefaults: UserDefaults 10 | 11 | public init(userDefaults: UserDefaults) { 12 | self.userDefaults = userDefaults 13 | } 14 | 15 | public func store(trialPeriod: TrialPeriod) { 16 | 17 | userDefaults.set(trialPeriod.startDate, forKey: TrialPeriod.UserDefaultsKeys.startDate) 18 | userDefaults.set(trialPeriod.endDate, forKey: TrialPeriod.UserDefaultsKeys.endDate) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Trial/UserDefaultsTrialPeriodReader.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | public protocol ReadsTrialPeriod { 8 | /// `Nil` when the info couldn't be found; a `TrialPeriod` of the source otherwise. 9 | var currentTrialPeriod: TrialPeriod? { get } 10 | } 11 | 12 | open class UserDefaultsTrialPeriodReader: ReadsTrialPeriod { 13 | 14 | public let startDateKey: String 15 | public let endDateKey: String 16 | public let userDefaults: UserDefaults 17 | 18 | public init(startDateKey: String = TrialPeriod.UserDefaultsKeys.startDate, 19 | endDateKey: String = TrialPeriod.UserDefaultsKeys.endDate, 20 | userDefaults: UserDefaults) { 21 | self.startDateKey = startDateKey 22 | self.endDateKey = endDateKey 23 | self.userDefaults = userDefaults 24 | } 25 | 26 | open var isConfigured: Bool { currentTrialPeriod != nil } 27 | 28 | open var currentTrialPeriod: TrialPeriod? { 29 | guard let startDate = userDefaults.object(forKey: TrialPeriod.UserDefaultsKeys.startDate) as? Date, 30 | let endDate = userDefaults.object(forKey: TrialPeriod.UserDefaultsKeys.endDate) as? Date 31 | else { return nil } 32 | 33 | return TrialPeriod(startDate: startDate, endDate: endDate) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/TrialLicense/AppLicensing.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | import Trial 7 | 8 | typealias LicenseChangeCallback = (_ licenseInformation:LicenseInformation) -> Void 9 | 10 | /// Central licensing configuration object. 11 | /// 12 | /// Exposes `currentLicensingInformation` as a means to query for the 13 | /// active mode of the app. 14 | /// 15 | /// The `delegate` will receive notifications about license changes proactively. 16 | public class AppLicensing { 17 | 18 | public static private(set) var sharedInstance: AppLicensing? 19 | 20 | /// - returns: `nil` if the `AppLicensing` module wasn't set up prior to accessing the value, or the up-to date information otherwise. 21 | public static var currentLicenseInformation: LicenseInformation? { 22 | return sharedInstance?.currentLicenseInformation 23 | } 24 | 25 | public static var isLicenseInvalid: Bool? { 26 | return sharedInstance?.isLicenseInvalid 27 | } 28 | 29 | public static var registerApplication: HandlesRegistering? { 30 | return sharedInstance?.register 31 | } 32 | 33 | @available(*, deprecated, message: "Use invalidLicenseInformationBlock of type (RegistrationPayload) -> Void") 34 | public static func startUp( 35 | configuration: LicenseConfiguration, 36 | initialTrialDuration: Days, 37 | licenseChangeBlock: @escaping ((LicenseInformation) -> Void), 38 | invalidLicenseInformationBlock: @escaping ((String, String) -> Void), 39 | clock: KnowsTimeAndDate = Clock(), 40 | userDefaults: UserDefaults = UserDefaults.standard, 41 | fireInitialState: Bool = false) { 42 | 43 | startUp( 44 | configuration: configuration, 45 | initialTrialDuration: initialTrialDuration, 46 | licenseChangeBlock: licenseChangeBlock, 47 | invalidLicenseInformationBlock: { (payload) in 48 | invalidLicenseInformationBlock(payload.name ?? "", payload.licenseCode) 49 | }, 50 | licensingScheme: .personalizedLicense, 51 | clock: clock, 52 | userDefaults: userDefaults, 53 | fireInitialState: fireInitialState) 54 | } 55 | 56 | /// Performs initial licensing setup: 57 | /// 58 | /// 1. Sets up the `sharedInstance`, 59 | /// 2. starts the trial timer, 60 | /// 3. optionally reports back the current `LicenseInformation`. 61 | /// 62 | /// - parameter configuration: Settings used to verify licenses for 63 | /// this app. 64 | /// - parameter initialTrialDuration: Number of `Days` the time-based 65 | /// trial should run beginning at the first invocation. (Consecutive 66 | /// initialization calls will not alter the expiration date.) 67 | /// - parameter licenseChangeBlock: Invoked on license state changes. 68 | /// - parameter invalidLicenseInformationBlock: Invoked when license details 69 | /// used to register are invalid. 70 | /// - parameter registrationStrategy: Which parameters to use to verify licenses. 71 | /// Default is `.personalizedLicense`. 72 | /// - parameter clock: Testing seam so you can see what happens if the 73 | /// trial is up with manual or integration tests. 74 | /// - parameter userDefaults: The UserDefaults instance you want to store the 75 | /// trial info in. Default is `standard`. 76 | /// - parameter fireInitialState: Pass `true` to have the end of the 77 | /// setup routine immediately call `licenseChangeBlock`. 78 | /// 79 | /// Default is `false`. 80 | /// 81 | /// The callback is dispatched asynchronously on the main queue. 82 | public static func startUp( 83 | configuration: LicenseConfiguration, 84 | initialTrialDuration: Days, 85 | licenseChangeBlock: @escaping ((LicenseInformation) -> Void), 86 | invalidLicenseInformationBlock: @escaping ((_ payload: RegistrationPayload) -> Void), 87 | licensingScheme: LicensingScheme = .personalizedLicense, 88 | clock: KnowsTimeAndDate = Clock(), 89 | userDefaults: UserDefaults = UserDefaults.standard, 90 | fireInitialState: Bool = false 91 | ) { 92 | guard AppLicensing.sharedInstance == nil 93 | else { preconditionFailure("AppLicensing.startUp was called twice") } 94 | 95 | AppLicensing.sharedInstance = { 96 | 97 | let appLicensing = AppLicensing( 98 | configuration: configuration, 99 | initialTrialDuration: initialTrialDuration, 100 | licenseChangeBlock: licenseChangeBlock, 101 | invalidLicenseInformationBlock: invalidLicenseInformationBlock, 102 | licensingScheme: licensingScheme, 103 | clock: clock, 104 | userDefaults: userDefaults) 105 | 106 | appLicensing.setupTrial(initialTrialDuration: initialTrialDuration) 107 | appLicensing.configureTrialRunner() 108 | 109 | return appLicensing 110 | }() 111 | 112 | if fireInitialState { 113 | AppLicensing.reportCurrentLicenseInformation() 114 | } 115 | } 116 | 117 | fileprivate static func reportCurrentLicenseInformation() { 118 | 119 | guard let instance = AppLicensing.sharedInstance else { return } 120 | 121 | instance.licenseDidChange(licenseInformation: instance.currentLicenseInformation) 122 | } 123 | 124 | public static func tearDown() { 125 | 126 | guard let sharedInstance = AppLicensing.sharedInstance else { return } 127 | 128 | sharedInstance.stopTrialRunner() 129 | 130 | AppLicensing.sharedInstance = nil 131 | } 132 | 133 | fileprivate func setupTrial(initialTrialDuration: Days) { 134 | 135 | guard !trialPeriodReader.isConfigured else { return } 136 | 137 | let trialPeriod = TrialPeriod(numberOfDays: initialTrialDuration, 138 | clock: self.clock) 139 | TrialWriter(userDefaults: userDefaults).store(trialPeriod: trialPeriod) 140 | } 141 | 142 | fileprivate func configureTrialRunner() { 143 | // TODO: change `configureTrialRunner` and `TrialRunner.startTrialTimer` to accept `TrialPeriod` parameter. 144 | 145 | guard shouldStartTrialRunner else { return } 146 | 147 | self.trialRunner.startTrialTimer() 148 | } 149 | 150 | fileprivate var shouldStartTrialRunner: Bool { 151 | 152 | if case .registered = self.currentLicenseInformation { 153 | return false 154 | } 155 | 156 | return true 157 | } 158 | 159 | fileprivate func stopTrialRunner() { 160 | 161 | self.trialRunner.stopTrialTimer() 162 | } 163 | 164 | 165 | // MARK: App license cycle convenience methods 166 | 167 | /// Try to validate a license from `payload` and change the state 168 | /// of the app; will fire a change event if things go well. 169 | /// 170 | /// See the callbacks or `AppLicensingDelegate` methods. 171 | /// 172 | /// - important: Set up licensing with `setUp` first or the app will crash here. 173 | /// - parameter payload: The information to attempt a registration with. 174 | /// 175 | /// See also for convenience: 176 | /// - `register(name:licenseCode:)` 177 | /// - `register(licenseCode:)` 178 | @inlinable 179 | @inline(__always) 180 | public static func register(payload: RegistrationPayload) { 181 | 182 | guard let registerApplication = registerApplication else { 183 | preconditionFailure("Call setUp first") 184 | } 185 | 186 | registerApplication.register(payload: payload) 187 | } 188 | 189 | /// Try to validate a license with a personalized `RegistrationPayload` 190 | /// and change the state of the app; will fire a change event if 191 | /// things go well. 192 | /// 193 | /// See the callbacks or `AppLicensingDelegate` methods. 194 | /// 195 | /// - important: Set up licensing with `setUp` first or the app will crash here. 196 | /// - parameter name: Licensee name. 197 | /// - parameter licenseCode: License code for validation. 198 | /// 199 | /// See also: 200 | /// - `register(payload:)` 201 | /// - `register(licenseCode:)` 202 | @inlinable 203 | @inline(__always) 204 | public static func register(name: String, licenseCode: String) { 205 | 206 | register(payload: RegistrationPayload(name: name, licenseCode: licenseCode)) 207 | } 208 | 209 | /// Try to validate a license with a non-personalized `RegistrationPayload` 210 | /// and change the state of the app; will fire a change event if 211 | /// things go well. 212 | /// 213 | /// See the callbacks or `AppLicensingDelegate` methods. 214 | /// 215 | /// - important: Set up licensing with `setUp` first or the app will crash here. 216 | /// - parameter licenseCode: License code for validation. 217 | /// 218 | /// See also: 219 | /// - `register(payload:)` 220 | /// - `register(name:licenseCode)` 221 | @inlinable 222 | @inline(__always) 223 | public static func register(licenseCode: String) { 224 | 225 | register(payload: RegistrationPayload(licenseCode: licenseCode)) 226 | } 227 | 228 | /// Registers a license owner from an incoming URL Scheme query. 229 | /// 230 | /// The parser expects requests of the format: 231 | /// 232 | /// ://activate?name=ENC_NAME&licenseCode=CODE 233 | /// 234 | /// Where `ENC_NAME` is a base64-encoded version of the 235 | /// licensee name and `CODE` is the regularly encoded 236 | /// license code. 237 | /// 238 | /// You can create a supported URL from the incoming event like this: 239 | /// 240 | /// ``` 241 | /// if let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue, 242 | /// let urlComponents = URLComponents(string: urlString) { 243 | /// AppLicensing.register(urlComponents: urlComponents) 244 | /// } 245 | /// ``` 246 | /// 247 | /// - parameter urlComponents: URL components from the URL scheme. 248 | @inlinable 249 | @inline(__always) 250 | public static func register(urlComponents: Foundation.URLComponents) { 251 | 252 | let queryParser = URLQueryLicenseParser() 253 | guard urlComponents.host == TrialLicense.URLComponents.host, 254 | let queryItems = urlComponents.queryItems, 255 | let license = queryParser.parse(queryItems: queryItems) 256 | else { return } 257 | register(payload: license.payload) 258 | } 259 | 260 | /// Unregisters from whatever state the app is in; 261 | /// only makes sense to call this when the app is `.registered` 262 | /// or the state won't change and no event will 263 | /// be fired. 264 | /// 265 | /// See the callbacks or `AppLicensingDelegate` methods. 266 | /// 267 | /// - important: Set up licensing with `setUp` first or the app will crash here. 268 | @inlinable 269 | @inline(__always) 270 | public static func unregister() { 271 | 272 | guard let registerApplication = registerApplication 273 | else { preconditionFailure("Call setUp first") } 274 | 275 | registerApplication.unregister() 276 | } 277 | 278 | // MARK: - 279 | 280 | public let clock: KnowsTimeAndDate 281 | public let userDefaults: UserDefaults 282 | internal let trialPeriodReader: UserDefaultsTrialPeriodReader 283 | public let trialProvider: ProvidesTrial 284 | public let licenseInformationProvider: ProvidesLicenseInformation 285 | fileprivate(set) var register: RegisterApplication! 286 | fileprivate(set) var trialRunner: TrialRunner! 287 | let licensingScheme: LicensingScheme 288 | 289 | fileprivate(set) var licenseChangeBlock: (LicenseInformation) -> Void 290 | fileprivate(set) var invalidLicenseInformationBlock: (_ payload: RegistrationPayload) -> Void 291 | 292 | init( 293 | configuration: LicenseConfiguration, 294 | initialTrialDuration: Days, 295 | licenseChangeBlock: @escaping ((LicenseInformation) -> Void), 296 | invalidLicenseInformationBlock: @escaping ((_ payload: RegistrationPayload) -> Void), 297 | licensingScheme: LicensingScheme, 298 | clock: KnowsTimeAndDate = Clock(), 299 | userDefaults: UserDefaults = UserDefaults.standard 300 | ) { 301 | self.clock = clock 302 | self.userDefaults = userDefaults 303 | self.licenseChangeBlock = licenseChangeBlock 304 | self.invalidLicenseInformationBlock = invalidLicenseInformationBlock 305 | self.licensingScheme = licensingScheme 306 | 307 | let licenseVerifier = LicenseVerifier(configuration: configuration) 308 | let licenseProvider = UserDefaultsLicenseProvider( 309 | userDefaults: userDefaults, 310 | removingWhitespace: configuration.removeWhitespaceFromLicenseCodes) 311 | self.trialPeriodReader = UserDefaultsTrialPeriodReader(userDefaults: userDefaults) 312 | self.trialProvider = TrialProvider(trialPeriodReader: trialPeriodReader) 313 | self.licenseInformationProvider = LicenseInformationProvider( 314 | trialProvider: trialProvider, 315 | licenseProvider: licenseProvider, 316 | licenseVerifier: licenseVerifier, 317 | registrationStrategy: licensingScheme.registrationStrategy, 318 | configuration: configuration, 319 | clock: clock) 320 | 321 | self.trialRunner = TrialRunner( 322 | trialProvider: trialProvider, 323 | licenseChangeCallback: { [weak self] in 324 | self?.licenseDidChange(licenseInformation: $0) 325 | }) 326 | 327 | let licenseWriter = UserDefaultsLicenseWriter( 328 | userDefaults: userDefaults, 329 | removingWhitespace: configuration.removeWhitespaceFromLicenseCodes) 330 | self.register = RegisterApplication( 331 | licenseVerifier: licenseVerifier, 332 | licenseWriter: licenseWriter, 333 | licenseInformationProvider: licenseInformationProvider, 334 | trialProvider: trialProvider, 335 | registrationStrategy: licensingScheme.registrationStrategy, 336 | configuration: configuration, 337 | licenseChangeCallback: { [weak self] in 338 | self?.licenseDidChange(licenseInformation: $0) 339 | }, 340 | invalidLicenseCallback: { [weak self] in 341 | self?.didEnterInvalidLicense(payload: $0) 342 | }) 343 | } 344 | 345 | public var isLicenseInvalid: Bool { 346 | return self.licenseInformationProvider.isLicenseInvalid 347 | } 348 | 349 | public var currentLicenseInformation: LicenseInformation { 350 | 351 | return self.licenseInformationProvider.currentLicenseInformation 352 | } 353 | 354 | 355 | // MARK: Delegate adapters 356 | 357 | fileprivate func licenseDidChange(licenseInformation: LicenseInformation) { 358 | 359 | switch licenseInformation { 360 | case .onTrial: self.configureTrialRunner() 361 | case .registered: self.stopTrialRunner() 362 | case .trialUp: break 363 | } 364 | 365 | DispatchQueue.main.async { 366 | 367 | self.licenseChangeBlock(licenseInformation) 368 | } 369 | } 370 | 371 | fileprivate func didEnterInvalidLicense(payload: RegistrationPayload) { 372 | 373 | DispatchQueue.main.async { 374 | 375 | self.invalidLicenseInformationBlock(payload) 376 | } 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /Sources/TrialLicense/Collection+safeSubscript.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | extension Collection { 6 | subscript (safe index: Self.Index) -> Self.Iterator.Element? { 7 | return index < endIndex ? self[index] : nil 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/TrialLicense/GenericRegistrationStrategy.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | struct GenericRegistrationStrategy: RegistrationStrategy { 6 | 7 | func isValid(payload: RegistrationPayload, 8 | configuration: LicenseConfiguration, 9 | licenseVerifier: LicenseCodeVerification) 10 | -> Bool 11 | { 12 | let licenseCode = payload.licenseCode 13 | let registrationName = LicensingScheme.generic.registrationName( 14 | appName: configuration.appName, 15 | payload: payload) 16 | return licenseVerifier.isValid( 17 | licenseCode: licenseCode, 18 | registrationName: registrationName) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/TrialLicense/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2016 Christian Tietze. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/TrialLicense/License.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | public struct License { 8 | 9 | public let name: String? 10 | public let licenseCode: String 11 | 12 | public var payload: RegistrationPayload { 13 | if let name = self.name { 14 | return RegistrationPayload(name: name, licenseCode: self.licenseCode) 15 | } 16 | return RegistrationPayload(licenseCode: self.licenseCode) 17 | } 18 | 19 | public init(name: String?, licenseCode: String) { 20 | 21 | self.name = name 22 | self.licenseCode = licenseCode 23 | } 24 | 25 | public struct UserDefaultsKeys { 26 | private init() { } 27 | 28 | public static func change(nameKey: String, licenseCodeKey: String) { 29 | UserDefaultsKeys.name = nameKey 30 | UserDefaultsKeys.licenseCode = licenseCodeKey 31 | } 32 | 33 | public static var name = "licensee" 34 | public static var licenseCode = "license_code" 35 | } 36 | } 37 | 38 | extension License: Equatable { } 39 | 40 | public func ==(lhs: License, rhs: License) -> Bool { 41 | 42 | return lhs.name == rhs.name && lhs.licenseCode == rhs.licenseCode 43 | } 44 | -------------------------------------------------------------------------------- /Sources/TrialLicense/LicenseConfiguration.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | /// Configuration for the app's license generator and verifier 8 | /// using CocoaFob. 9 | public struct LicenseConfiguration: Equatable { 10 | 11 | public let appName: String 12 | public let publicKey: String 13 | public let removeWhitespaceFromLicenseCodes: Bool 14 | 15 | /// - Parameters: 16 | /// - appName: Name of the product as it is used by the license generator. 17 | /// - publicKey: Complete public key PEM, including the header and footer lines. 18 | /// 19 | /// Example: 20 | /// 21 | /// -----BEGIN DSA PUBLIC KEY-----\n 22 | /// MIHwMIGoBgcqhkjOOAQBMI... 23 | /// GcAkEAoKLaPXkgAPng5YtV... 24 | /// -----END DSA PUBLIC KEY-----\n 25 | /// - removeWhitespaceFromLicenseCodes: Determines whether writing and reading license codes 26 | /// from `UserDefaults` will remove any whitespace from license codes. This can make pasting 27 | /// license codes easier for end users. 28 | public init( 29 | appName: String, 30 | publicKey: String, 31 | removingWhitespaceFromLicenseCodes: Bool = true 32 | ) { 33 | self.appName = appName 34 | self.publicKey = publicKey 35 | self.removeWhitespaceFromLicenseCodes = removingWhitespaceFromLicenseCodes 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/TrialLicense/LicenseInformation.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | import Trial 7 | 8 | public enum LicenseInformation { 9 | 10 | case registered(License) 11 | case onTrial(TrialPeriod) 12 | case trialUp 13 | } 14 | 15 | extension LicenseInformation { 16 | 17 | public typealias UserInfo = [AnyHashable : Any] 18 | 19 | public var userInfo: UserInfo { 20 | 21 | switch self { 22 | case let .onTrial(trialPeriod): 23 | return [ 24 | "registered" : false, 25 | "on_trial" : true, 26 | "trial_start_date" : trialPeriod.startDate, 27 | "trial_end_date" : trialPeriod.endDate, 28 | ] 29 | case let .registered(license): 30 | var result: UserInfo = [ 31 | "registered" : true, 32 | "on_trial" : false, 33 | "licenseCode" : license.licenseCode 34 | ] 35 | result["name"] = license.name 36 | return result 37 | case .trialUp: 38 | return [ 39 | "registered" : false, 40 | "on_trial" : false 41 | ] 42 | } 43 | } 44 | 45 | public init?(userInfo: UserInfo) { 46 | guard let registered = userInfo["registered"] as? Bool else { return nil } 47 | 48 | if let onTrial = userInfo["on_trial"] as? Bool, 49 | !registered { 50 | 51 | if !onTrial { 52 | self = .trialUp 53 | return 54 | } 55 | 56 | if let startDate = userInfo["trial_start_date"] as? Date, 57 | let endDate = userInfo["trial_end_date"] as? Date { 58 | 59 | self = .onTrial(TrialPeriod(startDate: startDate, endDate: endDate)) 60 | return 61 | } 62 | } 63 | 64 | guard let licenseCode = userInfo["licenseCode"] as? String else { return nil } 65 | let name = userInfo["name"] as? String 66 | 67 | self = .registered(License(name: name, licenseCode: licenseCode)) 68 | } 69 | 70 | /// Uses `userInfo` of `notification` to try to initialize a `LicenseInformation` object. 71 | public init?(notification: Notification) { 72 | guard let userInfo = notification.userInfo else { return nil } 73 | self.init(userInfo: userInfo) 74 | } 75 | } 76 | 77 | extension LicenseInformation: Equatable { } 78 | 79 | public func ==(lhs: LicenseInformation, rhs: LicenseInformation) -> Bool { 80 | 81 | switch (lhs, rhs) { 82 | case (.trialUp, .trialUp): return true 83 | case let (.onTrial(lPeriod), .onTrial(rPeriod)): return lPeriod == rPeriod 84 | case let (.registered(lLicense), .registered(rLicense)): return lLicense == rLicense 85 | default: return false 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/TrialLicense/LicenseInformationProvider.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | import Trial 7 | 8 | public protocol ProvidesLicenseInformation { 9 | var isLicenseInvalid: Bool { get } 10 | var currentLicenseInformation: LicenseInformation { get } 11 | } 12 | 13 | public protocol ProvidesLicense { 14 | /// `Nil` when the info couldn't be found; a `License` from the source otherwise. 15 | var currentLicense: License? { get } 16 | } 17 | 18 | class LicenseInformationProvider: ProvidesLicenseInformation { 19 | 20 | let trialProvider: ProvidesTrial 21 | let licenseProvider: ProvidesLicense 22 | let licenseVerifier: LicenseVerifier 23 | let registrationStrategy: RegistrationStrategy 24 | let configuration: LicenseConfiguration 25 | let clock: KnowsTimeAndDate 26 | 27 | init(trialProvider: ProvidesTrial, 28 | licenseProvider: ProvidesLicense, 29 | licenseVerifier: LicenseVerifier, 30 | registrationStrategy: RegistrationStrategy, 31 | configuration: LicenseConfiguration, 32 | clock: KnowsTimeAndDate = Clock()) { 33 | 34 | self.trialProvider = trialProvider 35 | self.licenseProvider = licenseProvider 36 | self.licenseVerifier = licenseVerifier 37 | self.registrationStrategy = registrationStrategy 38 | self.configuration = configuration 39 | self.clock = clock 40 | } 41 | 42 | var isLicenseInvalid: Bool { 43 | 44 | guard let license = self.license() else { 45 | return false 46 | } 47 | 48 | return !self.isLicenseValid(license) 49 | } 50 | 51 | var currentLicenseInformation: LicenseInformation { 52 | 53 | if let license = self.license(), 54 | isLicenseValid(license) { 55 | 56 | return .registered(license) 57 | } 58 | 59 | if let trial = self.trial(), 60 | trial.isActive { 61 | 62 | return .onTrial(trial.trialPeriod) 63 | } 64 | 65 | return .trialUp 66 | } 67 | 68 | private func license() -> License? { 69 | 70 | return licenseProvider.currentLicense 71 | } 72 | 73 | private func trial() -> Trial? { 74 | 75 | return trialProvider.currentTrial(clock: clock) 76 | } 77 | 78 | private func isLicenseValid(_ license: License) -> Bool { 79 | return registrationStrategy.isValid( 80 | payload: license.payload, 81 | configuration: self.configuration, 82 | licenseVerifier: self.licenseVerifier) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/TrialLicense/LicenseVerifier.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | import CocoaFob 7 | 8 | public protocol LicenseCodeVerification: AnyObject { 9 | func isValid(licenseCode: String, registrationName: String) -> Bool 10 | } 11 | 12 | /// Verifies license information based on the registration 13 | /// scheme. This is either 14 | /// - `APPNAME,LICENSEE_NAME`, which ties licenses to both application and user, or 15 | /// - `APPNAME` only. 16 | class LicenseVerifier: LicenseCodeVerification { 17 | 18 | let configuration: LicenseConfiguration 19 | fileprivate var publicKey: String { return configuration.publicKey } 20 | 21 | init(configuration: LicenseConfiguration) { 22 | 23 | self.configuration = configuration 24 | } 25 | 26 | /// - parameter licenseCode: License key to verify. 27 | /// - parameter registrationName: Format as used on FastSpring, e.g. "appName,userName". 28 | func isValid(licenseCode: String, registrationName: String) -> Bool { 29 | 30 | guard let verifier = verifier(publicKey: self.publicKey) else { 31 | assertionFailure("CocoaFob.LicenseVerifier cannot be constructed") 32 | return false 33 | } 34 | 35 | return verifier.verify(licenseCode, forName: registrationName) 36 | } 37 | 38 | fileprivate func verifier(publicKey: String) -> CocoaFob.LicenseVerifier? { 39 | 40 | return CocoaFob.LicenseVerifier(publicKeyPEM: publicKey) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/TrialLicense/LicensingScheme.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | /// The supported license generation template you use. 6 | public enum LicensingScheme { 7 | 8 | /// Template `"APPNAME"`. 9 | case generic 10 | 11 | /// Template `"APPNAME,LICENSEE_NAME"`. 12 | case personalizedLicense 13 | 14 | func registrationName(appName: String, payload: RegistrationPayload) -> String { 15 | switch self { 16 | case .generic: 17 | return appName 18 | 19 | case .personalizedLicense: 20 | let licenseeName = payload.name ?? "" 21 | return "\(appName),\(licenseeName)" 22 | } 23 | } 24 | 25 | internal var registrationStrategy: RegistrationStrategy { 26 | switch self { 27 | case .generic: 28 | return GenericRegistrationStrategy() 29 | 30 | case .personalizedLicense: 31 | return PersonalizedLicenseRegistrationStrategy() 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/TrialLicense/PersonalizedLicenseRegistrationStrategy.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | struct PersonalizedLicenseRegistrationStrategy: RegistrationStrategy { 6 | 7 | func isValid(payload: RegistrationPayload, 8 | configuration: LicenseConfiguration, 9 | licenseVerifier: LicenseCodeVerification) 10 | -> Bool 11 | { 12 | let licenseCode = payload.licenseCode 13 | let registrationName = LicensingScheme.personalizedLicense.registrationName( 14 | appName: configuration.appName, 15 | payload: payload) 16 | return licenseVerifier.isValid( 17 | licenseCode: licenseCode, 18 | registrationName: registrationName) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/TrialLicense/RegisterApplication.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | import Trial 7 | 8 | /// Implemented by `RegisterApplication`; use this for delegates 9 | /// or view controller callbacks. 10 | public protocol HandlesRegistering: AnyObject { 11 | 12 | func register(payload: RegistrationPayload) 13 | func unregister() 14 | } 15 | 16 | protocol RegistrationStrategy { 17 | func isValid(payload: RegistrationPayload, configuration: LicenseConfiguration, licenseVerifier: LicenseCodeVerification) -> Bool 18 | } 19 | 20 | public protocol WritesLicense { 21 | func store(license: License) 22 | func store(licenseCode: String, forName name: String?) 23 | func removeLicense() 24 | } 25 | 26 | extension WritesLicense { 27 | func store(license: License) { 28 | self.store(licenseCode: license.licenseCode, forName: license.name) 29 | } 30 | } 31 | 32 | class RegisterApplication: HandlesRegistering { 33 | 34 | let licenseVerifier: LicenseVerifier 35 | let licenseWriter: WritesLicense 36 | let licenseInformationProvider: ProvidesLicenseInformation 37 | let trialProvider: ProvidesTrial 38 | let registrationStrategy: RegistrationStrategy 39 | let configuration: LicenseConfiguration 40 | 41 | let licenseChangeCallback: LicenseChangeCallback 42 | 43 | typealias InvalidLicenseCallback = (_ payload: RegistrationPayload) -> Void 44 | let invalidLicenseCallback: InvalidLicenseCallback 45 | 46 | init(licenseVerifier: LicenseVerifier, 47 | licenseWriter: WritesLicense, 48 | licenseInformationProvider: ProvidesLicenseInformation, 49 | trialProvider: ProvidesTrial, 50 | registrationStrategy: RegistrationStrategy, 51 | configuration: LicenseConfiguration, 52 | licenseChangeCallback: @escaping LicenseChangeCallback, 53 | invalidLicenseCallback: @escaping InvalidLicenseCallback) { 54 | 55 | self.licenseVerifier = licenseVerifier 56 | self.licenseWriter = licenseWriter 57 | self.licenseInformationProvider = licenseInformationProvider 58 | self.trialProvider = trialProvider 59 | self.registrationStrategy = registrationStrategy 60 | self.configuration = configuration 61 | self.licenseChangeCallback = licenseChangeCallback 62 | self.invalidLicenseCallback = invalidLicenseCallback 63 | } 64 | 65 | func register(payload: RegistrationPayload) { 66 | 67 | guard payloadIsValid(payload) else { 68 | invalidLicenseCallback(payload) 69 | return 70 | } 71 | 72 | let license = License(name: payload.name, licenseCode: payload.licenseCode) 73 | let licenseInformation = LicenseInformation.registered(license) 74 | 75 | licenseWriter.store(license: license) 76 | licenseChangeCallback(licenseInformation) 77 | } 78 | 79 | private func payloadIsValid(_ payload: RegistrationPayload) -> Bool { 80 | return self.registrationStrategy.isValid( 81 | payload: payload, 82 | configuration: self.configuration, 83 | licenseVerifier: self.licenseVerifier) 84 | } 85 | 86 | func unregister() { 87 | 88 | let currentLicenseInformation = licenseInformationProvider.currentLicenseInformation 89 | 90 | licenseWriter.removeLicense() 91 | 92 | // Pass through when there was no registration info. 93 | guard case .registered = currentLicenseInformation else { return } 94 | 95 | guard let trialPeriod = trialProvider.currentTrialPeriod else { 96 | licenseChangeCallback(.trialUp) 97 | return 98 | } 99 | 100 | licenseChangeCallback(.onTrial(trialPeriod)) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/TrialLicense/RegistrationPayload.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | /// User data to be processed for registration. 6 | /// See `License` for the counterpart of a valid license. 7 | public struct RegistrationPayload: Equatable { 8 | 9 | public let name: String? 10 | public let licenseCode: String 11 | 12 | public init(name: String, licenseCode: String) { 13 | self.name = name 14 | self.licenseCode = licenseCode 15 | } 16 | 17 | public init(licenseCode: String) { 18 | self.name = nil 19 | self.licenseCode = licenseCode 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/TrialLicense/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyCollectedDataTypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | NSPrivacyAccessedAPIType 15 | NSPrivacyAccessedAPICategoryUserDefaults 16 | NSPrivacyAccessedAPITypeReasons 17 | 18 | C56D.1 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Sources/TrialLicense/Sequence+mapDictionary.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | extension Sequence { 8 | func mapDictionary(_ transform: (Self.Iterator.Element) throws -> (K, V)) rethrows -> [K : V] { 9 | var result = [K : V]() 10 | 11 | for element in self { 12 | let (key, value) = try transform(element) 13 | result[key] = value 14 | } 15 | 16 | return result 17 | } 18 | 19 | func mapDictionary(_ transform: (Self.Iterator.Element) throws -> (K, V)?) rethrows -> [K : V] { 20 | var result = [K : V]() 21 | 22 | for element in self { 23 | if let (key, value) = try transform(element) { 24 | result[key] = value 25 | } 26 | } 27 | 28 | return result 29 | } 30 | 31 | func flatMapDictionary(_ transform: (Self.Iterator.Element) throws -> (K, V)?) rethrows -> [K : V] { 32 | var result = [K : V]() 33 | 34 | for element in self { 35 | if let (key, value) = try transform(element) { 36 | result[key] = value 37 | } 38 | } 39 | 40 | return result 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/TrialLicense/String+replacingCharactersOfnCharacterSetWith.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2022 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | extension String { 8 | func replacingCharacters( 9 | of characterSet: CharacterSet, 10 | with replacement: String 11 | ) -> String { 12 | return self.components(separatedBy: characterSet) 13 | .joined(separator: replacement) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/TrialLicense/TrialLicense.h: -------------------------------------------------------------------------------- 1 | // 2 | // TrialLicense.h 3 | // TrialLicense 4 | // 5 | // Created by Christian Tietze on 22/09/16. 6 | // Copyright © 2016 Christian Tietze. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for TrialLicense. 12 | FOUNDATION_EXPORT double TrialLicenseVersionNumber; 13 | 14 | //! Project version string for TrialLicense. 15 | FOUNDATION_EXPORT const unsigned char TrialLicenseVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/TrialLicense/TrialRunner.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | import Trial 7 | 8 | class TrialRunner { 9 | 10 | let licenseChangeCallback: LicenseChangeCallback 11 | let trialProvider: ProvidesTrial 12 | 13 | init(trialProvider: ProvidesTrial, licenseChangeCallback: @escaping LicenseChangeCallback) { 14 | 15 | self.licenseChangeCallback = licenseChangeCallback 16 | self.trialProvider = trialProvider 17 | } 18 | 19 | fileprivate var trialTimer: TrialTimer? 20 | 21 | func startTrialTimer() { 22 | 23 | stopTrialTimer() 24 | 25 | guard let trialPeriod = trialProvider.currentTrialPeriod 26 | else { return } 27 | 28 | let trialTimer = TrialTimer(trialPeriod: trialPeriod) { [weak self] in 29 | self?.licenseChangeCallback(.trialUp) 30 | } 31 | trialTimer.start() 32 | self.trialTimer = trialTimer 33 | } 34 | 35 | func stopTrialTimer() { 36 | 37 | if let trialTimer = trialTimer, trialTimer.isRunning { 38 | 39 | trialTimer.stop() 40 | } 41 | 42 | self.trialTimer = nil 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/TrialLicense/URLComponents.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | public enum URLComponents { 6 | @inlinable 7 | @inline(__always) 8 | public static var host: String { "activate" } 9 | 10 | @inlinable 11 | @inline(__always) 12 | public static var licensee: String { "name" } 13 | 14 | @inlinable 15 | @inline(__always) 16 | public static var licenseCode: String { "licenseCode" } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/TrialLicense/URLQueryLicenseParser.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | public struct URLQueryLicenseParser { 8 | @inlinable 9 | @inline(__always) 10 | public init() { } 11 | 12 | @inlinable 13 | @inline(__always) 14 | public func parse(queryItems: [Foundation.URLQueryItem]) -> License? { 15 | let nameQueryItem: URLQueryItem? = queryItems 16 | .filter { $0.name == TrialLicense.URLComponents.licensee } 17 | .first 18 | 19 | let licenseCodeQueryItem: URLQueryItem? = queryItems 20 | .filter { $0.name == TrialLicense.URLComponents.licenseCode } 21 | .first 22 | 23 | guard let licenseCode = licenseCodeQueryItem?.value else { return nil } 24 | 25 | return parse( 26 | base64EncodedName: nameQueryItem?.value, 27 | licenseCode: licenseCode 28 | ) 29 | } 30 | 31 | @inlinable 32 | @inline(__always) 33 | public func parse(base64EncodedName: String?, licenseCode: String) -> License? { 34 | let name = base64EncodedName.flatMap { string -> String? in 35 | guard let decodedData = Data(base64Encoded: string) else { return nil } 36 | return String(data: decodedData, encoding: .utf8) 37 | } 38 | 39 | return License(name: name, licenseCode: licenseCode) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/TrialLicense/UserDefaultsLicenseProvider.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | class UserDefaultsLicenseProvider: ProvidesLicense { 8 | 9 | let userDefaults: UserDefaults 10 | let removingWhitespace: Bool 11 | 12 | init(userDefaults: UserDefaults, removingWhitespace: Bool) { 13 | self.userDefaults = userDefaults 14 | self.removingWhitespace = removingWhitespace 15 | } 16 | 17 | var currentLicense: License? { 18 | guard let licenseCode = self.licenseCode else { return nil } 19 | return License(name: self.name, licenseCode: licenseCode) 20 | } 21 | 22 | @inline(__always) 23 | private var licenseCode: String? { 24 | let licenseCode = userDefaults.string(forKey: "\(License.UserDefaultsKeys.licenseCode)") 25 | if removingWhitespace { 26 | return licenseCode?.replacingCharacters(of: .whitespacesAndNewlines, with: "") 27 | } else { 28 | return licenseCode 29 | } 30 | } 31 | 32 | @inline(__always) 33 | private var name: String? { userDefaults.string(forKey: "\(License.UserDefaultsKeys.name)") } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/TrialLicense/UserDefaultsLicenseWriter.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | class UserDefaultsLicenseWriter: WritesLicense { 8 | 9 | let userDefaults: UserDefaults 10 | let removingWhitespace: Bool 11 | 12 | init(userDefaults: UserDefaults, removingWhitespace: Bool) { 13 | self.userDefaults = userDefaults 14 | self.removingWhitespace = removingWhitespace 15 | } 16 | 17 | func store(licenseCode untreatedLicenseCode: String, forName name: String?) { 18 | 19 | let licenseCode: String 20 | if removingWhitespace { 21 | licenseCode = untreatedLicenseCode.replacingCharacters(of: .whitespacesAndNewlines, with: "") 22 | } else { 23 | licenseCode = untreatedLicenseCode 24 | } 25 | 26 | userDefaults.setValue(name, forKey: License.UserDefaultsKeys.name) 27 | userDefaults.setValue(licenseCode, forKey: License.UserDefaultsKeys.licenseCode) 28 | } 29 | 30 | func removeLicense() { 31 | 32 | userDefaults.removeObject(forKey: License.UserDefaultsKeys.name) 33 | userDefaults.removeObject(forKey: License.UserDefaultsKeys.licenseCode) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/TrialLicenseTests/GenericRegistrationStrategyTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Cocoa 6 | import XCTest 7 | @testable import TrialLicense 8 | 9 | class GenericRegistrationStrategyTests: XCTestCase { 10 | 11 | var verifierDouble: TestVerifier! 12 | 13 | override func setUp() { 14 | super.setUp() 15 | verifierDouble = TestVerifier() 16 | } 17 | 18 | override func tearDown() { 19 | verifierDouble = nil 20 | super.tearDown() 21 | } 22 | 23 | 24 | // MARK: - 25 | 26 | func testIsValid_WithoutName_PassesDataToVerifier() { 27 | 28 | let appName = "AmazingAppName2000" 29 | let licenseCode = "supposed to be a license code" 30 | let payload = RegistrationPayload(licenseCode: licenseCode) 31 | 32 | _ = GenericRegistrationStrategy().isValid( 33 | payload: payload, 34 | configuration: LicenseConfiguration(appName: appName, publicKey: "irrelevant"), 35 | licenseVerifier: verifierDouble) 36 | 37 | XCTAssertNotNil(verifierDouble.didCallIsValidWith) 38 | if let values = verifierDouble.didCallIsValidWith { 39 | let expectedRegistrationName = LicensingScheme.generic.registrationName(appName: appName, payload: payload) 40 | XCTAssertEqual(values.registrationName, expectedRegistrationName) 41 | XCTAssertEqual(values.licenseCode, licenseCode) 42 | } 43 | } 44 | 45 | func testIsValid_WithName_PassesDataToVerifier() { 46 | 47 | let appName = "TheAppHere" 48 | let licenseCode = "code to unlock" 49 | let payload = RegistrationPayload(name: "irrelevant", licenseCode: licenseCode) 50 | 51 | _ = GenericRegistrationStrategy().isValid( 52 | payload: payload, 53 | configuration: LicenseConfiguration(appName: appName, publicKey: "irrelevant"), 54 | licenseVerifier: verifierDouble) 55 | 56 | XCTAssertNotNil(verifierDouble.didCallIsValidWith) 57 | if let values = verifierDouble.didCallIsValidWith { 58 | let expectedRegistrationName = LicensingScheme.generic.registrationName(appName: appName, payload: payload) 59 | XCTAssertEqual(values.registrationName, expectedRegistrationName) 60 | XCTAssertEqual(values.licenseCode, licenseCode) 61 | } 62 | } 63 | 64 | func testIsValid_ReturnsVerifierResult() { 65 | 66 | let irrelevantPayload = RegistrationPayload(name: "irrelevant", licenseCode: "irrelevant") 67 | let irrelevantConfiguration = LicenseConfiguration(appName: "irrelevant", publicKey: "irrelevant") 68 | 69 | verifierDouble.testValidity = true 70 | XCTAssertTrue(GenericRegistrationStrategy().isValid( 71 | payload: irrelevantPayload, 72 | configuration: irrelevantConfiguration, 73 | licenseVerifier: verifierDouble)) 74 | 75 | verifierDouble.testValidity = false 76 | XCTAssertFalse(GenericRegistrationStrategy().isValid( 77 | payload: irrelevantPayload, 78 | configuration: irrelevantConfiguration, 79 | licenseVerifier: verifierDouble)) 80 | } 81 | 82 | 83 | // MARK: - 84 | 85 | class TestVerifier: LicenseVerifier { 86 | 87 | init() { 88 | super.init(configuration: LicenseConfiguration(appName: "irrelevant app name", publicKey: "irrelevant key")) 89 | } 90 | 91 | var testValidity = false 92 | var didCallIsValidWith: (licenseCode: String, registrationName: String)? 93 | override func isValid(licenseCode: String, registrationName: String) -> Bool { 94 | 95 | didCallIsValidWith = (licenseCode, registrationName) 96 | 97 | return testValidity 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Tests/TrialLicenseTests/Helpers.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | class NullUserDefaults: UserDefaults { 8 | 9 | override func register(defaults registrationDictionary: [String : Any]) { } 10 | override func value(forKey key: String) -> Any? { return nil } 11 | override func setValue(_ value: Any?, forKey key: String) { } 12 | } 13 | 14 | func hasValue(_ value: T?) -> Bool { 15 | switch (value) { 16 | case .some(_): return true 17 | case .none: return false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/TrialLicenseTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/TrialLicenseTests/LicenseInformationProviderTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import XCTest 6 | @testable import TrialLicense 7 | import Trial 8 | 9 | class LicenseInformationProviderTests: XCTestCase { 10 | 11 | var licenseInfoProvider: LicenseInformationProvider! 12 | 13 | var trialProviderDouble: TestTrialProvider! 14 | var licenseProviderDouble: TestLicenseProvider! 15 | var clockDouble: TestClock! 16 | var registrationStrategyDouble: TestRegistrationStrategy! 17 | 18 | var configuration: LicenseConfiguration { 19 | return LicenseConfiguration(appName: "NameOfTheApp", publicKey: "the-irrelevant-key") 20 | } 21 | 22 | override func setUp() { 23 | super.setUp() 24 | 25 | trialProviderDouble = TestTrialProvider() 26 | licenseProviderDouble = TestLicenseProvider() 27 | clockDouble = TestClock() 28 | registrationStrategyDouble = TestRegistrationStrategy() 29 | 30 | licenseInfoProvider = LicenseInformationProvider( 31 | trialProvider: trialProviderDouble, 32 | licenseProvider: licenseProviderDouble, 33 | licenseVerifier: NullVerifier(), 34 | registrationStrategy: registrationStrategyDouble, 35 | configuration: configuration, 36 | clock: clockDouble) 37 | } 38 | 39 | override func tearDown() { 40 | trialProviderDouble = nil 41 | licenseProviderDouble = nil 42 | clockDouble = nil 43 | licenseInfoProvider = nil 44 | registrationStrategyDouble = nil 45 | super.tearDown() 46 | } 47 | 48 | let irrelevantLicense = License(name: "", licenseCode: "") 49 | 50 | func testLicenceInvalidity_NoLicense_ReturnsFalse() { 51 | 52 | XCTAssertFalse(licenseInfoProvider.isLicenseInvalid) 53 | } 54 | 55 | func testLicenceInvalidity_ValidLicense_ReturnsFalse() { 56 | 57 | registrationStrategyDouble.testIsValid = true 58 | licenseProviderDouble.testLicense = irrelevantLicense 59 | 60 | XCTAssertFalse(licenseInfoProvider.isLicenseInvalid) 61 | } 62 | 63 | func testLicenceInvalidity_InvalidLicense_ReturnsFalse() { 64 | 65 | registrationStrategyDouble.testIsValid = false 66 | licenseProviderDouble.testLicense = irrelevantLicense 67 | 68 | XCTAssert(licenseInfoProvider.isLicenseInvalid) 69 | } 70 | 71 | func testCurrentInfo_NoLicense_NoTrialPeriod_ReturnsTrialUp() { 72 | 73 | let licenseInfo = licenseInfoProvider.currentLicenseInformation 74 | 75 | let trialIsUp: Bool 76 | 77 | switch licenseInfo { 78 | case .trialUp: trialIsUp = true 79 | default: trialIsUp = false 80 | } 81 | 82 | XCTAssert(trialIsUp) 83 | } 84 | 85 | func testCurrentInfo_NoLicense_ActiveTrialPeriod_ReturnsOnTrial() { 86 | 87 | let endDate = Date() 88 | let expectedPeriod = TrialPeriod(startDate: Date(), endDate: Date()) 89 | clockDouble.testDate = endDate.addingTimeInterval(-1000) 90 | trialProviderDouble.testTrialPeriod = expectedPeriod 91 | 92 | let licenseInfo = licenseInfoProvider.currentLicenseInformation 93 | 94 | switch licenseInfo { 95 | case let .onTrial(trialPeriod): XCTAssertEqual(trialPeriod, expectedPeriod) 96 | default: XCTFail("expected to be onTrial, got \(licenseInfo)") 97 | } 98 | } 99 | 100 | func testCurrentInfo_NoLicense_PassedTrialPeriod_ReturnsTrialUp() { 101 | 102 | let endDate = Date() 103 | let expectedPeriod = TrialPeriod(startDate: Date(), endDate: Date()) 104 | clockDouble.testDate = endDate.addingTimeInterval(100) 105 | trialProviderDouble.testTrialPeriod = expectedPeriod 106 | 107 | let licenseInfo = licenseInfoProvider.currentLicenseInformation 108 | 109 | let trialIsUp: Bool 110 | switch licenseInfo { 111 | case .trialUp: trialIsUp = true 112 | default: trialIsUp = false 113 | } 114 | 115 | XCTAssert(trialIsUp) 116 | } 117 | 118 | func testCurrentInfo_WithInvalidLicense_NoTrial_ReturnsTrialUp() { 119 | 120 | registrationStrategyDouble.testIsValid = false 121 | licenseProviderDouble.testLicense = irrelevantLicense 122 | 123 | let licenseInfo = licenseInfoProvider.currentLicenseInformation 124 | 125 | let trialIsUp: Bool 126 | switch licenseInfo { 127 | case .trialUp: trialIsUp = true 128 | default: trialIsUp = false 129 | } 130 | 131 | XCTAssert(trialIsUp) 132 | } 133 | 134 | func testCurrentInfo_WithInvalidLicense_OnTrial_ReturnsTrial() { 135 | 136 | // Given 137 | registrationStrategyDouble.testIsValid = false 138 | licenseProviderDouble.testLicense = irrelevantLicense 139 | 140 | let startDate = Date(timeIntervalSince1970: 1000) 141 | let endDate = Date(timeIntervalSince1970: 9999) 142 | let expectedPeriod = TrialPeriod(startDate: startDate, endDate: endDate) 143 | clockDouble.testDate = endDate.addingTimeInterval(-1000) // rewind before end date 144 | trialProviderDouble.testTrialPeriod = expectedPeriod 145 | 146 | // When 147 | let licenseInfo = licenseInfoProvider.currentLicenseInformation 148 | 149 | // Then 150 | switch licenseInfo { 151 | case let .onTrial(trialPeriod): XCTAssertEqual(trialPeriod, expectedPeriod) 152 | default: XCTFail("expected to be onTrial, got \(licenseInfo)") 153 | } 154 | } 155 | 156 | func testCurrentInfo_WithValidLicense_NoTrial_ReturnsRegisteredWithInfo() { 157 | 158 | registrationStrategyDouble.testIsValid = true 159 | let name = "a name" 160 | let licenseCode = "a license code" 161 | let license = License(name: name, licenseCode: licenseCode) 162 | licenseProviderDouble.testLicense = license 163 | 164 | let licenseInfo = licenseInfoProvider.currentLicenseInformation 165 | 166 | switch licenseInfo { 167 | case let .registered(foundLicense): XCTAssertEqual(foundLicense, license) 168 | default: XCTFail("expected .registered(_)") 169 | } 170 | } 171 | 172 | func testCurrentInfo_WithValidLicense_OnTrial_ReturnsRegistered() { 173 | 174 | // Given 175 | registrationStrategyDouble.testIsValid = true 176 | 177 | let endDate = Date() 178 | let expectedPeriod = TrialPeriod(startDate: Date(), endDate: endDate) 179 | clockDouble.testDate = endDate.addingTimeInterval(-1000) 180 | trialProviderDouble.testTrialPeriod = expectedPeriod 181 | 182 | let name = "a name" 183 | let licenseCode = "a license code" 184 | let license = License(name: name, licenseCode: licenseCode) 185 | licenseProviderDouble.testLicense = license 186 | 187 | // When 188 | let licenseInfo = licenseInfoProvider.currentLicenseInformation 189 | 190 | // Then 191 | switch licenseInfo { 192 | case let .registered(foundLicense): XCTAssertEqual(foundLicense, license) 193 | default: XCTFail("expected .registered(_)") 194 | } 195 | } 196 | 197 | func testCurrentInfo_WithValidLicense_PassedTrial_ReturnsRegistered() { 198 | 199 | // Given 200 | registrationStrategyDouble.testIsValid = true 201 | let endDate = Date() 202 | let expectedPeriod = TrialPeriod(startDate: Date(), endDate: endDate) 203 | clockDouble.testDate = endDate.addingTimeInterval(+9999) 204 | trialProviderDouble.testTrialPeriod = expectedPeriod 205 | 206 | let name = "a name" 207 | let licenseCode = "a license code" 208 | let license = License(name: name, licenseCode: licenseCode) 209 | licenseProviderDouble.testLicense = license 210 | 211 | // When 212 | let licenseInfo = licenseInfoProvider.currentLicenseInformation 213 | 214 | // Then 215 | switch licenseInfo { 216 | case let .registered(foundLicense): XCTAssertEqual(foundLicense, license) 217 | default: XCTFail("expected .registered(_)") 218 | } 219 | } 220 | 221 | 222 | // MARK: - 223 | 224 | class TestTrialProvider: ProvidesTrial { 225 | 226 | var testTrialPeriod: TrialPeriod? 227 | var currentTrialPeriod: TrialPeriod? { 228 | return testTrialPeriod 229 | } 230 | } 231 | 232 | class TestLicenseProvider: ProvidesLicense { 233 | 234 | var testLicense: License? 235 | var currentLicense: License? { 236 | return testLicense 237 | } 238 | } 239 | 240 | class TestClock: KnowsTimeAndDate { 241 | 242 | var testDate: Date! 243 | func now() -> Date { 244 | 245 | return testDate 246 | } 247 | } 248 | 249 | class TestRegistrationStrategy: RegistrationStrategy { 250 | 251 | var testIsValid = false 252 | func isValid(payload: RegistrationPayload, configuration: LicenseConfiguration, licenseVerifier: LicenseCodeVerification) -> Bool { 253 | return testIsValid 254 | } 255 | } 256 | 257 | class NullVerifier: LicenseVerifier { 258 | 259 | init() { 260 | super.init(configuration: LicenseConfiguration(appName: "irrelevant app name", publicKey: "irrelevant key")) 261 | } 262 | 263 | override func isValid(licenseCode: String, registrationName: String) -> Bool { 264 | return false 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /Tests/TrialLicenseTests/LicenseInformationTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Cocoa 6 | import XCTest 7 | @testable import TrialLicense 8 | import Trial 9 | 10 | class LicenseInformationTests: XCTestCase { 11 | 12 | // MARK: Trial Up user info 13 | 14 | func testToUserInfo_TrialUp_SetsRegisteredToFalse() { 15 | 16 | let licenseInfo = LicenseInformation.trialUp 17 | 18 | let registered = licenseInfo.userInfo["registered"] as? Bool 19 | XCTAssert(hasValue(registered)) 20 | if let registered = registered { 21 | XCTAssert(registered == false) 22 | } 23 | } 24 | 25 | func testToUserInfo_TrialUp_SetsOnTrialToFalse() { 26 | 27 | let licenseInfo = LicenseInformation.trialUp 28 | 29 | let registered = licenseInfo.userInfo["on_trial"] as? Bool 30 | XCTAssert(hasValue(registered)) 31 | if let registered = registered { 32 | XCTAssert(registered == false) 33 | } 34 | } 35 | 36 | func testToUserInfo_TrialUp_HasNoNameKey() { 37 | 38 | let licenseInfo = LicenseInformation.trialUp 39 | 40 | XCTAssertFalse(hasValue(licenseInfo.userInfo["name"])) 41 | } 42 | 43 | func testToUserInfo_TrialUp_HasNoLicenseCodeKey() { 44 | 45 | let licenseInfo = LicenseInformation.trialUp 46 | 47 | XCTAssertFalse(hasValue(licenseInfo.userInfo["licenseCode"])) 48 | } 49 | 50 | func testToUserInfo_TrialUp_HasNoStartDateKey() { 51 | 52 | let licenseInfo = LicenseInformation.trialUp 53 | 54 | XCTAssertFalse(hasValue(licenseInfo.userInfo["trial_start_date"])) 55 | } 56 | 57 | func testToUserInfo_TrialUp_HasNoEndDateKey() { 58 | 59 | let licenseInfo = LicenseInformation.trialUp 60 | 61 | XCTAssertFalse(hasValue(licenseInfo.userInfo["trial_end_date"])) 62 | } 63 | 64 | 65 | // MARK: On Trial user info 66 | 67 | let trialPeriod = TrialPeriod(startDate: Date(timeIntervalSince1970: 1234), endDate: Date(timeIntervalSince1970: 98765)) 68 | 69 | func testToUserInfo_OnTrial_SetsRegisteredToFalse() { 70 | 71 | let licenseInfo = LicenseInformation.onTrial(trialPeriod) 72 | 73 | let registered = licenseInfo.userInfo["registered"] as? Bool 74 | XCTAssert(hasValue(registered)) 75 | if let registered = registered { 76 | XCTAssert(registered == false) 77 | } 78 | } 79 | 80 | func testToUserInfo_OnTrial_SetsOnTrialToTrue() { 81 | 82 | let licenseInfo = LicenseInformation.onTrial(trialPeriod) 83 | 84 | let registered = licenseInfo.userInfo["on_trial"] as? Bool 85 | XCTAssert(hasValue(registered)) 86 | if let registered = registered { 87 | XCTAssert(registered == true) 88 | } 89 | } 90 | 91 | func testToUserInfo_OnTrial_HasNoNameKey() { 92 | 93 | let licenseInfo = LicenseInformation.onTrial(trialPeriod) 94 | 95 | XCTAssertFalse(hasValue(licenseInfo.userInfo["name"])) 96 | } 97 | 98 | func testToUserInfo_OnTrial_HasNoLicenseCodeKey() { 99 | 100 | let licenseInfo = LicenseInformation.onTrial(trialPeriod) 101 | 102 | XCTAssertFalse(hasValue(licenseInfo.userInfo["licenseCode"])) 103 | } 104 | 105 | func testToUserInfo_OnTrial_SetsStartDate() { 106 | 107 | let licenseInfo = LicenseInformation.onTrial(trialPeriod) 108 | 109 | let startDate = licenseInfo.userInfo["trial_start_date"] as? Date 110 | XCTAssert(hasValue(startDate)) 111 | if let startDate = startDate { 112 | XCTAssertEqual(startDate, trialPeriod.startDate) 113 | } 114 | } 115 | 116 | func testToUserInfo_OnTrial_SetsEndDate() { 117 | 118 | let licenseInfo = LicenseInformation.onTrial(trialPeriod) 119 | 120 | let endDate = licenseInfo.userInfo["trial_end_date"] as? Date 121 | XCTAssert(hasValue(endDate)) 122 | if let startDate = endDate { 123 | XCTAssertEqual(startDate, trialPeriod.endDate) 124 | } 125 | } 126 | 127 | 128 | // MARK: Registered user info 129 | 130 | let licenseWithName = License(name: "a name", licenseCode: "a license code") 131 | let licenseWithoutName = License(name: nil, licenseCode: "another license code") 132 | 133 | func testToUserInfo_RegisteredWithName_SetsRegisteredToTrue() { 134 | 135 | let licenseInfo = LicenseInformation.registered(licenseWithName) 136 | 137 | let registered = licenseInfo.userInfo["registered"] as? Bool 138 | XCTAssert(hasValue(registered)) 139 | if let registered = registered { 140 | XCTAssert(registered) 141 | } 142 | } 143 | 144 | func testToUserInfo_RegisteredWithName_SetsOnTrialToFalse() { 145 | 146 | let licenseInfo = LicenseInformation.registered(licenseWithName) 147 | 148 | let registered = licenseInfo.userInfo["on_trial"] as? Bool 149 | XCTAssert(hasValue(registered)) 150 | if let registered = registered { 151 | XCTAssert(registered == false) 152 | } 153 | } 154 | 155 | func testToUserInfo_RegisteredWithName_SetsNameKeyToLicense() { 156 | 157 | let licenseInfo = LicenseInformation.registered(licenseWithName) 158 | 159 | let name = licenseInfo.userInfo["name"] as? String 160 | XCTAssert(hasValue(name)) 161 | if let name = name { 162 | XCTAssertEqual(name, licenseWithName.name) 163 | } 164 | } 165 | 166 | func testToUserInfo_RegisteredWithName_SetsLicenseCodeKeyToLicense() { 167 | 168 | let licenseInfo = LicenseInformation.registered(licenseWithName) 169 | 170 | let licenseCode = licenseInfo.userInfo["licenseCode"] as? String 171 | XCTAssert(hasValue(licenseCode)) 172 | if let licenseCode = licenseCode { 173 | XCTAssertEqual(licenseCode, licenseWithName.licenseCode) 174 | } 175 | } 176 | 177 | func testToUserInfo_RegisteredWithName_HasNoStartDateKey() { 178 | 179 | let licenseInfo = LicenseInformation.registered(licenseWithName) 180 | 181 | XCTAssertFalse(hasValue(licenseInfo.userInfo["trial_start_date"])) 182 | } 183 | 184 | func testToUserInfo_RegisteredWithName_HasNoEndDateKey() { 185 | 186 | let licenseInfo = LicenseInformation.registered(licenseWithName) 187 | 188 | XCTAssertFalse(hasValue(licenseInfo.userInfo["trial_end_date"])) 189 | } 190 | 191 | func testToUserInfo_RegisteredWithoutName_SetsRegisteredToTrue() { 192 | 193 | let licenseInfo = LicenseInformation.registered(licenseWithoutName) 194 | 195 | let registered = licenseInfo.userInfo["registered"] as? Bool 196 | XCTAssert(hasValue(registered)) 197 | if let registered = registered { 198 | XCTAssert(registered) 199 | } 200 | } 201 | 202 | func testToUserInfo_RegisteredWithoutName_SetsOnTrialToFalse() { 203 | 204 | let licenseInfo = LicenseInformation.registered(licenseWithoutName) 205 | 206 | let registered = licenseInfo.userInfo["on_trial"] as? Bool 207 | XCTAssert(hasValue(registered)) 208 | if let registered = registered { 209 | XCTAssert(registered == false) 210 | } 211 | } 212 | 213 | func testToUserInfo_RegisteredWithoutName_HasNoNameKey() { 214 | 215 | let licenseInfo = LicenseInformation.registered(licenseWithoutName) 216 | 217 | XCTAssertFalse(hasValue(licenseInfo.userInfo["name"] as? String)) 218 | } 219 | 220 | func testToUserInfo_RegisteredWithoutName_SetsLicenseCodeKeyToLicense() { 221 | 222 | let licenseInfo = LicenseInformation.registered(licenseWithoutName) 223 | 224 | let licenseCode = licenseInfo.userInfo["licenseCode"] as? String 225 | XCTAssert(hasValue(licenseCode)) 226 | if let licenseCode = licenseCode { 227 | XCTAssertEqual(licenseCode, licenseWithoutName.licenseCode) 228 | } 229 | } 230 | 231 | func testToUserInfo_RegisteredWithoutName_HasNoStartDateKey() { 232 | 233 | let licenseInfo = LicenseInformation.registered(licenseWithoutName) 234 | 235 | XCTAssertFalse(hasValue(licenseInfo.userInfo["trial_start_date"])) 236 | } 237 | 238 | func testToUserInfo_RegisteredWithoutName_HasNoEndDateKey() { 239 | 240 | let licenseInfo = LicenseInformation.registered(licenseWithoutName) 241 | 242 | XCTAssertFalse(hasValue(licenseInfo.userInfo["trial_end_date"])) 243 | } 244 | 245 | 246 | // MARK: - 247 | 248 | typealias UserInfo = LicenseInformation.UserInfo 249 | 250 | func testFromUserInfo_EmptyUserInfo_ReturnsNil() { 251 | 252 | let userInfo = UserInfo() 253 | 254 | let result = LicenseInformation(userInfo: userInfo) 255 | 256 | XCTAssertFalse(hasValue(result)) 257 | } 258 | 259 | func testFromUserInfo_UserInfoWithoutRegistered_ReturnsNil() { 260 | 261 | let userInfo: UserInfo = ["name" : "foo", "licenseCode" : "bar"] 262 | 263 | let result = LicenseInformation(userInfo: userInfo) 264 | 265 | XCTAssertFalse(hasValue(result)) 266 | } 267 | 268 | 269 | // MARK: From user info to TrialUp 270 | 271 | func testFromUserInfo_UnregisteredUserInfo_ReturnsNil() { 272 | 273 | let userInfo: UserInfo = ["registered" : false] 274 | 275 | let result = LicenseInformation(userInfo: userInfo) 276 | 277 | XCTAssertFalse(hasValue(result)) 278 | } 279 | 280 | func testFromUserInfo_UnregisteredUserInfo_NotOnTrial_ReturnsTrialUp() { 281 | 282 | let userInfo: UserInfo = ["registered" : false, "on_trial" : false] 283 | 284 | let result = LicenseInformation(userInfo: userInfo) 285 | 286 | XCTAssert(hasValue(result)) 287 | if let result = result { 288 | 289 | let valid: Bool 290 | switch result { 291 | case .trialUp: valid = true 292 | default: valid = false 293 | } 294 | 295 | XCTAssert(valid) 296 | } 297 | } 298 | 299 | func testFromUserInfo_UnregisteredUserInfo_NotOnTrial_WithAdditionalInfo_ReturnsTrialUp() { 300 | 301 | let userInfo: UserInfo = ["registered" : false, "on_trial" : false, "bogus" : 123] 302 | 303 | let result = LicenseInformation(userInfo: userInfo) 304 | 305 | XCTAssert(hasValue(result)) 306 | if let result = result { 307 | 308 | let valid: Bool 309 | switch result { 310 | case .trialUp: valid = true 311 | default: valid = false 312 | } 313 | 314 | XCTAssert(valid) 315 | } 316 | } 317 | 318 | 319 | // MARK: From user info to On Trial 320 | 321 | func testFromUserInfo_Unregistered_OnTrialOnly_ReturnsNil() { 322 | 323 | let userInfo: UserInfo = ["registered" : false, "on_trial" : true] 324 | 325 | let result = LicenseInformation(userInfo: userInfo) 326 | 327 | XCTAssertFalse(hasValue(result)) 328 | } 329 | 330 | func testFromUserInfo_UnregisteredOnTrial_WithStartDateOnly_ReturnsNil() { 331 | 332 | let userInfo: UserInfo = ["registered" : false, "on_trial" : true, "trial_start_date" : NSDate()] 333 | 334 | let result = LicenseInformation(userInfo: userInfo) 335 | 336 | XCTAssertFalse(hasValue(result)) 337 | } 338 | 339 | func testFromUserInfo_UnregisteredOnTrial_WithEndDateOnly_ReturnsNil() { 340 | 341 | let userInfo: UserInfo = ["registered" : false, "on_trial" : true, "trial_end_date" : NSDate()] 342 | 343 | let result = LicenseInformation(userInfo: userInfo) 344 | 345 | XCTAssertFalse(hasValue(result)) 346 | } 347 | 348 | func testFromUserInfo_UnregisteredOnTrial_WithStartAndEndDate_ReturnsOnTrial() { 349 | 350 | let startDate = Date(timeIntervalSinceReferenceDate: 1000) 351 | let endDate = Date(timeIntervalSinceReferenceDate: 9000) 352 | let userInfo: UserInfo = ["registered" : false, "on_trial" : true, "trial_start_date" : startDate, "trial_end_date" : endDate] 353 | 354 | let result = LicenseInformation(userInfo: userInfo) 355 | 356 | switch result { 357 | case let .some(.onTrial(trialPeriod)): 358 | XCTAssertEqual(trialPeriod.startDate, startDate) 359 | XCTAssertEqual(trialPeriod.endDate, endDate) 360 | default: 361 | XCTFail("expected OnTrial") 362 | } 363 | } 364 | 365 | 366 | func testFromUserInfo_UnregisteredOnTrial_WithStartAndEndDateAndAdditionalData_ReturnsOnTrial() { 367 | 368 | let startDate = Date(timeIntervalSinceReferenceDate: 1000) 369 | let endDate = Date(timeIntervalSinceReferenceDate: 9000) 370 | let userInfo: UserInfo = ["registered" : false, "on_trial" : true, "trial_start_date" : startDate, "trial_end_date" : endDate, "to be ignored" : "stuff"] 371 | 372 | let result = LicenseInformation(userInfo: userInfo) 373 | 374 | switch result { 375 | case let .some(.onTrial(trialPeriod)): 376 | XCTAssertEqual(trialPeriod.startDate, startDate) 377 | XCTAssertEqual(trialPeriod.endDate, endDate) 378 | default: 379 | XCTFail("expected OnTrial") 380 | } 381 | } 382 | 383 | 384 | // MARK: From user info to Registered 385 | 386 | func testFromUserInfo_RegisteredUserInfo_WithoutDetails_ReturnsNil() { 387 | 388 | let userInfo: UserInfo = ["registered" : true] 389 | 390 | let result = LicenseInformation(userInfo: userInfo) 391 | 392 | XCTAssertFalse(hasValue(result)) 393 | } 394 | 395 | func testFromUserInfo_RegisteredUserInfo_WithNameOnly_ReturnsNil() { 396 | 397 | let userInfo: UserInfo = ["registered" : true, "name" : "a name"] 398 | 399 | let result = LicenseInformation(userInfo: userInfo) 400 | 401 | XCTAssertFalse(hasValue(result)) 402 | } 403 | 404 | func testFromUserInfo_RegisteredUserInfo_WithLicenseCodeOnly_ReturnsLicense() { 405 | 406 | let licenseCode = "the license code" 407 | let userInfo: UserInfo = ["registered" : true, "licenseCode" : licenseCode] 408 | 409 | let result = LicenseInformation(userInfo: userInfo) 410 | 411 | switch result { 412 | case let .some(.registered(license)): 413 | XCTAssertNil(license.name) 414 | XCTAssertEqual(license.licenseCode, licenseCode) 415 | default: 416 | XCTFail("expected Registered") 417 | } 418 | } 419 | 420 | func testFromUserInfo_RegisteredUserInfo_WithpersonalizedLicense_ReturnsRegistered() { 421 | 422 | let name = "the name" 423 | let licenseCode = "the license code" 424 | let userInfo: UserInfo = ["registered" : true, "name" : name, "licenseCode" : licenseCode] 425 | 426 | let result = LicenseInformation(userInfo: userInfo) 427 | 428 | switch result { 429 | case let .some(.registered(license)): 430 | XCTAssertEqual(license.name, name) 431 | XCTAssertEqual(license.licenseCode, licenseCode) 432 | default: 433 | XCTFail("expected Registered") 434 | } 435 | } 436 | 437 | func testFromUserInfo_RegisteredUserInfo_WithpersonalizedLicenseAndAdditionalData_ReturnsRegistered() { 438 | 439 | let name = "the name" 440 | let licenseCode = "the license code" 441 | let userInfo: UserInfo = ["registered" : true, "name" : name, "licenseCode" : licenseCode, "irrelevant" : 999] 442 | 443 | let result = LicenseInformation(userInfo: userInfo) 444 | 445 | switch result { 446 | case let .some(.registered(license)): 447 | XCTAssertEqual(license.name, name) 448 | XCTAssertEqual(license.licenseCode, licenseCode) 449 | default: 450 | XCTFail("expected Registered") 451 | } 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /Tests/TrialLicenseTests/LicenseVerifierTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Cocoa 6 | import XCTest 7 | @testable import TrialLicense 8 | 9 | fileprivate let publicKey: String = { 10 | 11 | var parts = [String]() 12 | 13 | parts.append("-----BEGIN DSA PUBLIC KEY-----\n") 14 | parts.append("MIHwMIGoBgcqhkjOOAQBMIGcAkEAoKLaPXkgAPng5YtV") 15 | parts.append("G14BUE1I5Q") 16 | parts.append("aGesaf9PTC\nnmUlYMp4m7M") 17 | parts.append("rVC2/YybXE") 18 | parts.append("QlaILBZBmyw+A4Kps2k/T12q") 19 | parts.append("L8EUwIVAPxEzzlcqbED\nKaw6oJ9THk1i4Lu") 20 | parts.append("TAkAG") 21 | parts.append("RPr6HheNNnH9GQZGjCuv") 22 | parts.append("6pLUOBo64QJ0WNEs2c9QOSBU\nHpWZU") 23 | parts.append("m8bGMQevt38P") 24 | parts.append("iSZZwU0hCAJ6pd09eeTP983A0MAAkB+yDfp+53KPSk") 25 | parts.append("5dH") 26 | parts.append("xh\noBm6kTBKsYk") 27 | parts.append("xonpPlBrFJTJeyvZInHIKrd0N8Du") 28 | parts.append("i3XKDtqrLWPIQcM0mWOj") 29 | parts.append("YHUlf\nUpIg\n") 30 | parts.append("-----END DSA PUBLIC KEY-----\n") 31 | 32 | let publicKey = parts.joined(separator: "") 33 | 34 | return publicKey 35 | }() 36 | 37 | fileprivate let appName = "MyNewApp" 38 | 39 | fileprivate func personalizedRegistrationName(licenseeName: String) -> String { 40 | return LicensingScheme.personalizedLicense.registrationName( 41 | appName: appName, 42 | payload: RegistrationPayload(name: licenseeName, licenseCode: "irrelevant")) 43 | } 44 | 45 | class LicenseVerifierTests: XCTestCase { 46 | 47 | var verifier: LicenseVerifier! 48 | 49 | override func setUp() { 50 | super.setUp() 51 | let configuration = LicenseConfiguration(appName: appName, publicKey: publicKey) 52 | verifier = LicenseVerifier(configuration: configuration) 53 | } 54 | 55 | override func tearDown() { 56 | verifier = nil 57 | super.tearDown() 58 | } 59 | 60 | func testVerify_EmptyStrings_ReturnsFalse() { 61 | 62 | let result = verifier.isValid(licenseCode: "", registrationName: "") 63 | 64 | XCTAssertFalse(result) 65 | } 66 | 67 | // MARK: Personalized license 68 | 69 | var validPersonalizedLicense: License { 70 | return License( 71 | name: "John Appleseed", 72 | licenseCode: "GAWQE-FABU3-HNQXA-B7EGM-34X2E-DGMT4-4F44R-9PUQC-CUANX-FXMCZ-4536Y-QKX9D-PU2C3-QG2ZA-U88NJ-Q") 73 | } 74 | 75 | func testVerifyPersonalizedLicense_ValidCodeWrongName_ReturnsFalse() { 76 | 77 | let result = verifier.isValid(licenseCode: validPersonalizedLicense.licenseCode, 78 | registrationName: personalizedRegistrationName(licenseeName: "Jon Snow")) 79 | 80 | XCTAssertFalse(result) 81 | } 82 | 83 | func testVerifyPersonalizedLicense_ValidLicense_ReturnsTrue() { 84 | 85 | let result = verifier.isValid(licenseCode: validPersonalizedLicense.licenseCode, 86 | registrationName: personalizedRegistrationName(licenseeName: validPersonalizedLicense.name!)) 87 | 88 | XCTAssert(result) 89 | } 90 | 91 | func testVerifyPersonalizedLicense_ValidLicenseWrongAppName_ReturnsFalse() { 92 | 93 | let registrationNameForWrongApp = LicensingScheme.personalizedLicense.registrationName( 94 | appName: "totally-wrong-app-name", 95 | payload: RegistrationPayload(name: validPersonalizedLicense.name!, licenseCode: "irrelevant")) 96 | 97 | let result = verifier.isValid(licenseCode: validPersonalizedLicense.licenseCode, 98 | registrationName: registrationNameForWrongApp) 99 | 100 | XCTAssertFalse(result) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Tests/TrialLicenseTests/LicensingSchemeTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Cocoa 6 | import XCTest 7 | @testable import TrialLicense 8 | 9 | class LicensingSchemeTests: XCTestCase { 10 | 11 | func testPersonalizedRegistrationName() { 12 | let appName = "foobar" 13 | let name = "person" 14 | let scheme = LicensingScheme.personalizedLicense 15 | 16 | XCTAssertEqual( 17 | "\(appName),\(name)", 18 | scheme.registrationName( 19 | appName: appName, 20 | payload: RegistrationPayload(name: name, licenseCode: "irrelevant"))) 21 | XCTAssertEqual( 22 | "\(appName),", 23 | scheme.registrationName( 24 | appName: appName, 25 | payload: RegistrationPayload(licenseCode: "irrelevant"))) 26 | } 27 | 28 | func testGenericRegistrationName() { 29 | let appName = "foobar" 30 | let scheme = LicensingScheme.generic 31 | 32 | XCTAssertEqual( 33 | "\(appName)", 34 | scheme.registrationName( 35 | appName: appName, 36 | payload: RegistrationPayload(name: "irrelevant", licenseCode: "irrelevant"))) 37 | XCTAssertEqual( 38 | "\(appName)", 39 | scheme.registrationName( 40 | appName: appName, 41 | payload: RegistrationPayload(licenseCode: "irrelevant"))) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Tests/TrialLicenseTests/PersonalizedLicenseRegistrationStrategyTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Cocoa 6 | import XCTest 7 | @testable import TrialLicense 8 | 9 | class PersonalizedLicenseRegistrationStrategyTests: XCTestCase { 10 | 11 | var verifierDouble: TestVerifier! 12 | 13 | override func setUp() { 14 | super.setUp() 15 | verifierDouble = TestVerifier() 16 | } 17 | 18 | override func tearDown() { 19 | verifierDouble = nil 20 | super.tearDown() 21 | } 22 | 23 | 24 | // MARK: - 25 | 26 | func testIsValid_WithoutName_PassesDataToVerifier() { 27 | 28 | let appName = "AmazingAppName2000" 29 | let licenseCode = "supposed to be a license code" 30 | let payload = RegistrationPayload(licenseCode: licenseCode) 31 | 32 | _ = PersonalizedLicenseRegistrationStrategy().isValid( 33 | payload: payload, 34 | configuration: LicenseConfiguration(appName: appName, publicKey: "irrelevant"), 35 | licenseVerifier: verifierDouble) 36 | 37 | XCTAssertNotNil(verifierDouble.didCallIsValidWith) 38 | if let values = verifierDouble.didCallIsValidWith { 39 | let expectedRegistrationName = LicensingScheme.personalizedLicense.registrationName(appName: appName, payload: payload) 40 | XCTAssertEqual(values.registrationName, expectedRegistrationName) 41 | XCTAssertEqual(values.licenseCode, licenseCode) 42 | } 43 | } 44 | 45 | func testIsValid_WithName_PassesDataToVerifier() { 46 | 47 | let appName = "AmazingAppName2000" 48 | let name = "a person" 49 | let licenseCode = "supposed to be a license code" 50 | let payload = RegistrationPayload(name: name, licenseCode: licenseCode) 51 | 52 | _ = PersonalizedLicenseRegistrationStrategy().isValid( 53 | payload: payload, 54 | configuration: LicenseConfiguration(appName: appName, publicKey: "irrelevant"), 55 | licenseVerifier: verifierDouble) 56 | 57 | XCTAssertNotNil(verifierDouble.didCallIsValidWith) 58 | if let values = verifierDouble.didCallIsValidWith { 59 | let expectedRegistrationName = LicensingScheme.personalizedLicense.registrationName(appName: appName, payload: payload) 60 | XCTAssertEqual(values.registrationName, expectedRegistrationName) 61 | XCTAssertEqual(values.licenseCode, licenseCode) 62 | } 63 | } 64 | 65 | func testIsValid_ReturnsVerifierResult() { 66 | 67 | let irrelevantPayload = RegistrationPayload(name: "irrelevant", licenseCode: "irrelevant") 68 | let irrelevantConfiguration = LicenseConfiguration(appName: "irrelevant", publicKey: "irrelevant") 69 | 70 | verifierDouble.testValidity = true 71 | XCTAssertTrue(PersonalizedLicenseRegistrationStrategy().isValid( 72 | payload: irrelevantPayload, 73 | configuration: irrelevantConfiguration, 74 | licenseVerifier: verifierDouble)) 75 | 76 | verifierDouble.testValidity = false 77 | XCTAssertFalse(PersonalizedLicenseRegistrationStrategy().isValid( 78 | payload: irrelevantPayload, 79 | configuration: irrelevantConfiguration, 80 | licenseVerifier: verifierDouble)) 81 | } 82 | 83 | 84 | // MARK: - 85 | 86 | class TestVerifier: LicenseVerifier { 87 | 88 | init() { 89 | super.init(configuration: LicenseConfiguration(appName: "irrelevant app name", publicKey: "irrelevant key")) 90 | } 91 | 92 | var testValidity = false 93 | var didCallIsValidWith: (licenseCode: String, registrationName: String)? 94 | override func isValid(licenseCode: String, registrationName: String) -> Bool { 95 | 96 | didCallIsValidWith = (licenseCode, registrationName) 97 | 98 | return testValidity 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/TrialLicenseTests/RegisterApplicationTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Cocoa 6 | import XCTest 7 | @testable import TrialLicense 8 | import Trial 9 | 10 | class RegisterApplicationTests: XCTestCase { 11 | 12 | var service: RegisterApplication! 13 | 14 | var verifierDouble: TestVerifier! 15 | var writerDouble: TestWriter! 16 | var licenseChangeCallback: LicenseChangeCallbackDouble! 17 | var invalidLicenseCallback: InvalidLicenseCallbackDouble! 18 | var informationProviderDouble: TestLicenseInformationProvider! 19 | var trialProviderDouble: TestTrialProvider! 20 | var registrationStrategyDouble: TestRegistrationStrategy! 21 | 22 | var configuration: LicenseConfiguration { 23 | return LicenseConfiguration(appName: "theAppName", publicKey: "irrelevant") 24 | } 25 | 26 | override func setUp() { 27 | 28 | super.setUp() 29 | 30 | verifierDouble = TestVerifier() 31 | writerDouble = TestWriter() 32 | licenseChangeCallback = LicenseChangeCallbackDouble() 33 | invalidLicenseCallback = InvalidLicenseCallbackDouble() 34 | informationProviderDouble = TestLicenseInformationProvider() 35 | trialProviderDouble = TestTrialProvider() 36 | registrationStrategyDouble = TestRegistrationStrategy() 37 | 38 | service = RegisterApplication( 39 | licenseVerifier: verifierDouble, 40 | licenseWriter: writerDouble, 41 | licenseInformationProvider: informationProviderDouble, 42 | trialProvider: trialProviderDouble, 43 | registrationStrategy: registrationStrategyDouble, 44 | configuration: configuration, 45 | licenseChangeCallback: licenseChangeCallback.receive, 46 | invalidLicenseCallback: invalidLicenseCallback.receive) 47 | } 48 | 49 | override func tearDown() { 50 | service = nil 51 | verifierDouble = nil 52 | writerDouble = nil 53 | licenseChangeCallback = nil 54 | invalidLicenseCallback = nil 55 | informationProviderDouble = nil 56 | trialProviderDouble = nil 57 | registrationStrategyDouble = nil 58 | super.tearDown() 59 | } 60 | 61 | var irrelevantName: String { return "irrelevant" } 62 | var irrelevantLicenseCode: String { return "irrelevant" } 63 | var irrelevantLicense: License { return License(name: "irrelevant", licenseCode: "irrelevant") } 64 | var irrelevantTrialPeriod: TrialPeriod { return TrialPeriod(startDate: Date(timeIntervalSince1970: 1234), endDate: Date(timeIntervalSince1970: 9999)) } 65 | var irrelevantPayload: RegistrationPayload { return RegistrationPayload(licenseCode: irrelevantLicenseCode) } 66 | 67 | 68 | // MARK: - Register 69 | 70 | func testRegister_VerifiesWithStrategy() { 71 | 72 | let payload = RegistrationPayload(name: "a name", licenseCode: "123-456") 73 | 74 | service.register(payload: payload) 75 | 76 | XCTAssertNotNil(registrationStrategyDouble.didTestValidity) 77 | if let values = registrationStrategyDouble.didTestValidity { 78 | 79 | XCTAssertEqual(values.payload, payload) 80 | XCTAssert(values.licenseVerifier === verifierDouble) 81 | XCTAssertEqual(values.configuration, configuration) 82 | } 83 | } 84 | 85 | func testRegister_InvalidLicense_DoesntTryToStore() { 86 | 87 | registrationStrategyDouble.testIsValid = false 88 | 89 | service.register(payload: irrelevantPayload) 90 | 91 | XCTAssertNil(writerDouble.didStoreWith) 92 | } 93 | 94 | func testRegister_InvalidLicense_DoesntBroadcastChange() { 95 | 96 | verifierDouble.testValidity = false 97 | 98 | service.register(payload: irrelevantPayload) 99 | 100 | XCTAssertNil(licenseChangeCallback.didReceiveWith) 101 | } 102 | 103 | func testRegister_InvalidLicense_TriggersInvalidLicenseCallback() { 104 | 105 | let payload = RegistrationPayload(name: "the name", licenseCode: "the code") 106 | registrationStrategyDouble.testIsValid = false 107 | 108 | service.register(payload: payload) 109 | 110 | XCTAssertEqual(invalidLicenseCallback.didReceivePayload, payload) 111 | } 112 | 113 | func testRegister_ValidLicense_DelegatesToStore() { 114 | 115 | let payload = RegistrationPayload(name: "It's Me", licenseCode: "0900-ACME") 116 | registrationStrategyDouble.testIsValid = true 117 | 118 | service.register(payload: payload) 119 | 120 | XCTAssertNotNil(writerDouble.didStoreWith) 121 | if let values = writerDouble.didStoreWith { 122 | 123 | XCTAssertEqual(values.name, payload.name) 124 | XCTAssertEqual(values.licenseCode, payload.licenseCode) 125 | } 126 | } 127 | 128 | func testRegister_ValidLicense_BroadcastsChange() { 129 | 130 | let payload = RegistrationPayload(name: "Hello again", licenseCode: "fr13nd-001") 131 | registrationStrategyDouble.testIsValid = true 132 | 133 | service.register(payload: payload) 134 | 135 | XCTAssertNotNil(licenseChangeCallback.didReceiveWith) 136 | if let licenseInfo = licenseChangeCallback.didReceiveWith { 137 | 138 | switch licenseInfo { 139 | case let .registered(license): 140 | XCTAssertEqual(license.name, payload.name) 141 | XCTAssertEqual(license.licenseCode, payload.licenseCode) 142 | default: XCTFail("should be registered") 143 | } 144 | } 145 | } 146 | 147 | 148 | // MARK: Unregister 149 | 150 | func testUnregister_CurrentlyRegistered_RemovesLicenseFromWriter() { 151 | 152 | informationProviderDouble.testCurrentLicenseInformation = .registered(irrelevantLicense) 153 | 154 | service.unregister() 155 | 156 | XCTAssert(writerDouble.didRemove) 157 | } 158 | 159 | func testUnregister_CurrentlyRegistered_TrialIsUp_InvokesCallback() { 160 | 161 | informationProviderDouble.testCurrentLicenseInformation = .registered(irrelevantLicense) 162 | trialProviderDouble.testCurrentTrialPeriod = nil 163 | 164 | service.unregister() 165 | 166 | XCTAssertEqual(licenseChangeCallback.didReceiveWith, LicenseInformation.trialUp) 167 | } 168 | 169 | func testUnregister_CurrentlyRegistered_TrialDaysLeft_InvokesCallback() { 170 | 171 | informationProviderDouble.testCurrentLicenseInformation = .registered(irrelevantLicense) 172 | let trialPeriod = TrialPeriod(startDate: Date(timeIntervalSinceReferenceDate: -100), endDate: Date(timeIntervalSinceReferenceDate: 200)) 173 | trialProviderDouble.testCurrentTrialPeriod = trialPeriod 174 | 175 | service.unregister() 176 | 177 | XCTAssertEqual(licenseChangeCallback.didReceiveWith, LicenseInformation.onTrial(trialPeriod)) 178 | } 179 | 180 | func testUnregister_CurrentlyOnTrial_RemovesLicenseFromWriter() { 181 | 182 | informationProviderDouble.testCurrentLicenseInformation = .onTrial(irrelevantTrialPeriod) 183 | 184 | service.unregister() 185 | 186 | XCTAssert(writerDouble.didRemove) 187 | } 188 | 189 | func testUnregister_CurrentlyOnTrial_DoesNotInvokeCallback() { 190 | 191 | informationProviderDouble.testCurrentLicenseInformation = .onTrial(irrelevantTrialPeriod) 192 | 193 | service.unregister() 194 | 195 | XCTAssertNil(licenseChangeCallback.didReceiveWith) 196 | } 197 | 198 | func testUnregister_CurrentlyTrialIsUp_RemovesLicenseFromWriter() { 199 | 200 | informationProviderDouble.testCurrentLicenseInformation = .trialUp 201 | 202 | service.unregister() 203 | 204 | XCTAssert(writerDouble.didRemove) 205 | } 206 | 207 | func testUnregister_CurrentlyTrialIsUp_DoesNotInvokeCallback() { 208 | 209 | informationProviderDouble.testCurrentLicenseInformation = .trialUp 210 | 211 | service.unregister() 212 | 213 | XCTAssertNil(licenseChangeCallback.didReceiveWith) 214 | } 215 | 216 | 217 | // MARK: - 218 | 219 | class TestWriter: WritesLicense { 220 | 221 | var didStoreWith: (licenseCode: String, name: String?)? 222 | func store(licenseCode: String, forName name: String?) { 223 | didStoreWith = (licenseCode, name) 224 | } 225 | 226 | var didRemove = false 227 | func removeLicense() { 228 | didRemove = true 229 | } 230 | } 231 | 232 | class TestVerifier: LicenseVerifier { 233 | 234 | init() { 235 | super.init(configuration: LicenseConfiguration(appName: "irrelevant app name", publicKey: "irrelevant key")) 236 | } 237 | 238 | var testValidity = false 239 | override func isValid(licenseCode: String, registrationName: String) -> Bool { 240 | return false 241 | } 242 | } 243 | 244 | class TestTrialProvider: ProvidesTrial { 245 | 246 | var testCurrentTrialPeriod: TrialPeriod? = nil 247 | var currentTrialPeriod: TrialPeriod? { 248 | return testCurrentTrialPeriod 249 | } 250 | 251 | var testCurrentTrial: Trial? = nil 252 | var didRequestCurrentTrial: KnowsTimeAndDate? 253 | func currentTrial(clock: KnowsTimeAndDate) -> Trial? { 254 | didRequestCurrentTrial = clock 255 | return testCurrentTrial 256 | } 257 | } 258 | 259 | class TestLicenseInformationProvider: ProvidesLicenseInformation { 260 | 261 | init() { } 262 | 263 | var testIsLicenseInvalid = false 264 | var isLicenseInvalid: Bool { return testIsLicenseInvalid } 265 | 266 | var testCurrentLicenseInformation: LicenseInformation = .trialUp 267 | var currentLicenseInformation: LicenseInformation { return testCurrentLicenseInformation } 268 | } 269 | 270 | class LicenseChangeCallbackDouble { 271 | 272 | var didReceiveWith: LicenseInformation? 273 | func receive(licenseInformation: LicenseInformation) { 274 | 275 | didReceiveWith = licenseInformation 276 | } 277 | } 278 | 279 | class InvalidLicenseCallbackDouble { 280 | 281 | var didReceivePayload: RegistrationPayload? 282 | func receive(payload: RegistrationPayload) { 283 | 284 | didReceivePayload = (payload) 285 | } 286 | } 287 | 288 | class TestRegistrationStrategy: RegistrationStrategy { 289 | 290 | var testIsValid = false 291 | var didTestValidity: (payload: RegistrationPayload, configuration: LicenseConfiguration, licenseVerifier: LicenseCodeVerification)? 292 | func isValid(payload: RegistrationPayload, configuration: LicenseConfiguration, licenseVerifier: LicenseCodeVerification) -> Bool { 293 | didTestValidity = (payload, configuration, licenseVerifier) 294 | return testIsValid 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /Tests/TrialLicenseTests/UserDefaultsLicenseProviderTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import XCTest 6 | @testable import TrialLicense 7 | 8 | class UserDefaultsLicenseProviderTests: XCTestCase { 9 | 10 | var licenseProvider: UserDefaultsLicenseProvider! 11 | var userDefaultsDouble: TestUserDefaults! 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | userDefaultsDouble = TestUserDefaults() 17 | 18 | licenseProvider = UserDefaultsLicenseProvider(userDefaults: userDefaultsDouble, removingWhitespace: true) 19 | } 20 | 21 | override func tearDown() { 22 | userDefaultsDouble = nil 23 | licenseProvider = nil 24 | super.tearDown() 25 | } 26 | 27 | func provideLicenseDefaults(name: String?, licenseCode: String?) { 28 | userDefaultsDouble.testValues[License.UserDefaultsKeys.name] = name 29 | userDefaultsDouble.testValues[License.UserDefaultsKeys.licenseCode] = licenseCode 30 | } 31 | 32 | 33 | // MARK: - 34 | // MARK: Empty Defaults, no License 35 | 36 | func testObtainingCurrentLicense_WithEmptyDefaults_QueriesDefaultsForLicenseCode() { 37 | 38 | _ = licenseProvider.currentLicense 39 | 40 | let usedDefaultNames = userDefaultsDouble.didCallStringForKeyWith 41 | XCTAssert(hasValue(usedDefaultNames)) 42 | 43 | if let usedDefaultNames = usedDefaultNames { 44 | XCTAssert(usedDefaultNames.contains(License.UserDefaultsKeys.licenseCode)) 45 | XCTAssertFalse(usedDefaultNames.contains(License.UserDefaultsKeys.name)) 46 | } 47 | } 48 | 49 | func testObtainingCurrentLicense_WithEmptyDefaults_ReturnsNil() { 50 | 51 | XCTAssertFalse(hasValue(licenseProvider.currentLicense)) 52 | } 53 | 54 | 55 | // MARK: Existing Defaults, Registered 56 | 57 | func testObtainingCurrentLicense_QueriesDefaultsForNameAndKey() { 58 | 59 | provideLicenseDefaults(name: "irrelevant name", licenseCode: "irrelevant key") 60 | 61 | _ = licenseProvider.currentLicense 62 | 63 | let usedDefaultNames = userDefaultsDouble.didCallStringForKeyWith 64 | XCTAssert(hasValue(usedDefaultNames)) 65 | 66 | if let usedDefaultNames = usedDefaultNames { 67 | XCTAssert(usedDefaultNames.contains(License.UserDefaultsKeys.name)) 68 | XCTAssert(usedDefaultNames.contains(License.UserDefaultsKeys.licenseCode)) 69 | } 70 | } 71 | 72 | func testObtainingCurrentLicense_WithLicenseCodeOnly_ReturnsLicenseWithInfoStrippingWhitespace() { 73 | 74 | provideLicenseDefaults(name: nil, licenseCode: " \t a license key \n") 75 | 76 | let licenseInfo = licenseProvider.currentLicense 77 | 78 | XCTAssert(hasValue(licenseInfo)) 79 | if let licenseInfo = licenseInfo { 80 | XCTAssertNil(licenseInfo.name, name) 81 | XCTAssertEqual(licenseInfo.licenseCode, "alicensekey") 82 | } 83 | } 84 | 85 | func testObtainingCurrentLicense_WithNameOnly_ReturnsNil() { 86 | 87 | provideLicenseDefaults(name: "a name", licenseCode: nil) 88 | 89 | XCTAssertNil(licenseProvider.currentLicense) 90 | } 91 | 92 | func testObtainingCurrentLicense_WithNameAndLicenseCode_ReturnsLicenseWithInfo() { 93 | 94 | let name = "a name" 95 | provideLicenseDefaults(name: name, licenseCode: " \t a license key \n \t ") 96 | 97 | let licenseInfo = licenseProvider.currentLicense 98 | 99 | XCTAssert(hasValue(licenseInfo)) 100 | if let licenseInfo = licenseInfo { 101 | XCTAssertEqual(licenseInfo.name, name) 102 | XCTAssertEqual(licenseInfo.licenseCode, "alicensekey") 103 | } 104 | } 105 | 106 | 107 | // MARK: - 108 | 109 | class TestUserDefaults: NullUserDefaults { 110 | 111 | var testValues = [String : String]() 112 | var didCallStringForKeyWith: [String]? 113 | override func string(forKey defaultName: String) -> String? { 114 | 115 | if !hasValue(didCallStringForKeyWith) { 116 | didCallStringForKeyWith = [String]() 117 | } 118 | 119 | didCallStringForKeyWith?.append(defaultName) 120 | 121 | return testValues[defaultName] 122 | } 123 | } 124 | } 125 | 126 | -------------------------------------------------------------------------------- /Tests/TrialLicenseTests/UserDefaultsLicenseWriterTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import XCTest 6 | @testable import TrialLicense 7 | 8 | class UserDefaultsLicenseWriterTests: XCTestCase { 9 | 10 | var userDefaultsDouble: TestUserDefaults! 11 | 12 | override func setUp() { 13 | super.setUp() 14 | 15 | userDefaultsDouble = TestUserDefaults() 16 | } 17 | 18 | override func tearDown() { 19 | userDefaultsDouble = nil 20 | super.tearDown() 21 | } 22 | 23 | // MARK: Storing 24 | 25 | func testStoring_TrimmingWhitespace_DelegatesToUserDefaults() { 26 | let writer = UserDefaultsLicenseWriter(userDefaults: userDefaultsDouble, removingWhitespace: true) 27 | let name = "a name" 28 | 29 | writer.store(licenseCode: " \t \n a license code \n ", forName: name) 30 | 31 | let changedDefaults = userDefaultsDouble.didSetValuesForKeys 32 | XCTAssert(hasValue(changedDefaults)) 33 | if let changedDefaults = changedDefaults { 34 | 35 | XCTAssert(changedDefaults[License.UserDefaultsKeys.name] == name) 36 | XCTAssert(changedDefaults[License.UserDefaultsKeys.licenseCode] == "alicensecode") 37 | } 38 | } 39 | 40 | func testStoring_PreservingWhitespace_DelegatesToUserDefaults() { 41 | let writer = UserDefaultsLicenseWriter(userDefaults: userDefaultsDouble, removingWhitespace: false) 42 | let licenseCode = " \t \n a license code \n " 43 | let name = "a name" 44 | 45 | writer.store(licenseCode: licenseCode, forName: name) 46 | 47 | let changedDefaults = userDefaultsDouble.didSetValuesForKeys 48 | XCTAssert(hasValue(changedDefaults)) 49 | if let changedDefaults = changedDefaults { 50 | 51 | XCTAssert(changedDefaults[License.UserDefaultsKeys.name] == name) 52 | XCTAssert(changedDefaults[License.UserDefaultsKeys.licenseCode] == licenseCode) 53 | } 54 | } 55 | 56 | 57 | // MARK: - 58 | 59 | class TestUserDefaults: NullUserDefaults { 60 | 61 | var didSetValuesForKeys: [String : String]? 62 | override func setValue(_ value: Any?, forKey key: String) { 63 | 64 | if !hasValue(didSetValuesForKeys) { 65 | didSetValuesForKeys = [String : String]() 66 | } 67 | 68 | didSetValuesForKeys![key] = value as? String 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/TrialTests/Helpers.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Foundation 6 | 7 | class NullUserDefaults: UserDefaults { 8 | 9 | override func register(defaults registrationDictionary: [String : Any]) { } 10 | override func value(forKey key: String) -> Any? { return nil } 11 | override func setValue(_ value: Any?, forKey key: String) { } 12 | } 13 | 14 | func hasValue(_ value: T?) -> Bool { 15 | switch (value) { 16 | case .some(_): return true 17 | case .none: return false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/TrialTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/TrialTests/TrialPeriodTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Cocoa 6 | import XCTest 7 | @testable import Trial 8 | 9 | class TrialPeriodTests: XCTestCase { 10 | 11 | var clockDouble: TestClock! 12 | 13 | override func setUp() { 14 | super.setUp() 15 | clockDouble = TestClock() 16 | } 17 | 18 | override func tearDown() { 19 | clockDouble = nil 20 | super.tearDown() 21 | } 22 | 23 | let irrelevantDate = Date(timeIntervalSinceNow: 987654321) 24 | 25 | func testCreation_WithClock_AddsDaysToCurrentTime() { 26 | 27 | let date = Date(timeIntervalSinceReferenceDate: 9999) 28 | clockDouble.testDate = date 29 | // 10 days 30 | let expectedDate = date.addingTimeInterval(10 * 24 * 60 * 60) 31 | 32 | let trialPeriod = TrialPeriod(numberOfDays: Days(10), clock: clockDouble) 33 | 34 | XCTAssertEqual(trialPeriod.startDate, date) 35 | XCTAssertEqual(trialPeriod.endDate, expectedDate) 36 | } 37 | 38 | 39 | // MARK: Days left 40 | 41 | func testDaysLeft_12Hours_ReturnsHalfDay() { 42 | 43 | let currentDate = Date() 44 | clockDouble.testDate = currentDate 45 | let endDate = currentDate.addingTimeInterval(12*60*60) 46 | let trialPeriod = TrialPeriod(startDate: irrelevantDate, endDate: endDate) 47 | 48 | let daysLeft = trialPeriod.daysLeft(clock: clockDouble) 49 | 50 | XCTAssertEqual(daysLeft, Days(0.5)) 51 | } 52 | 53 | 54 | // MARK: Ended? 55 | 56 | func testTrialEnded_WithEarlierClock_ReturnsFalse() { 57 | 58 | let endDate = Date(timeIntervalSinceNow: 4567) 59 | let trialPeriod = TrialPeriod(startDate: irrelevantDate, endDate: endDate) 60 | 61 | clockDouble.testDate = Date(timeIntervalSinceNow: 1) 62 | 63 | XCTAssertFalse(trialPeriod.ended(clock: clockDouble)) 64 | } 65 | 66 | func testTrialEnded_WithLaterClock_ReturnsTrue() { 67 | 68 | let endDate = Date(timeIntervalSinceNow: 4567) 69 | let trialPeriod = TrialPeriod(startDate: irrelevantDate, endDate: endDate) 70 | 71 | clockDouble.testDate = Date(timeIntervalSinceNow: 9999) 72 | 73 | XCTAssert(trialPeriod.ended(clock: clockDouble)) 74 | } 75 | 76 | 77 | // MARK: - 78 | 79 | class TestClock: KnowsTimeAndDate { 80 | 81 | var testDate: Date! 82 | func now() -> Date { 83 | 84 | return testDate 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tests/TrialTests/TrialProviderTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import XCTest 6 | @testable import Trial 7 | 8 | class TrialProviderTests: XCTestCase { 9 | 10 | var trialProvider: TrialProvider! 11 | var trialReaderDouble: TrialPeriodReaderDouble! 12 | var clockDouble: KnowsTimeAndDate! 13 | 14 | override func setUp() { 15 | super.setUp() 16 | clockDouble = TestClock() 17 | trialReaderDouble = TrialPeriodReaderDouble() 18 | trialProvider = TrialProvider(trialPeriodReader: trialReaderDouble) 19 | } 20 | 21 | override func tearDown() { 22 | clockDouble = nil 23 | trialReaderDouble = nil 24 | trialProvider = nil 25 | super.tearDown() 26 | } 27 | 28 | 29 | // MARK: - 30 | // MARK: Empty Defaults, no trial 31 | 32 | func testCurrentPeriod_ReaderReturnsNil_ReturnsNil() { 33 | trialReaderDouble.testTrialPeriod = nil 34 | 35 | XCTAssertNil(trialProvider.currentTrialPeriod) 36 | } 37 | 38 | func testCurrentPeriod_ReaderReturnsTrialPeriod_ForwardsResult() { 39 | let trialPeriod = TrialPeriod.init(startDate: Date(timeIntervalSince1970: 1234), endDate: Date(timeIntervalSince1970: 5678)) 40 | trialReaderDouble.testTrialPeriod = trialPeriod 41 | 42 | XCTAssertEqual(trialProvider.currentTrialPeriod, trialPeriod) 43 | } 44 | 45 | 46 | // MARK: Trial wrapping 47 | 48 | func testCurrentTrial_EmptyReader_ReturnsNil() { 49 | trialReaderDouble.testTrialPeriod = nil 50 | 51 | XCTAssertFalse(hasValue(trialProvider.currentTrial(clock: clockDouble))) 52 | } 53 | 54 | func testCurrentTrial_WithTrialPeriod_ReturnsTrialWithClockAndPeriod() { 55 | let trialPeriod = TrialPeriod(startDate: Date(timeIntervalSince1970: 456), endDate: Date(timeIntervalSince1970: 999)) 56 | trialReaderDouble.testTrialPeriod = trialPeriod 57 | 58 | let trial = trialProvider.currentTrial(clock: clockDouble) 59 | 60 | XCTAssert(hasValue(trial)) 61 | if let trial = trial { 62 | XCTAssertEqual(trial.trialPeriod, trialPeriod) 63 | XCTAssert(trial.clock === clockDouble) 64 | } 65 | } 66 | 67 | 68 | // MARK : - 69 | 70 | class TrialPeriodReaderDouble: ReadsTrialPeriod { 71 | var testTrialPeriod: TrialPeriod? = nil 72 | var currentTrialPeriod: TrialPeriod? { 73 | return testTrialPeriod 74 | } 75 | } 76 | 77 | class TestClock: KnowsTimeAndDate { 78 | var testDate: Date! 79 | func now() -> Date { 80 | return testDate 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Tests/TrialTests/TrialWriterTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import Cocoa 6 | import XCTest 7 | @testable import Trial 8 | 9 | class TrialWriterTests: XCTestCase { 10 | 11 | var writer: TrialWriter! 12 | var userDefaultsDouble: TestUserDefaults! 13 | 14 | override func setUp() { 15 | super.setUp() 16 | userDefaultsDouble = TestUserDefaults() 17 | writer = TrialWriter(userDefaults: userDefaultsDouble) 18 | } 19 | 20 | override func tearDown() { 21 | userDefaultsDouble = nil 22 | writer = nil 23 | super.tearDown() 24 | } 25 | 26 | func testStoring_DelegatesToUserDefaults() { 27 | 28 | // Given 29 | let startDate = Date(timeIntervalSince1970: 4567) 30 | let endDate = Date(timeIntervalSince1970: 121314) 31 | let trialPeriod = TrialPeriod(startDate: startDate, endDate: endDate) 32 | 33 | // When 34 | writer.store(trialPeriod: trialPeriod) 35 | 36 | // Then 37 | let changedDefaults = userDefaultsDouble.didSetObjectsForKeys 38 | XCTAssert(hasValue(changedDefaults)) 39 | 40 | if let changedDefaults = changedDefaults { 41 | 42 | XCTAssert(changedDefaults[TrialPeriod.UserDefaultsKeys.startDate] == startDate) 43 | XCTAssert(changedDefaults[TrialPeriod.UserDefaultsKeys.endDate] == endDate) 44 | } 45 | } 46 | 47 | 48 | // MARK: - 49 | 50 | class TestUserDefaults: NullUserDefaults { 51 | 52 | var didSetObjectsForKeys: [String : Date]? 53 | override func set(_ value: Any?, forKey key: String) { 54 | 55 | if !hasValue(didSetObjectsForKeys) { 56 | didSetObjectsForKeys = [String : Date]() 57 | } 58 | 59 | didSetObjectsForKeys![key] = value as? Date 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/TrialTests/UserDefaultsTrialPeriodReaderTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2019 Christian Tietze 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | import XCTest 6 | @testable import Trial 7 | 8 | class UserDefaultsTrialPeriodReaderTests: XCTestCase { 9 | 10 | var reader: UserDefaultsTrialPeriodReader! 11 | var userDefaultsDouble: TestUserDefaults! 12 | 13 | override func setUp() { 14 | super.setUp() 15 | userDefaultsDouble = TestUserDefaults() 16 | reader = UserDefaultsTrialPeriodReader(userDefaults: userDefaultsDouble) 17 | } 18 | 19 | override func tearDown() { 20 | reader = nil 21 | userDefaultsDouble = nil 22 | super.tearDown() 23 | } 24 | 25 | func provideTrialDefaults(_ startDate: Date, endDate: Date) { 26 | userDefaultsDouble.testValues = [ 27 | TrialPeriod.UserDefaultsKeys.startDate : startDate, 28 | TrialPeriod.UserDefaultsKeys.endDate : endDate 29 | ] 30 | } 31 | 32 | 33 | // MARK: - 34 | // MARK: Empty Defaults, no trial 35 | 36 | func testCurrentPeriod_WithEmptyDefaults_QueriesDefaultsForStartData() { 37 | 38 | _ = reader.currentTrialPeriod 39 | 40 | let usedDefaultNames = userDefaultsDouble.didCallObjectForKeyWith 41 | XCTAssert(hasValue(usedDefaultNames)) 42 | 43 | if let usedDefaultNames = usedDefaultNames { 44 | 45 | XCTAssert(usedDefaultNames.contains(TrialPeriod.UserDefaultsKeys.startDate)) 46 | } 47 | } 48 | 49 | func testCurrentPeriod_WithEmptyDefaults_ReturnsNil() { 50 | 51 | let trialInfo = reader.currentTrialPeriod 52 | 53 | XCTAssertFalse(hasValue(trialInfo)) 54 | } 55 | 56 | 57 | // MARK: Existing Defaults, returns trial period 58 | 59 | func testCurrentPeriod_WithDefaultsValues_QueriesDefaultsForStartAndEndDate() { 60 | 61 | provideTrialDefaults(Date(), endDate: Date()) 62 | 63 | _ = reader.currentTrialPeriod 64 | 65 | let usedDefaultNames = userDefaultsDouble.didCallObjectForKeyWith 66 | XCTAssert(hasValue(usedDefaultNames)) 67 | 68 | if let usedDefaultNames = usedDefaultNames { 69 | 70 | XCTAssert(usedDefaultNames.contains(TrialPeriod.UserDefaultsKeys.startDate)) 71 | } 72 | } 73 | 74 | func testCurrentPeriod_WithDefaultsValues_ReturnsTrialPeriodWithInfo() { 75 | 76 | let startDate = Date(timeIntervalSince1970: 0) 77 | let endDate = Date(timeIntervalSince1970: 12345) 78 | provideTrialDefaults(startDate, endDate: endDate) 79 | 80 | let trialPeriod = reader.currentTrialPeriod 81 | 82 | XCTAssert(hasValue(trialPeriod)) 83 | if let trialPeriod = trialPeriod { 84 | XCTAssertEqual(trialPeriod.startDate, startDate) 85 | XCTAssertEqual(trialPeriod.endDate, endDate) 86 | } 87 | } 88 | 89 | 90 | // MARK : - 91 | 92 | class TestUserDefaults: NullUserDefaults { 93 | 94 | var testValues = [AnyHashable : Any]() 95 | var didCallObjectForKeyWith: [String]? 96 | override func object(forKey defaultName: String) -> Any? { 97 | 98 | if !hasValue(didCallObjectForKeyWith) { 99 | didCallObjectForKeyWith = [String]() 100 | } 101 | 102 | didCallObjectForKeyWith?.append(defaultName) 103 | 104 | return testValues[defaultName] 105 | } 106 | } 107 | } 108 | --------------------------------------------------------------------------------