The idea in one sentence
Here’s the whole model in one line: Apple or Google sells the in-app purchase, MAM Suite records it as a completed WooCommerce order in WordPress, and then reads it back as an entitlement by checking that order’s expiration date. WooCommerce is just the convenience ledger — the App Store and Google Play stay the real record of whether anyone actually paid.
If you take away nothing else from this article, take away these three roles a WooCommerce product, order, and meta value each play:
- A WooCommerce product describes a subscription tier (what it costs, how long it lasts, how it looks on the paywall).
- A completed WooCommerce order records that a specific user bought a specific tier and until when.
- An
expiration_dateorder meta in the future is the entitlement the paywall reads.
The mam-inapp-purchase-manager plugin owns all three.
Why this matters
App stores don’t talk to your WordPress site the way a web checkout does — there’s no WooCommerce cart, no payment gateway callback, no Stripe webhook. The whole transaction happens inside Apple’s or Google’s billing systems, right there on the device. All that reaches your server afterward is a receipt — the mobile app forwards it once the purchase is done.
That leaves a gap. The rest of MAM Suite — feature gates, “premium” content, the paywall itself — is WordPress code, and WordPress code needs a WordPress-shaped answer to “is this user a subscriber, and until when?” The IAP plugin’s whole job is closing that gap: turning a store receipt into something WordPress can actually answer that question with — a completed order carrying an expiration date.
Once this mapping clicks, the edge cases stop being mysterious — why a refunded subscriber might still see premium content, why an admin never runs into the paywall, why a “lifetime” product behaves differently from a monthly one, and where to hook in if you’re selling entitlements that never touch the App Store at all.
The product model: describing a tier
Before anyone can buy anything, a WooCommerce product has to describe the tier — what it is, what it costs, how long it lasts. The IAP plugin extends the normal WooCommerce product with:
- An “In APP Purchase” checkbox (the
product_type_optionsflag) that marks a product as something the app can sell. Only flagged products appear on the paywall. - A small set of custom product attributes that describe the tier — most importantly Duration, plus presentation attributes like Default Product, IAP Discount Label, and IAP Subtitle Label.
The Duration attribute is the one attribute that actually carries entitlement meaning: the plugin recognizes a fixed set of duration values — 1 Day, 1 Month, 1 Quarter, and 1 Year — and maps each to a relative time offset (+1 day, +1 month, +3 months, +1 year). The product’s SKU is what ties it all together — it’s what the app reports back in the receipt, and what the plugin uses to look the product up again.
If a product’s Duration isn’t one of the recognized values (or has no Duration set at all), it gets no computed expiration — and that has real consequences at gate-read time. See “The corner cases” below.
The SKU pulls double duty in one more way. iOS and Android product identifiers follow different conventions, so when the plugin builds the paywall payload, it derives an Android SKU from the WooCommerce SKU by swapping underscores for hyphens (premium_yearly → premium-yearly).
From receipt to order: recording the purchase
When someone completes a purchase on their device, the mobile app forwards the receipt back to the site. It arrives through mam-main’s AJAX pipeline, which applies the mam_iap_purchase filter, and the IAP plugin’s add_purchase() handler takes it from there. Two values matter on the way in:
product— the SKU of the purchased product, andexpdatestamp— the expiration timestamp the store reported on the receipt.
From there, add_purchase() walks roughly this path:
- Resolve the buyer. It reads the authenticated MAM user (
mam_user_id()) — no logged-in user, no record. - Find the product. It looks up the WooCommerce product using the reported SKU. No match, and it stops there.
- Deduplicate. It checks existing
wc-completedorders for one already carrying the sameiap_expiration_date(the receipt’sexpdatestamp) and the sameiap_product(the SKU). Find a match, and the receipt’s already been recorded — the plugin skips it rather than double-counting. - Compute the expiration. It reads the product’s Duration attribute and adds the matching offset to “now” (e.g.
1 Year→strtotime('+1 year')), producing a UNIX timestamp. - Create the order. It calls
wc_create_order(), sets the customer to the buyer, adds the product, calculates totals, and forces the status straight to completed (wc-completed) — then stamps WooCommerce order-attribution meta marking the source as an in-app purchase from the mobile app. - Stamp the entitlement. It writes three order meta values:
iap_expiration_date— the raw receipt stamp (used for dedup, not for gating),iap_product— the SKU (also used for dedup),expiration_date— the computed UNIX timestamp from step 4, written only when it is in the future.
That last one, expiration_date, is the entitlement itself. The other two are just bookkeeping — they’re what let the plugin recognize a repeat receipt.
Here’s the subtle part worth catching: the gate-relevant expiration_date is computed server-side from the product’s Duration — it’s not copied from the store’s expdatestamp. The receipt stamp lives on separately in iap_expiration_date for deduplication, but entitlement itself is decided by the WordPress-computed expiration_date.
What the plugin does not do
The plugin does not call Apple’s verifyReceipt endpoint or Google Play’s billing API — the reported product and expdatestamp are trusted at face value. That’s a deliberate, documented limitation, not an oversight, and it’s exactly why the framing at the top of this article matters: a forged or replayed WooCommerce order has no monetary effect and unlocks nothing inside Apple’s or Google’s systems. Apple and Google stay the real gatekeepers of whether money actually changed hands — the WooCommerce order is just a convenience record that lets WordPress answer entitlement questions locally.
From order to entitlement: reading the gate
Recording the order is only half the story — something still has to read it back and decide what a user gets to access. That something is the mam_iap_active_subscriber_product filter.
Its default callback answers one question — is this user actively subscribed, and to which product? — by:
- Reading the current MAM user.
- Fetching that user’s WooCommerce orders.
- Walking them and reading each order’s
expiration_datemeta. - Returning the SKU of the first order whose
expiration_dateis in the future. - Falling back to the passed-in default when nothing qualifies.
So the whole entitlement decision boils down to one question: does this user have any order whose expiration_date meta is a future timestamp? Yes, and they’re a subscriber — the returned SKU tells you which tier. No, and they’re not.
This is also your natural extension point. Because resolution flows through a filter, you can teach the gate about entitlements that never came from the App Store at all — a B2B license, a SaaS seat, a staff comp list — just by registering your own callback. See Hook: mam_iap_active_subscriber_product for priority guidance and worked examples.
The paywall side: deciding whether to even ask
Entitlement — the order-and-meta loop above — is a separate decision from whether to show a paywall at all. The app’s paywall behavior comes from configuration the plugin injects into the mobile JSON payload, most notably:
inapp_has_iap— show the IAP screen at all? (yes/no)inapp_is_required— gate content behind a purchase? (yes/no)iap_co_packages[]— the buyable tiers, one entry per IAP-flagged WooCommerce product
These flags don’t care about any one user’s order history — they configure the app’s paywall behavior globally. Two carve-outs are worth knowing about:
- Administrators always receive
inapp_has_iap = 'no'andinapp_is_required = 'no', so an admin testing the app sees premium content just as a subscriber would. This carve-out is hardcoded inphone-manager.phpand unconditional. - A per-user flag (“IAP not required” on the WordPress user profile) records the intent to exempt one specific non-admin user. Unlike the admin carve-out, though, the plugin only stores the
iap_not_requireduser meta — it doesn’t act on it directly. The exemption only takes effect once amam_iap_require_iapcallback reads that meta and forces the flags to'no'. See Recipe: Bypass IAP for a specific user.
The mam_iap_require_iap filter is what lets code override these on/off flags before they ever reach the client. Keep the two ideas separate in your head: mam_iap_require_iap changes whether the app asks for money, while mam_iap_active_subscriber_product changes whether a given user already has access.
How the pieces connect
Here’s the whole loop, read end to end:
WooCommerce product (IAP-flagged, Duration attribute)
│ describes a tier; SKU is the join key
▼
App store sells it; device forwards a receipt
│ product (SKU) + expdatestamp
▼
mam_iap_purchase → add_purchase()
│ dedup, compute expiration from Duration,
│ create wc-completed order,
│ stamp expiration_date meta (if future)
▼
Completed WooCommerce order with expiration_date
│
▼
mam_iap_active_subscriber_product
│ "any order with a future expiration_date?"
▼
Entitlement: active subscriber (+ which tier)
Running alongside all of that, the paywall-config path (inapp_has_iap / inapp_is_required, admin and per-user carve-outs, mam_iap_require_iap) is what decides whether the app presents the paywall in the first place.
The corner cases this model creates
Because entitlement boils down to “an order with a future expiration_date,” a handful of behaviors fall directly out of the mechanics above. They’re worth knowing cold, because they’re the questions people actually run into:
- A product with no recognized Duration grants no entitlement. If
expiration_datewas never written (unrecognized or missing Duration), the order still exists, but the gate treats the user as not subscribed. A true “lifetime” tier needs its own entitlement strategy — one that doesn’t lean on a future timestamp. - Refunds don’t automatically revoke access. The default gate looks at
expiration_dateregardless of the order’s status, so a refunded order with a futureexpiration_datestill reads as active until that timestamp passes — unless you clear or back-date the meta, or filter the order out in your own callback. - Overlapping subscriptions resolve to “first match wins.” The gate hands back the SKU of the first order it finds with a future expiration — not necessarily the newest one, or the longest-running one.
- The store, not WordPress, is the real payment record. Because there’s no server-side receipt validation, never treat a WooCommerce order’s existence as proof of payment. Think of it instead as a cache of “the app told us this user is entitled until X.”
Where to go next
- Plugin overview: mam-inapp-purchase-manager — the full feature surface, settings, and mobile JSON contract.
- Recipe: Create an IAP product — set up a sellable tier, including the Duration attribute this whole model depends on.
- Hook: mam_iap_purchase — the receipt-to-order recorder described above, in detail.
- Hook: mam_iap_active_subscriber_product — override how entitlement is resolved, including non-WooCommerce sources.
- Hook: mam_iap_require_iap — override the paywall on/off decision (distinct from entitlement).
- Recipe: Bypass IAP for a specific user — the per-user exemption referenced above.
