Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Purchases Laravel Package

imdhemy/laravel-purchases

Laravel Purchases helps you validate and manage in-app purchases and subscriptions from Apple App Store and Google Play. It provides receipt verification, server notification handling, and subscription lifecycle tools with a Laravel-friendly API.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package and its published assets:

    composer require imdhemy/laravel-purchases
    php artisan vendor:publish --provider="Imdhemy\Purchases\PurchasesServiceProvider"
    php artisan migrate
    

    Publishing gives you config/purchases.php plus migrations for the receipts, subscriptions, subscription_history, products, and transactions tables the package uses to track state.

  2. Look first at config/purchases.php. It is the single source of truth for Apple credentials (shared secret or App Store Server API key), Google Play package name and service-account JSON path, webhook route prefix/middleware, and the eventListeners map that wires package events to your own listeners.

  3. First use case — validate a one-off purchase on the server (never trust the client):

    use Imdhemy\Purchases\Facades\Product;
    
    // Apple
    $response = Product::appStore()->receipt($base64Receipt)->validate();
    $response->getReceiptResponse()->getInApp(); // line items
    
    // Google Play
    $response = Product::googlePlay()->id($productId)->token($purchaseToken)->validate();
    
  4. Second use case — check subscription state before granting access:

    use Imdhemy\Purchases\Facades\Subscription;
    
    $subscription = Subscription::appStore()->receipt($receipt)->validate();
    if ($subscription->isActive()) { /* grant */ }
    

    The Subscription facade also works with Google tokens and exposes getExpiryTime(), getSubscriptionInfo(), and renewal/cancellation details used for reconciliation. Since v1.20.0 it additionally exposes immutable subscription and renewal dates — read them once after validation and store/compare them rather than recomputing from raw payloads.

Implementation Patterns

  • Two entry points, same object. Product::* is for validating a receipt/token right after a purchase; Subscription::* re-validates and understands recurring state. Call Product at checkout, Subscription on every subsequent access check or scheduled job.

  • Webhook-driven subscription lifecycle. Point Apple's App Store Server Notifications (v1/v2) and Google's Pub/Sub real-time developer notifications at the routes the package registers (default prefix laravel-purchases, e.g. /laravel-purchases/app-store/notifications and /laravel-purchases/google-play/notifications). The package parses the payload, dispatches a Laravel event, and (if configured) persists a SubscriptionHistory row.

  • Map package events to domain events in config, not by scattering listeners everywhere:

    'eventListeners' => [
        'AppStore' => [
            \Imdhemy\Purchases\Events\AppStore\InitialBuy::class => [GrantAccess::class],
            \Imdhemy\Purchases\Events\AppStore\Cancel::class     => [RevokeAccess::class],
        ],
        'GooglePlay' => [
            \Imdhemy\Purchases\Events\GooglePlay\SubscriptionRenewed::class => [ExtendAccess::class],
        ],
    ],
    

    Google and Apple each have provider-specific event classes (InitialBuy, Renewed, Cancel, DidFailToRenew, Refund, SubscriptionPurchased, etc.) — do not assume a shared name.

  • Use the v1.20.0 immutable dates as your single source of time. Subscription objects (and the persisted Subscription model) now expose immutable subscription/renewal date accessors. Prefer them over parsing expires_date_ms / expiryTimeMillis yourself: they normalise Apple's millisecond timestamps and Google's millisecond strings into consistent date objects, and because they're immutable you can safely hand them to jobs, DTOs, and audit logs without defensive copying. Snapshot them onto your own subscriptions record (e.g. current_period_start, current_period_end) when a renewal event lands, so historical rows keep the period they actually covered.

  • Reconcile on a schedule. Webhooks are best-effort. Add a queued job that iterates active Subscription models and calls Subscription::*()->validate() on a cadence (e.g. hourly/daily) to catch missed notifications, expiry, and grace-period transitions. This is the pattern the package's models are designed for — and the moment to refresh your stored immutable dates.

  • Persist the raw receipt once, reference it thereafter. Store the original receipt/token on your User or order, and use the package's Receipt model (or your own column) as the dedupe key — Apple and Google both re-deliver notifications.

  • Separate sandbox and production in config/env. Use APPLE_ENVIRONMENT=sandbox in local/staging and production (or App Store Server API environment) in prod. The same receipt will validate in one and fail in the other; this is a common staging source of confusion.

Gotchas and Tips

  • Apple shared secret vs. App Store Server API are different modes. The older password (shared secret) approach works for receipt validation but is deprecated for many features; newer Apple notifications (v2) are JWS-signed and require the App Store Server API key config. Pick one mode and configure it consistently — mixing them causes silent "valid but no data" results.

  • Google service-account JSON must be a path, not inline JSON in most configs. Keep the file outside the repo, set the path via env, and ensure the server's clock is synced (Google APIs reject skew). The service account also needs the Android Publisher API enabled and the app added in Play Console.

  • CSRF and auth middleware. Webhook routes are POSTed by Apple/Google, not by your users. The package respects the routing.middleware config; set it to ['api'] (no session/CSRF) or explicitly exclude the prefix in VerifyCsrfToken. If you have global auth middleware, webhooks will 401 and you'll see retries followed by Apple/Google disabling notifications.

  • Google notifications aren't signed the way you might expect. The Pub/Sub push body is a base64 message.data JSON envelope; authenticity comes from validating the JWT in the Authorization header against Google's public keys (or from IAM-restricting the push endpoint). Don't rely on the package alone for auth — add a middleware that verifies the bearer token.

  • Immutable dates are read-only by design (v1.20.0). Don't attempt to setAttribute/mutate them on the model or expect a setter — they're derived from the validated payload. If you need a different timezone or a mutable "grace period end", compute and store your own column. Also note immutable ≠ authoritative forever: a re-validated subscription can return a new set of dates (renewal, billing retry, grace period), so always read them off the freshly validated object, not a cached instance.

  • Test with real sandbox transactions. Apple sandbox receipts and Google license-test accounts are the only reliable way to exercise renewal, cancellation, and billing-retry paths. Unit-testing the facades is fine, but renewal semantics — including the new date accessors across a renewal — need live sandbox runs.

  • Extension points: the config eventListeners array, the Subscription/Receipt models (extendable), and the underlying imdhemy/appstore-iap + imdhemy/google-play-billing libraries if you need fields the facade doesn't expose. Reach for the underlying library before forking the package.

  • Watch the version. The package moved between major versions (v1 → v2/v3) with breaking changes to facades, event names, and Apple notification support. Pin a version, read the upgrade guide, and be wary of copy-pasted snippets from older blog posts using Receipt:: or validateReceipt methods that no longer exist. 1.20.0 is additive (date accessors) and should be safe to adopt, but it's still worth pinning and re-running your reconciliation job against a sandbox account to confirm the dates you persist line up with what you previously computed.

  • Debugging tip: log the raw webhook payload, the parsed event name, and the dates extracted from the validated subscription before your listener runs. Most "subscription didn't update" bugs are a mismatched event-key in eventListeners, a typo'd route prefix, or a notification arriving for an environment the app isn't configured for.

  • Idempotency is on you. Both stores retry; wrap listener logic in DB::transaction plus a unique constraint on (provider, original_transaction_id, event_id) or equivalent so a re-delivered Renewed doesn't double-extend access. With immutable dates this gets easier — compare the incoming renewal date to the stored one and no-op if unchanged.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib