Sample App 2: How to Add a Time-Based Trial

In the sample app we’ve developed in the previous chapter, registering and unregistering now is fully functional. You can put your app online, and nobody can access the license-protected features without having valid license information.

The thing is that test-driving apps is very important in practice when you target customers directly. It is less important if you target businesses, pitch their IT department, and have them install the app on devices on behalf of users. Business-to-customer indie shops will fare better with a demo to help prospects decide if they want to buy the app. You can also frame it in a less appealing way: when you already have a foot in the door and manage to make your app part of a users workflows, they are more inclined to become customers. For a price of US$2, your business model can be paid-up-front and appealing to impulse buyers. If your app costs US$50, people will be far less likely to buy without test-driving the app. I don’t have numbers to back this claim. This advice is solely based on anecdotes. Search your feelings, and find out where the tipping point from “might buy” to “definitely not without testing” is. Not being able to test iOS apps was a serious source of complaints for ages. Mac developers could still provide a demo on their website for direct download, even though the Mac App Store didn’t support trials or demos. Now, developers and Apple have adapted and turn to a freemium pricing model, with a free download and premium features behind an in-app purchase paywall.

To help users decide if they want to buy, time-based trials are one battle-tested approach: You offer new users a test drive for a limited time so they can decide if they want to keep using the product. Your trial period should be long enough to make your app part of your user’s lives or they will shrug the purchase dialog off too easily. For The Archive, a note-taking app, we’ve settled for a generous 60-day trial period to help newcomers implement the tool into their lives. For the WordCounter, a menu-bar app that tracks how many words you type, I figured 14 days would suffice to massage users into the beat of the app. Ask around and talk to your users to find out what works best for you.

Time-based trials are the de facto standard to limit functionality of what was used to be called “shareware” in the 1990s. Next to time-based trials, it’s also popular to offer feature-limited trials. There, you offer a demo version with limited capability from the start but potentially running forever. Users have to pay to unlock the full potential of the app. We won’t cover that here, because it’ll turn out that this is essentially an in-app purchase. When you learn how to implement in-app purchases to unlock features in the appendix, you can create feature-based trials, too.

To add a time-based trial to the sample app, we will end up with a couple of changes that all revolve around the primary state model, Licensing, to look more like this:

1 enum Licensing {
2     case registered(License)
3     case trial(TrialPeriod)
4     case trialExpired
5 }

After I experimented with different TrialPeriod implementations, I settled for a value type with both start and end date. To use a start date and a duration instead of an end date came natural, but this was very clumsy to use in practice. To find out if the trial period is expired right now requires a helper to get the current time, then add the duration to the start date and compare it to the current time. Since most timer-based operations, you have to compute the absolute end date, too, I suggest dropping the duration in favor of a fixed expiration date. TrialPeriod, in its most basic form, then looks like this:

1 struct TrialPeriod {
2     public let startDate: Date
3     public let endDate: Date
4 }

I asked around for feedback on this approach to modeling the licensing state and got a lot of great feedback and suggestions, some of which is reflected in the current names of the types. I actually favor a different base type:5

1 enum Licensing {
2     case licensed(License)
3     case trial(TrialPeriod) 
4     // `TrialPeriod.isExpired == true` replaces .unregistered
5 }

This variant communicates clearly that it’s all about moving the app from trial to licensed state. The third state above makes it harder to infer the transition rules in regard to .registered. Instead of a third state, an expired trial is just a variant of the .trial case. The associated TrialPeriod object knows if the trial is expired.

With Swift’s modern and very powerful pattern matching, you can then have very succinct case statements. This requires the addition of a pattern matching operator, ~=, and an enum that represents the isExpired state of TrialPeriod. First, have a look at the resulting API:

 1 func handleLicensingChange(licensing: Licensing) {
 2     switch licensing {
 3     case .trial(.valid):
 4         // ... 
 5     case .trial(.expired):
 6         // ...
 7     case .registered(let license):
 8         // etc.
 9     }
10 }

This reads as if TrialPeriod itself was an enum. But it isn’t. The ~=(pattern:value:) operator definition below makes an inner enum available in TrialPeriods place:

 1 extension TrialPeriod {
 2     // Enum that's used by the ~= operator
 3     enum Validity { case valid, expired }
 4 
 5     var validity: Validity {
 6         return self.isExpired ? .expired : .valid
 7     }
 8 
 9     // Current date falls outside the covered range
10     var isExpired: Bool {
11         return !(self.start...self.end ~= Date())
12     }
13 }
14 
15 func ~= (pattern: TrialPeriod.Validity, 
16          value: TrialPeriod) -> Bool {
17     return pattern == value.validity
18 }

If you never worked with custom pattern matching, this might look very weird at first. Even though the Licensing.trial case does not have an associated enum value but a struct value object, the pattern matching operator allows us to write .trial(.expired) as a shortcut to reach into a TrialPeriods validity property nevertheless.

This is a very cool feat, but also very advanced Swift territory, so I ultimately dropped it from the sample code for this book. But if you understand the code and prefer it, you’re welcome to change the implementation!

From here on, this chapter will teach you how to adjust the existing sample application to take time-based trials into account.

You can find a fully functional sample app incorporating a trial mode in the book’s code repository. It’s in the Trial-Expire-While-Running folder. You can use it as a template to quick-start your development process and refer to this chapter for details where needed.

Store and Read Trial Information

You have to store the TrialPeriod somewhere. We will start by storing the trial dates in the UserDefaults next to the license information – mostly so you can inspect the result easily from one place. Persisting a date for the sample app could produce these entries:

$ defaults read de.christiantietze.MyNewApp
{
    "trial_ending" = "2019-08-30 09:17:25 +0000";
    "trial_starting" = "2019-08-25 09:17:25 +0000";
}

If you want to make bypassing the time-based trial limitation harder than overwriting date information that’s in plain sight, you have a couple of options.

First, you may try to encode the expiration date and put it into UserDefaults. But if users simply delete the related defaults, the app will likely start a new trial period, just as if it was launched for the first time.

Then you might try to find a secret hiding spot instead, like a file stored somewhere. With Sandboxed applications, savvy users will know that the trial information is very likely contained in the app’s Group Container. That’s not a good hiding spot.

Lastly, you might come to the conclusion that the UserDefaults are just as good a place to store the trial period unencoded as I do in the samples if your options are this limited, anyway. But you do have another option: you can use a different UserDefaults suite or domain. macOS apps don’t have to use defaults with their bundle ID as the domain, like de.christiantietze.MyNewApp above. You can take advantage of this and store your trial info in a totally different defaults domain, like com.google.project-infinity or whatever.

On top of these offline solutions, you can require users to register on the web and store the trial duration on your server. This essentially results in your server dishing out a time-limited license. That’s exactly how devs model this feature: you get one free time-limited license from the start but can purchase unlimited licenses anytime. Both actions result in the user’s account being associated with specific license information. We won’t be looking at an online license verification and trial activation service in this edition of the book; that’s way beyond the scope.

Remember not to become paranoid, but do weigh the cost and benefits carefully. For an app for US$5, it won’t pay off to make it harder for cheap users to extend the trial or hack the license mechanism. Every hour you spend on copy protection against a handful of people is an hour you won’t spend improving the app for paying customers. The cost/benefit-analysis can easily change when you start to sell premium software for a couple hundred dollars. Just try not to punish the wrong people.

Since we use the UserDefaults for license information storage already, I wrote TrialProvider and TrialWriter in a way that mimics the license counterparts. If you remember that code, this one’s pretty close to a copy & paste job, changing some names and keys, really.

 1 extension TrialPeriod {
 2     struct DefaultsKey: RawRepresentable {
 3         let rawValue: String
 4         
 5         init(rawValue: String) {
 6             self.rawValue = rawValue
 7         }
 8         
 9         static let startDate = DefaultsKey(rawValue: "trial_starting")
10         static let endDate = DefaultsKey(rawValue: "trial_ending")
11     }
12 }
13 
14 // Convenience methods to store Date objects for TrialPeriod keys
15 extension Foundation.UserDefaults {
16     func date(forTrialKey trialKey: TrialPeriod.DefaultsKey) -> Date? {
17         return self.object(forKey: trialKey.rawValue) as? Date
18     }
19     
20     func set(_ date: Date, forTrialKey trialKey: TrialPeriod.DefaultsKey) {
21         self.set(date, forKey: trialKey.rawValue)
22     }
23 }
24 
25 class TrialProvider {
26     init() { }
27     
28     lazy var userDefaults: Foundation.UserDefaults = .standard
29     
30     var trialPeriod: TrialPeriod? {
31         guard let startDate = userDefaults.date(forTrialKey: .startDate),
32             let endDate = userDefaults.date(forTrialKey: .endDate)
33             else { return nil }
34             
35         return TrialPeriod(startDate: startDate, endDate: endDate)
36     }
37 }
38 
39 class TrialWriter {
40     init() { }
41     
42     lazy var userDefaults: Foundation.UserDefaults = .standard
43     
44     func store(trialPeriod: TrialPeriod) {
45         userDefaults.set(trialPeriod.startDate, forTrialKey: .startDate)
46         userDefaults.set(trialPeriod.endDate, forTrialKey: .endDate)
47     }
48 }

This is the backbone of the infrastructure that makes time-based trial period persistence real.

You know the TrialPeriod model type already, and now you also some services to read from and write to UserDefaults. With these important prerequisites taken care of, we can focus on adding actual functionality or logic to the app to react to trial expiration dates, and lock and unlock access to features.

Obtain and Work with Current Time

For the sample app, I want to work with a trial duration of 5 days. Not because I think that’s the best duration you could use, but because it keeps date calculations pretty simple. To create a TrialPeriod in the app, we need the current time as startDate, and then add the duration to calculate endDate.

In Foundation, the current time can be obtained by creating a Date object with the default initializer. But that’s going to bite us during tests, where we want to have constant inputs and outputs, not dynamically changing values.

The go-to solution when you express your concepts as objects is to introduce a representation of a thing that tells the time. In other words, a clock:

1 protocol Clock {
2     func now() -> Date
3 }

To get a real clock, we can rely on the default parameterless initializer of Date:

1 class SystemClock: Clock {
2     init() { }
3 
4     func now() -> Date {
5         return Date()
6     }
7 }

And we can provide a clock substitute that always tells the same date and time. We will use that both in manual tests to rewind time and see how the trial-based functionality reacts, and in unit tests.

 1 class StaticClock: Clock {
 2     let date: Date
 3 
 4     init(date: Date) {
 5         self.date = date
 6     }
 7 
 8     func now() -> Date {
 9         return date
10     }
11 }

With this, we can add a convenience initializer to TrialPeriod that takes a Clock to tell the current time, then adds the trial duration to compute the expiration date. I also want to introduce a type that expresses the duration of a day in terms of the app, and call it Days. With daylight savings time and leap seconds and what not affecting the length of a calendar day, the following is not a good representation for accurate calculations in calendar app – but it’s good enough to express how long a trial should be. And, as you will see in the code, you can attach all sorts of useful behavior to it that a mere Int would not be suited to.

 1 struct Days {
 2     let amount: Double
 3     
 4     init(_ anAmount: Double) {
 5         self.amount = anAmount
 6     }
 7     
 8     // Compatible with Date calculations
 9     var timeInterval: TimeInterval {
10         return self.amount * 60 * 60 * 24
11     }
12 }
13 
14 extension TrialPeriod {
15     public init(numberOfDays duration: Days, clock: KnowsTimeAndDate) {
16         let startDate = clock.now()
17         let endDate = startDate.addingTimeInterval(duration.timeInterval)
18         self.init(startDate: startDate, endDate: endDate)
19     }
20 }

It’s very easy now to verify that TrialPeriod(numberOfDays:clock:) actually adds the appropriate time interval to the current time in tests:

 1 class TrialPeriodTests: XCTestCase {
 2     class TestClock: Clock {
 3         var testDate: Date!
 4         func now() -> Date {
 5             return testDate
 6         }
 7     }
 8 
 9     func testCreation_WithClock_AddsDaysToCurrentTime() {
10     
11         let date = Date(timeIntervalSinceReferenceDate: 9999)
12         let clockDouble = TestClock()
13         clockDouble.testDate = date
14         let duration = Days(10)
15     
16         let trialPeriod = TrialPeriod(numberOfDays: duration, clock: clockDoubl\
17 e)
18     
19         let expectedDate = date.addingTimeInterval(duration.timeInterval)
20         XCTAssertEqual(trialPeriod.startDate, date)
21         XCTAssertEqual(trialPeriod.endDate,   expectedDate)
22     }
23 }

You see how easy it is to work with a Clock object to produce Date objects and provide test doubles. We can put additional behavior and logic in TrialPeriod to encapsulate trial expiration logic using clocks, too:

 1 extension TrialPeriod {
 2     // Important piece of logic to tell if the trial is
 3     // still valid.
 4     func isExpired(clock: Clock) -> Bool {
 5         let now = clock.now()
 6         return endDate < now
 7     }
 8 
 9     // Convenient factory of remaining days, useful to
10     // display the information in the app.
11     func daysLeft(clock: Clock) -> Days {
12         let now = clock.now()
13         let remainingTime = now.timeIntervalSince(endDate)
14         return Days(timeInterval: remainingTime)
15     }
16 }
17 
18 extension Days {
19     init(timeInterval: TimeInterval) {
20         self.init(fabs(timeInterval / 60 / 60 / 24))
21     }
22 }

As you can see, we do not interface with Foundation’s Date directly but acquire dates exclusively through Clock objects. A parameterless daysLeft computed property would read nicer, but the method signature daysLeft(clock:) works really well in practice, too, once you get used to it. The upside of making the code testable is immense. Both daysLeft(clock:) and isExpired(clock:) would be a pain to test if you called Date.init() directly.

The main lesson we encounter with Dependency Injection of this kind is this: try not to create objects in your code, because their creation is hard to test, and instead defer object creation to dedicated service objects, like the Factory that Clock really is. This is especially important when it comes to types you do not own, like Date and similar Foundation types. It pays off to not worry about the unknown implementation details and introduction of subtle bugs because some of your assumptions turn out to be wrong.