├── .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 | [](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 |
--------------------------------------------------------------------------------