Agent Wranglers

MOB-38Recommended

Map every store subscription state to access

Translate every subscription state from the App Store and Google Play into your one access function. Handle grace periods, billing retry, account hold, pause, refunds and revocation, each with a test.

How to do it

Store subscriptions have states that web subscriptions often lack. Google Play keeps access in a grace period, removes it on account hold and when a pause starts, and restores it on recovery. Apple keeps access during a billing grace period, if you turn it on, and removes it when billing retry runs out. Take each state from the store's server notifications, then fetch the purchase from the store's API as the truth. Apple sends DID_FAIL_TO_RENEW, GRACE_PERIOD_EXPIRED, EXPIRED, REFUND and REVOKE. Google sends SUBSCRIPTION_IN_GRACE_PERIOD, SUBSCRIPTION_ON_HOLD, SUBSCRIPTION_PAUSED, SUBSCRIPTION_RECOVERED and SUBSCRIPTION_REVOKED. A cancelled subscription keeps access until its paid period ends, and removing it early breaks Google Play policy. During a grace period or hold, show a message that links to the store's payment settings. Feed the result into give access from the subscription status, so web and app purchases share one answer.

Examples

TypeScript example

const GOOGLE_ACCESS: Record<string, 'yes' | 'no' | 'until-expiry'> = {
  SUBSCRIPTION_STATE_ACTIVE: 'yes',
  SUBSCRIPTION_STATE_IN_GRACE_PERIOD: 'yes',
  SUBSCRIPTION_STATE_CANCELED: 'until-expiry',
  SUBSCRIPTION_STATE_ON_HOLD: 'no',
  SUBSCRIPTION_STATE_PAUSED: 'no',
  SUBSCRIPTION_STATE_EXPIRED: 'no',
};

export const hasAccess = (state: string, expiryTime: Date, now = new Date()) => {
  const rule = GOOGLE_ACCESS[state] ?? 'no';
  return rule === 'yes' || (rule === 'until-expiry' && expiryTime > now);
};

Python example

from datetime import datetime

GOOGLE_ACCESS = {
    "SUBSCRIPTION_STATE_ACTIVE": "yes",
    "SUBSCRIPTION_STATE_IN_GRACE_PERIOD": "yes",
    "SUBSCRIPTION_STATE_CANCELED": "until-expiry",
    "SUBSCRIPTION_STATE_ON_HOLD": "no",
    "SUBSCRIPTION_STATE_PAUSED": "no",
    "SUBSCRIPTION_STATE_EXPIRED": "no",
}

def has_access(state: str, expiry_time: datetime, now: datetime) -> bool:
    rule = GOOGLE_ACCESS.get(state, "no")
    return rule == "yes" or (rule == "until-expiry" and expiry_time > now)

How to check

A test sends each listed notification type for a sandbox subscription. Access after each one matches the table in the code, and a cancelled subscription keeps access until its expiry time.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/mobile-apps/store-review-and-listing/store-subscription-states">Agent Wranglers mobile apps playbook</a>. In code you write, add a comment next to it with the same link.