Zhenyi Tan And a Dinosaur

Launch: Web Inspector for iOS

Web Inspector is now available in the App Store 🥳!

Web Inspector is a Safari extension on iOS and iPadOS that works more or less like its desktop counterpart. It allows web developers to edit web pages on the fly, debug JavaScript, and more.

Web Inspector is free, with no in-app purchases, no ads, and no tracking. You can get it in the App Store today. I hope you find it useful 🙏.

Web Inspector Privacy Policy

Web Inspector does not collect, store, or transmit any personal information.

Web Inspector Support

If you have any questions, email me or contact me on Twitter.

Safari Web Inspector on iOS

So, I made another app.

It’s called Web Inspector, a browser console for Safari on iOS and iPadOS.

It works pretty much as you would expect: tap the blue “i” button to open Web Inspector, tap it again to close.

It has 6 tabs: DOM and Elements let you inspect elements on a webpage, Console is your JavaScript console, Network lists network requests and responses, Timing displays the loading time of resources, and Resources shows you cookies, local storage, etc.

It’s not as powerful as the Web Inspector on the desktop because it’s basically a glorified JavaScript bookmarklet. Think Firebug Lite instead of Firebug.

Web Inspector will be free, with no in-app purchases, no ads, and no tracking, because I feel like this kind of developer tool should be free. I mean, Firebug was free, right?

If you’re on the iOS 15 beta and would like to TestFlight Web Inspector, send me your email address… or you can wait until iOS 15 is out. I hope to get Web Inspector in the App Store on day 1 if possible.

I will make another announcement when I launch it in the App Store.

Dismissed Share Extensions (Can) Still Run in the Background

There’s a weird bug in Time Capsule: when you’re in the middle of saving a web page, sometimes you can tap “Cancel” and still get the output HTML.

Actually… this behavior seems useful! If I can reliably reproduce this, I can then dramatically cut down the saving time and let the users get back to their browsing sooner. Unfortunately, no one on the internet is talking about this. Apple’s documentation is also unhelpful: “Tells the host app to cancel the app extension request.”

So to test it out, I created a simple app: the main app shows a number and a reset button, the share extension increment the number while running. I ran it, and here’s what happened:

Nice. It seems like the extension continues to run even after being dismissed.

I’m a bit tempted to add an “It’s now safe to close this popup” label in the extension UI, but as long as the behavior remains undocumented, it’s too risky.

So if you tried it and it works for you, great! But keep this between you and me, OK?

Marketing Is Hard Because Marketing Is Hard Work

I know nothing about marketing.

Actually, I do know a bit about marketing. Based on my very limited knowledge, marketing is hard. But engineers are supposedly good at solving hard problems, right? Programming and design are both hard, and I’m not terrible at them.

So for many years, I’ve been trying to “solve” marketing. Maybe there’s an algorithm or something that I can implement, and I just haven’t figured it out.


Until recently, I read a blog post by Yongfook about how he got his first 25 users. He didn’t talk about things like “build the hype” or “define your campaign goals,” he just listed out the things he tried:

  • I launched big new features on ProductHunt 3 times - until people grew sick of me
  • I wrote about bootstrapping on my blog and built 8 other products before this one
  • I tweeted about my startup constantly and sometimes those tweets went viral
  • I implemented a referral credit system that had zero effect
  • I started an affiliate programme that has had some small effect
  • I built a shopify version of my product which was a failure, and shut it down

…and that's just the stuff I can remember right now.

And I think I finally get it: marketing is literally metaphorically throwing everything at the wall to see what sticks. Keep throwing things and keep measuring the stickiness, there’s no easy way to do it.

Explain like I’m Five: GPL and AGPL

If I understand correctly, GPL basically means “if you use this library, your app must also become GPL, and you must open source your app.” No buts, no what-ifs.

AGPL is GPL, plus “you must also open source your app even if it’s a web app.”

GPL is like King Midas’ golden touch.

A few days ago, I discovered that I was using an AGPL-licensed JavaScript library in Time Capsule. I don’t want to open-source my app, so:

  1. I removed the library and rewrote the feature in the iOS app.
  2. I keep the library in the browser extensions because they’re just dumb clients that send data to my server.
  3. The server is a Rails app that doesn’t use the library.

In this case, I only need to open source the browser extensions, right?

Gotchas of Testing Subscriptions with Sandbox, Part 3

Here’s how to handle in-app purchase transactions in StoreKit:

  1. Implement SKPaymentTransactionObserver.
  2. Wait for paymentQueue(_:updatedTransactions:) to get called when transactions are updated.
  3. Loop through the transactions:
    1. If the transactionState is .purchased or .restored, unlock the in-app purchase.
    2. If the transactionState is .failed, show an error or something.
    3. Other states are less important, and you can ignore them if you want.
  4. Call finishTransaction(_:) on .purchased, .restored, and .failed transactions to remove them from the queue.

If you don’t call finishTransaction(_:), the transactions will remain in the queue and show up over and over again. But it seems like finished transactions don’t always get removed in the sandbox environment.

I noticed that the receipt validation endpoint on my server gets called every time I launch my app. But it’s only supposed to happen when I process the transactions in paymentQueue(_:updatedTransactions:). So I added a few print statements, and as I suspected, paymentQueue(_:updatedTransactions:) is getting called whenever I launch the app.


Maybe there’s a bug in my code? So I simplified paymentQueue(_:updatedTransactions:) to this:

func paymentQueue(
    _ queue: SKPaymentQueue,
    updatedTransactions transactions: [SKPaymentTransaction]
) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchased, .restored:
        case .failed:

Nope, the transactions still keep coming back.


Next, I implemented paymentQueue(_:removedTransactions:) to confirm that the transactions are getting removed from the queue:

func paymentQueue(
    _ queue: SKPaymentQueue,
    removedTransactions transactions: [SKPaymentTransaction]
) {
    print("Removed \(transactions.count)")
    print("Remaining \(queue.transactions.count)")

They were removed, but come back after a relaunch for some reason.


And no, I didn’t register the observer twice: I start observing in application(_:didFinishLaunchingWithOptions:) and remove the observer in applicationWillTerminate(_:).

I’m not really sure what’s going on? I just hope that this is one of the gotchas with subscriptions and sandbox and doesn’t happen in production.

Gotchas of Testing Subscriptions with Sandbox, Part 2

You can go to Settings → App Store → Sandbox Account → Manage to manage the subscriptions of your sandbox accounts. The point is that you don’t need to sign out of iCloud on your phone (which can be a scary thing) just to test subscriptions.

But other than seeing the active subscriptions, the sandbox subscription management screen is not very useful. Here’s what happens if you try to upgrade, downgrade, or resubscribe to an expired subscription:

Note that the sheets are showing your actual Apple ID. If you enter the password for the sandbox account, it says the password is wrong. If you enter your password, the sheet goes away, and nothing happens.

To test upgrade/downgrade/resubscribe, you’ll need to:

  1. Sign out of iCloud (or better yet, get another phone).
  2. Sign in to iCloud with the sandbox account.
  3. Sign in to App Store with the sandbox account.

Then the sandbox subscription management screen will work as intended.

Gotchas of Testing Subscriptions with Sandbox, Part 1

When you need to test in-app purchases in your apps, Apple lets you create sandbox tester accounts, so you don’t have to create real Apple IDs just to test the purchase flow.

But when you try to pay for in-app purchases with the sandbox account, the process is a bit weird:

  1. You sign in with the sandbox account.

  2. A sheet slides up, and you tap “Subscribe.”

  3. You enter the password for the sandbox account. (Didn’t you just signed in?)

  4. The subscription appears successful.

  5. The subscription sheet slides up again, and you tap “Subscribe” again. (???)

  6. You enter the password for the sandbox account again.

  7. The “Done” animation shows up again.

  8. An alert pops up, saying the subscription is finally successful.

Now, it’s not a bug in your code and will not happen in production. So I guess it’s… fine?

Redesigning the Subscription Flow, Part 3

This will be a short post. I added two inline up-sells to the app:

Previously, I hide the sync toolbar for non-subscribers. Now I show the toolbar, but it only says “Sign up to enable sync.” Tapping the toolbar brings them to the subscription screen.

Previously, I hide the share button for non-subscribers. Now the share button is enabled, but when non-subscribers tap it, it prompts them to sign up. Tapping “Learn More” brings them to the subscription screen.

There’s no “before” image for comparison, unless you want a blank image.

Redesigning the Subscription Flow, Part 2

As of now, the subscription screen of Time Capsule looks like this:

I’m pretty happy with the layout. It has all the elements required by Apple’s guidelines and doesn’t feel very custom or over-designed. The copy, however, is due for a rewrite.

“Sync and share” sounds vague. Most people probably think, “I don’t need sync and share,” let alone care about the implementation details like it needs a server. I was selling features, not benefits:

Features are things that your software does. Objectively speaking, Microsoft Powerpoint reads PPT files and lets you animate bullet points. Benefits are the perceived improvements in the user’s life that will result from purchasing your software. Subjectively speaking, customers of Microsoft Powerpoint buy it because it will let them close the deal, please their bosses, and get promoted.

With that in mind, I came up with this:

  • Access your saved pages across all your devices, including mobile, tablet, and web.
  • Export your saved pages as single HTML files with all the assets embedded.
  • Browser extensions to save pages directly to the Time Capsule server.

Yes, it’s a reiteration of the subscribe button, but consistency is a good thing, right?