Zhenyi Tan And a Dinosaur

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?

Redesigning the Subscription Flow, Part 1

The subscribe button in Time Capsule is not very obvious, and I’m starting to think it’s a mistake.

Time Capsule has a subscription service where you pay $3/month for syncing and sharing your saved pages, and it lets you use the desktop browser extensions. Here’s how the button looked like:

Initially, I thought if you don’t need the subscription, I won’t blast it in your face, “Subscribe! Subscribe!” I wanted it to be non-obvious. But it felt like a half-assed job: The button doesn’t even say “subscribe.” Does it cost money? WTF does “Set up Sync and Share” even mean?

So I redesigned the button:

Is it too wordy? I don’t know. But at least it’s clearer and communicates the benefits of Capsule Account better. It’s less subtle than I hoped for, but developers have to eat.

How Much Should I Charge?

Pricing is a sensitive subject. Because people usually don’t like to give you their money. When it comes to software pricing, the general advice seems to be “charge more!” But… how much more?

The easiest way to set your price is to see what other people are charging. But unless you are them, you are not them. Do you have their economy of scale? Do you have their brand recognition? Seeing Apple selling wireless earbuds for $200 doesn’t mean you can sell yours for $200.

Another danger of comparing prices with similar products is it gets you into this price-per-feature mindset: “they have 10 features and charge $10. I have 5 features, so I should charge $5.”

So how much should I charge? I still don’t know. I’m just going to anyhow set the price to $3/month for now. If people buy, then the price is right.

Let’s see how it goes.