Skip to main content

Offline Entitlements

Starting with v13.18.0, the plugin ships CdvPurchase.OfflineEntitlements, a helper class that answers "is this product owned?" when the device is offline or has just restarted without connectivity.

It persists a subset of each VerifiedPurchase to device storage on every verified event, reloads it at app launch, and exposes isOwned() against that persisted cache. store.owned() itself is unchanged — it still checks the in-memory verified and local receipts. OfflineEntitlements adds a parallel check, so you can combine both:

const offline = new CdvPurchase.OfflineEntitlements(store, {
gracePeriodMs: 30 * 24 * 60 * 60 * 1000, // 30 days
onExpiredOffline: 'readonly',
detectClockRollback: true,
});

await offline.ready();

function isUserPremium() {
return store.owned('premium') || offline.isOwned('premium');
}

Security Boundary: Phase 1 Is an Unsigned Cache​

Phase 1 persists entitlement data with no signature, no JWT, and no cryptography. The cache is a convenience for UI gating while offline, not proof of purchase. A user with access to the device's storage can tamper with it. Do not use OfflineEntitlements as the sole gate for high-value content or server-side entitlements — always confirm with receipt validation (store.owned() / your validator) once connectivity is back.

Retrieving Entitlement Details​

Use find() to retrieve the persisted entitlement for a product — expiry date, renewal intent, last renewal date, etc. — without digging into local storage by hand:

const entitlement = offline.find('premium');
if (entitlement?.expiryDate) {
const daysLeft = Math.ceil((entitlement.expiryDate - Date.now()) / 86400000);
showRenewalBanner(daysLeft);
}

Options​

OptionDescription
storagePluggable OfflineStorageAdapter (defaults to a localStorage wrapper). For long-offline deployments (weeks without connectivity), pass a file-based or secure-storage adapter — localStorage can be evicted by the WebView under storage pressure.
gracePeriodMsGrace window after a subscription's expiryDate during which it is still considered owned (default 30 days).
onExpiredOfflineBehavior when the grace period has elapsed and the device is still offline: 'readonly' keeps granting access (default), 'deny' revokes it.
detectClockRollbackIf true, deny access when the persisted lastSeenTimestamp is in the future relative to now — clock tampering (default false).

Events​

Subscribe with offline.onEvent(event => ...) to be notified of state transitions:

EventWhen It Fires
graceA subscription is past expiryDate but still within the grace period
readonlyGrace period elapsed while offline, access kept (onExpiredOffline: 'readonly')
clock_rollbackClock tampering detected (persisted timestamp in the future)
entitlement_missingNo persisted entitlement for a queried product
expiredAn entitlement expired and access was revoked

Events are deduplicated per productId.

Clearing Persisted Data​

offline.clear() removes all persisted entitlements — call it on user logout.

Platform Compatibility​

OfflineEntitlements is a TypeScript-only helper with no native code. The StoreKit 2, Braintree, and Apple Pay companion plugins are unaffected and require no version bump.