I've been building Jottings as a web-first platform—you can use it on any device with a browser. But I kept hearing from iOS users asking if they could subscribe directly from the app, especially when they wanted to upgrade to Pro.
The answer used to be "not yet." Now it is.
This week, I shipped StoreKit 2 integration with Apple In-App Purchase (IAP) support. If you're using Jottings on iOS, you can now subscribe to Pro directly through the app. No redirects to the web. No confusing payment flows. Just tap, approve with Face ID, and you're done.
Why This Mattered
Before this, iOS users had two options:
- Subscribe on the web (jottings.me/pricing)
- Use the free tier
Both were fine, but they felt disconnected from the iOS app experience. Apple's own guidelines suggest supporting in-app purchases for subscriptions when you have an app, so it made sense to support this properly.
The challenge? Apple's subscription system is complex. You've got receipt validation, subscription status tracking, syncing across devices, handling cancellations, and managing refunds. All of this needs to happen securely between your app, Apple's servers, and your backend.
How I Built It
StoreKit 2 on the iOS Side
Apple's StoreKit 2 API is a huge improvement over the original StoreKit. It's async/await native, type-safe, and handles a lot of the complexity automatically.
The flow looks like this:
- Fetch available products from App Store Connect
- Show them to the user in the iOS app
- Initiate purchase with
Product.purchase() - Listen for transaction updates with
Transaction.updates - Finalize the transaction after validation
Here's a simplified version of what the iOS code does:
// Fetch products
let products = try await Product.products(for: ["com.jottings.pro.monthly"])
// Initiate purchase
let result = try await product.purchase()
// Listen for updates
for await result in Transaction.updates {
let transaction = try self.checkVerified(result)
await self.updateSubscriptionStatus(transaction)
await transaction.finish()
}
The beauty of StoreKit 2 is that Apple handles the hard cryptographic stuff. Your app can verify transactions are legitimate using Apple's built-in verification APIs.
Receipt Validation on the Backend
Here's where it gets tricky. The iOS app sends a receipt or transaction ID to your backend, and you need to:
- Verify it's actually from Apple (not forged)
- Check the subscription is still active
- Update the user's Pro status in your database
I'm using Apple's App Store Server API for this. Instead of trying to decode and validate receipt binaries myself, I call Apple's API to check subscription status.
The flow:
- iOS app sends
originalTransactionIdto backend - Backend calls
GET /inApps/v1/subscriptions/{originalTransactionId}on Apple's servers - Apple returns subscription status (active, expired, refunded, etc.)
- Update user's Pro status in Jottings database
- Return result to iOS app
This happens transparently—the user doesn't see these backend calls. They just see their Pro subscription activate in the app.
Syncing with Web Subscriptions
Here's a potential problem: what if someone subscribes via the web, then uses the iOS app? Or vice versa?
I handle this by treating all subscription sources as equal. Whether you subscribed through:
- The Jottings web app (Stripe)
- The iOS app (Apple IAP)
- The Android app (Google Play)
...you get the same Pro features and pricing. Your subscription status syncs across all platforms because it's stored in the backend.
When someone signs in on iOS, the app checks their Pro status with the backend. If they subscribed on the web, it activates immediately. If they subscribed via IAP, the backend tracks that separately and grants Pro access.
The Tricky Parts
Subscription Management: Users can manage their iOS subscriptions in Settings > [Your Name] > Subscriptions on their iPhone. Cancellations happen there, not in the app. I had to detect when an iOS subscription is cancelled and handle that gracefully in the Jottings dashboard.
Testing: Apple's StoreKit 2 has a sandbox environment for testing. I had to set up test accounts in App Store Connect and use Xcode's sandbox mode to properly test the purchase flow. In production, it works differently—Apple's verification is strict.
Receipt Timing: There's a slight delay between when a user completes a purchase and when Apple's servers register it. I added retry logic to handle cases where the backend checks subscription status immediately after purchase.
Refunds: If a user refunds their subscription through Apple, the status updates automatically the next time the app checks in. I'm not doing anything special here—Apple handles it, and my app respects whatever Apple says.
What This Enables
With StoreKit 2 integrated, I can now:
- Let iOS users subscribe without leaving the app
- Support subscriptions across all platforms (web, iOS, Android)
- Track which subscription source each user came from
- Handle cancellations and refunds automatically
- Provide a consistent Pro experience everywhere
It's one of those features that doesn't sound groundbreaking, but it makes the product feel more complete. iOS users aren't second-class anymore—they get a first-class subscription experience.
What's Next
I'm working on Android in-app purchases using Google Play Billing next. The flow is similar, though Google's API is slightly different. Once both are done, anyone can subscribe to Pro on their platform of choice.
For now, if you're an iOS user with Jottings installed, check the settings. You should see an option to upgrade to Pro. Give it a try and let me know how it feels.
If you hit any issues or have feedback about the implementation, reach out. I read every message.
Want to try it? Download the Jottings iOS app from the App Store and upgrade to Pro directly from your phone. No redirects, no friction, just tap and go.