Numbers Up, Lessons Mixed 📊
New users nearly 5x'd in Camp Notes, plus a technical trick to keep everyone on the latest version.
This past month, I really focused at the numbers in Camp Notes and looked at how people weren’t making accounts, which means they wouldn’t be able to save their trips. I decided I needed to redo onboarding in a way that gave them more information upfront and set them up to make an account.
Numbers
Last month:
MRR: $48
Track New Users: 61
iHog New Users: 21
Camp Notes New Users: 17
This month:
MRR: $51
Track New Users: 36
iHog New Users: 36
Camp Notes New Users: 80
Camp Notes really popped off with new users and a trial was started 🚀 These numbers moving seem to just be flukes and I can't really ties them to anything that caused increases or decreases.
Experiment Report
In March, I made the paywall open immediately after the user adds their first task on Track. I'm also not sure if it was properly implemented as in Analytics, I'm not seeing any opens for the paywall on task completion.
Technical spotlight
I made a few updates to Camp Notes and to Track that forces or nudges the user to update. I should have added this to the apps from the get go, but the next best time is now. Here's how it works:
User launches app
Service checks if there is an update from the iTunes API
Both apps release with a X.Y, which is crucial. If X changes, then the user is presented a not dismissible full screen cover to tell the user to upgrade. If Y changes, then the user is going to see a sheet that asks the user to update, but can dismiss it.
This allows me to keep users on the most up to date versions which when running a NOSQL database like Camp Notes does it's important so the database is consistent as possible for all users. It also makes less crashes for this. Here's the code on how I did this:
struct AppVersion: Comparable {
let major: Int
let minor: Int
init?(_ string: String) {
let parts = string.split(separator: ".").compactMap { Int($0) }
guard parts.count >= 2 else { return nil }
self.major = parts[0]
self.minor = parts[1]
}
static func < (lhs: AppVersion, rhs: AppVersion) -> Bool {
if lhs.major != rhs.major { return lhs.major < rhs.major }
return lhs.minor < rhs.minor
}
}
enum UpdateType {
case major
case minor
case none
}
@Observable
@MainActor
class AppUpdateService {
var updateType: UpdateType = .none
var storeURL: URL?
private let bundleID: String
private let currentVersion: AppVersion?
init() {
self.bundleID = Bundle.main.bundleIdentifier ?? ""
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
self.currentVersion = AppVersion(versionString)
}
func checkForUpdate() async {
guard let current = currentVersion,
let url = URL(string: "https://itunes.apple.com/lookup?bundleId=\(bundleID)") else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
let json = try JSONDecoder().decode(iTunesResponse.self, from: data)
guard let result = json.results.first,
let store = AppVersion(result.version) else { return }
storeURL = URL(string: result.trackViewUrl)
if store.major > current.major {
updateType = .major
} else if store.minor > current.minor {
updateType = .minor
}
} catch {
print("Update check failed: \(error)")
}
}
}
This month's focus
In April, I am focusing on the following:
Camp Notes onboarding A/B test and new screenshots
Track will get some bug fixes
Continue pushing JABA forward
Still figuring out how to make time for marketing 😅



