As you've probably guessed from the title, I am no longer actively maintaining Pronto Checker, a convenient app for checking your PRESTO card balance. I've written on my blog what this means for users (in short not much, the app still works), but I’ve also taken this as an opportunity to discuss a few details about the development of the app for those interested. I’ve found behind-the-scene posts by other indie developers to be insightful and I hope this post provides interesting content as well. So beware! The rest of this post will be a lot more technical.
The PRESTO card supports the largest transit system in Canada, with over two million cards and a million taps every weekday. I’m a big supporter of public transit but my biggest annoyance was checking to see if I needed to reload money onto my card. I still remember talking to my coworkers whether creating such a single-purpose app was worth pursuing. Although it brings a lot of convenience to those who specifically face this problem, it certainly was a niche market (this would be an iPhone-only app, users can use the first-party website instead, auto-loading with credit card even removes the need for such an app). Nevertheless I had already hacked together a scraper so I thought how much more work can it be?
I knew I had to build some kind of scraper because unfortunately there is no public PRESTO api, not even a private one from what I could tell. There were two main ways of doing so: I could either regex parse the html page myself, or let the browser do it for me.
I chose the latter option because most programmers are lazy (plus a few other reasons, of course). This turned out to be a good choice because the process of checking a balance is split into two network requests: login and fetch card summary. To complicate things, I discovered that the webpages included some dynamic session token which meant that the app always had to query the homepage to get a fresh set of tokens just to login1.
Here's a few other tidbits:
- An optimization I added was to use WKUserContentController (on iOS 11 and later) to prevent the browser from retrieving unnecessary content, thus reducing bandwidth usage and achieve faster page loading times.
- Since a user might have multiple accounts the app must be careful to use an ephemeral web session to clear cookies between logins.
- I was curious what I could add to make the app easier to use. I experimented with adding OCR so users don't need to enter the 17 digits by hand. After adding some simple optimizations and being smart about the formatting of the card I got it working fairly reliably. The irony was that in early 2016 PRESTO redesigned their card slightly so that the card number was grouped together into four blocks (previously it was one contiguous string) which made the OCR less reliable.
All of this is done on the users device for several reasons. I did not want to store any of the information on a server because that would be a lot of onus on me to keep everything secure. It also meant a central point of failure in that all PRESTO would have to do to prevent this from working (if they wanted) was to block my ip2. The downside is that I would have to update the app should the PRESTO website change its structure, but I thought it was worth this trade off.
An important aspect of building reliable software is to be able to monitor it. The app anonymously reports when a user is unable to login, as well as other analytics to get insight. I chose not to use a third-party analytics library/service because it was overkill for what I needed. I didn’t want to bring along a blob of binary3 which I had no idea what else it might be collecting.
The way I sent my analytics was very straightforward. The app generates an anonymous unique id on first launch, then sends GET requests that includes the relevant information as part of the URL. I simply log those requests to a file and take a look every so often. Although there’s no fancy GUI, there just wasn’t much need.
Obviously this solution is too simple for the majority of use cases and it takes a lot of work to build additional features and scale out. However one thing I would point out is that there’s probably only a few key statistics that really matter in the initial growth stage. It’s important to identify and focus on surfacing that information to stakeholders in the easiest way possible.
One other technology that I would say is crucial to have once the basic features are completed is the ability to present service status information to user (eg. system downtime or app version out of date). This could be a simple popup or just presenting a webpage. Having such a system in place allows one to prepare for the unexpected.
Recent popup from iTunes Connect app
I've been working on this project for a bit more than two and a half years and the cadence of work was very different from other projects I've worked on in the past. I mostly worked in short bursts where I focused on shipping a fix or adding a new feature, then left the project for several months. This provided for a few interesting observations.
The first time I jumped back into the project it took me a while to figure out how everything worked, almost as if I was reading someone else's code. A bit of this was due to the gap in time, but a lot was also my relative lack of experience of software engineering back then.
This app is a prime example of basic separation of concerns, for example breaking the app into 1) core balance checker, 2) model/persistence of the balance, 3) UI to display balance. Thus I took the chance in my first rewrite to clearly mark the boundaries of each section of the app. I defined new
structs to make it more explicit about how different sections of the app could interact with each other, and tried to anticipate properties I would need in the future. A related problem I had to consider was the compatibility of the persistence layer between versions.
There was a benefit, however, to working on the app every couple of months. It was easier to identify the improvement of my ability to write quality code because in each subsequent reread I could always find places where I now had a better solution or way of organizing the code. It can be exhilarating to be able to rewrite some code in a way that avoids a hidden problem that my past self did not anticipate. However there's also been a few times where I was pleasantly surprised that I had already handled a few special cases when rereading code. It's also an interesting experience to see the progression of the Swift language. I started working on the app initially in Swift 1.2 and went through several migrations (remember 2.0?), seeing the language mature and get better over time.
OK now onto the businessy side of things. Back then I listened to a few podcasts and followed indie developers who made their living by selling software. Creating apps is comparable to an investment (time is money right?). I thought it was a good opportunity to start with something small and build out a variety of different apps to provide a diversified income.
The feature I was bringing with this app is convenience and ease of use, which I hoped was enough of a reason for this to be a paid app. It would also be a lot easier to lower the price of an app than increase it4. And if I'm being honest, I would personally consider it a lot more impressive if I saw a steady stream of paid downloads with good reviews because this directly proves that people find it useful.
Initially after the launch I gave Google Adwords a try because I was curious what kinds of results I could get. I tried a variety of keywords, tweaking the landing page or linking directly to the App Store, etc. However I believe (it’s been a while) that I was barely breaking even because the CTR was pretty low. It was also more work than value when it came to testing different variations. I found it really draining to keep track of each hypothesis between testing (since it took time to see the results), or trying different layouts.
I also tried reaching out to press but didn’t get much response back. Thinking back there were several factors. Since this was a very niche app it’s hard to find the right customers when going to reporters, and reporters know this so it’s not worth their time. Furthermore, even if a person wants an app that can do this, there’s a high barrier due to the paid pricing. Realizing this, I tried making the app free in conjunction to cold-emailing. Eventually I got a link on the iPhoneinCanada blog.
When iOS 10.3 introduced an in-app review dialogue I knew immediately I wanted to add it in. At the time I didn’t have too many many reviews, which can be especially helpful to paid apps. This was in part because I didn’t add any invasive popovers because I wanted to create a pleasant experience for users. (Yet even if I did, I doubt the click-through rates would be high. The old process of leaving a review on the App Store was just too much to ask of users.)
There was a lot of small subtleties that I added to try to reduce user disruption and gain the best result. First the user must have successfully checked their balance a few times among several days. Satisfying this, the app then waits a few seconds after the next successful refresh so that the user can see their balance before they get prompted. This reduces the disturbance and hopefully results in the best possible result. Users will only see this popup at most once per version. Furthermore, the App Store now allows developers to keep the previous reviews after releasing new updates, reducing the need to continue showing these dialogues.
This brings us to the final topic of sun setting an app. I have a few vague memories of other apps doing this, but I've never really paid attention to the details of how other teams approached it. In my case however, the app will continue running as I am just giving everyone a heads up that no future app updates are planned.
I did debate if I should pull the app from the store. I didn't want to continue selling it since I am no longer actively developing it. However at the same time I still see value in making it available to people. Thus I decided to make the app free to download instead.
Thanks for reading! Would you have done anything differently? Let me know if you have any feedback in the comments below.
The dynamic token requirement was added around july of 2016. Previously, without the requirement, I was able to include a cached version of the login page with the app, skipping the first request ↩
If I was responsible for PRESTO website, seeing a single ip performing that many unique logins that would definitely be a red flag ↩