Ask
104
@smalltalk_sol ·

storekit 2 transaction updates missed a renewal, user lost pro for 3 days storekit

Annual subscriber renewed on the 14th. My backend did not hear about it until she emailed on the 17th, opened the app, and it silently fixed itself in front of her.

I listen to Transaction.updates in a task started at launch and post each transaction to my server. As far as I can tell the renewal happened while the app was closed and nothing was listening.

Is the client listener genuinely not enough, or have I wired it up wrong?

8 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @finops_reyna · 2d ago · 3 replies

    It is genuinely not enough and it never was. Transaction.updates delivers while your process is alive. A renewal at 03:00 on a phone in a drawer has nowhere to be delivered to.

    That listener exists for transactions that complete outside your normal purchase flow - a Family Sharing grant, an interrupted purchase resuming, a purchase that finished while you were backgrounded. It is not a renewal feed and treating it as one is the single most common subscription bug I see.

    Two things are the answer:

    • App Store Server Notifications to an endpoint you own. That is your source of truth for renewals, billing retry, grace period, refunds and cancellations. It arrives whether or not the app has ever been opened again.
    • On every launch, read Transaction.currentEntitlements and reconcile. That is exactly what fixed itself while she was watching.

    Server for truth, client for speed.

    172
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @stipend_sam · 3d ago

      And verify the signed payload properly instead of decoding the body and trusting it. There is a library for this in every language now. The number of apps granting entitlements from an unverified blob is not small and it is a genuinely bad day when someone notices.

      51
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @stropping_sal · 2d ago

      Notifications can arrive out of order and can be redelivered. Key on the original transaction id and treat every notification as an upsert of current state rather than an event that mutates state, or you will eventually process an expiry after the renewal that superseded it.

      22
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @kombucha_kai · 3d ago

    Do not forget the states between active and expired. Billing retry can run for weeks, and grace period keeps the user entitled while the payment is being retried.

    If you flip access off the instant the expiry date passes, you cut off people whose card is being retried and who would have paid you. They then churn for real, because you told them the subscription was over. That is the version of this bug that costs actual money - three days of missing Pro is a support email, a wrongly revoked grace period is a cancellation.

    81
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @overlap_owen · 2d ago

    Practical shape that works: grant from the client's currentEntitlements as a fast path so the user sees the right thing immediately, and let the server notification correct the record behind it. Neither one has to be fast for the other to be right, and you stop caring which arrives first.

    46
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @otis_haverford · yesterday · 2 replies

    Transaction.updates does fire for background renewals, the system wakes the app for it. Sounds more like your task is being cancelled - if you start it inside a view that can disappear, it dies with the view.

    27
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @lunchboxrotation · yesterday

      The task cancellation half is real and good advice - start it in your app entry point and hold it for the process lifetime, never inside a view.

      There is no background wake for renewals though. If the app is not running, nothing is delivered until it next runs, which is precisely why the server notification endpoint exists. Half right is unhelpful here because it sends OP looking at their task lifecycle for a problem that is not there.

      19
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @compost_turner · 3d ago

    Log every notification body you receive into a table before you process it, raw. When a customer says "I paid", being able to look at the store's own record of what happened beats any amount of reasoning about your own state machine, and it makes replaying a bad deploy trivial.

    28
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report