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 Appstore Purchases Laravel Package

aporat/laravel-appstore-purchases

Laravel package for verifying Apple App Store in-app purchase receipts and subscriptions server-side. Validate transactions, inspect purchase data, and integrate iOS payments into your app with a simple, framework-friendly API.

View on GitHub
Deep Wiki
Context7

Getting Started

This release turns the package from a receipt validator wrapper into a full store-subscription layer. Start here before touching any product code.

  1. Check the dependency floor first. 5.0.0 requires aporat/store-receipt-validator ^11.1, phpseclib/phpseclib ^3.0.36 and psr/cache ^2.0 || ^3.0. If another package in your tree pulls phpseclib 4, RTDN verification silently breaks (see Gotchas). Run composer why phpseclib/phpseclib before you upgrade.
  2. Publish config and migrations. Config first so you can eyeball the apple-app-store validator block, then the subscription migrations. Run the migrations before you wire webhooks.
  3. Rename key_path to private_key in the apple-app-store validator. This is the one hard migration. private_key accepts either the .p8 path or the PEM contents inline; a value containing a PEM header is treated as the key itself. The published default is env('APPLE_APPSTORE_PRIVATE_KEY', base_path('resources/keys/authkey.p8')). APPLE_APPSTORE_KEY_PATH is no longer read — delete it from your env files or you'll chase a phantom key.
  4. Configure subscribers. The App Store and Google Play notification listeners dispatch to whatever you list there. This is the seam where your domain logic lives, not in the controllers.
  5. Add ManagesStoreSubscriptions to your account model and let it own the subscriptions rows (one row per store, so an account with both Apple and Google gets two rows).

First use case: a purchase call from your mobile app. Your client posts a signed transaction (or a Play purchase token, or an opt-in StoreKit 1 receipt) to SubscriptionRecorder, which writes the account's row under its lock. Do this end to end with a sandbox purchase before you enable the webhooks — the recorder is the path you'll debug most often.

Implementation Patterns

Purchases flow through SubscriptionRecorder, everything else through webhooks. Treat the recorder as an optimistic fast path: signed transactions, Play tokens and StoreKit 1 receipts (opt-in), with a dedupe window so a retried client call is a no-op. Do not build entitlement logic on top of the recorder response alone — the design is explicit that a client credential only extends, and that webhooks plus the worker are authoritative.

Webhooks are the source of truth. Point the App Store Server Notifications URL and the Google RTDN Pub/Sub push at the package's controllers, then handle the dedicated events in your subscribers. Two behaviours matter operationally:

  • A verified Apple notification this package has no event for now dispatches Events\UnhandledNotification instead of 400ing. Handle it (at minimum log and alert) so RESCIND_CONSENT, METADATA_UPDATE, MIGRATION and PRICE_CHANGE don't vanish, and so future Apple types don't require a package bump.
  • Signature and certificate-chain failures still reject. Keep them rejected; don't loosen the controller to "make notifications arrive".

Use VerifyStoreSubscriptions for reconciliation, not for normal flow. The recorder plus webhooks cover the common case; the command exists to sweep up missed notifications, lapsed worker runs and out-of-band changes. Schedule it conservatively (hourly or nightly per store) rather than on every cron tick — it hits the stores' APIs.

Map products through the validator entries. subscription_products on validator config entries, exposed via subscriptionProducts(), keeps product identifiers in config rather than scattered in listeners. validatorConfig() and googlePlayValidatorForPackage() are the escape hatches for multi-package or dynamically constructed validators; prefer them to instantiating validators by hand.

Let the Google cache do its job. As of 5.0.0, Google Play access tokens are cached in a Laravel cache store, one entry per service account (keyed by a hash of email + scope), refreshed token_refresh_margin seconds (default 300) before expiry, with a cache lock to collapse concurrent misses. OIDC signing certificates for RTDN pushes are cached for Google's Cache-Control: max-age, refetched on an unknown key ID at most once a minute. In production:

APPSTORE_GOOGLE_CACHE=true
APPSTORE_GOOGLE_CACHE_STORE=redis
APPSTORE_GOOGLE_TOKEN_REFRESH_MARGIN=300

Use a store shared across all workers. array gives you back the old per-process behaviour with extra steps.

Test with the new injection points. AppleAppStoreServerNotificationController accepts an optional TokenVerifier for its first decode (Apple's chain when null) and GoogleOidcPushVerifier accepts an optional PSR-6 certificateCache and google/auth httpHandler. That means you can trust your own signing chain and stub HTTP without disabling verification.

Gotchas and Tips

The key_path removal fails loudly — good, but only at runtime. A config still carrying key_path errors with a message saying so. Grep your config directory and every environment's .env for APPLE_APPSTORE_KEY_PATH before deploying; the env var silently does nothing now.

PEM line breaks are restored, not preserved. Environment variables and secrets-manager fields collapse newlines to spaces or literal \n; the package puts them back before OpenSSL sees the key. It works, but it means a mangled key fails at signature time rather than at config load. If Apple calls start failing with crypto errors right after a key rotation, verify the key round-trips through your secret store first.

phpseclib 4 is the trap in this release. aporat/store-receipt-validator allows phpseclib 4, google/auth only works with 3 for RS256 and doesn't declare it, so a fresh resolve could produce a setup where every Google RTDN push returns 401 with "Please require phpseclib/phpseclib v3 to use this utility." 5.0.0 pins ^3.0.36 to prevent it — but if you have your own direct phpseclib constraint, check it. Symptom is a 401 on every push, which is easy to misread as a misconfigured Pub/Sub subscription.

Cache store requirements. Concurrent misses wait on a cache lock when the store supports one; a failing store falls back to a direct exchange. Both are fine, but they mean the cache is not a hard dependency — so you won't notice a misconfigured APPSTORE_GOOGLE_CACHE_STORE from errors alone. Watch Google API quota instead. Rotating or revoking a service account does not invalidate a cached token early; it lives until the refresh margin.

The row lock is per account. Writes go under the account's row lock, which is correct but serialises concurrent writes for a single account. If you have batch reconciliation for one large account, it contends with live webhooks. Keep listener bodies short and push anything slow onto a queue.

appAccountToken labelling failures are error-level, not fatal. The info log when it's set was removed in this release; a failure to set it still logs at error. If purchase-to-account attribution goes missing, that's the log line to look for — and it's the account's store_account_token that must exist and be a valid UUID for Apple.

Dedupe windows are per-call, not global. The recorder's dedupe window covers retried client submissions; it is not a substitute for idempotent listener handling. Make your subscriber handlers idempotent on the notification/transaction identifier anyway, because store webhooks are at-least-once.

Two stores, two rows. Don't model subscriptions as a single account attribute. An account can hold an Apple row and a Google row simultaneously, and the "client only extends" rule means your UI should read the reconciled state, not the last client report.

Timeouts changed the push endpoint's tail latency. The OIDC certificate fetch now has 5s connect / 10s request timeouts. On a cold cache that's the worst case added to a push; size your Pub/Sub ack deadline accordingly, or pre-warm after deploys.

The dist archive is leaner. tests/, .github/, composer.lock, PHPUnit and PHPStan configs are no longer shipped (.gitattributes limits it to src/, config/, composer.json, README, CHANGELOG, LICENSE). Nothing breaks for consumers, but don't go looking for the package's test signing keys to copy into your own suite — grab them from the repo instead.

Extension points worth knowing. Events\UnhandledNotification (catch-all for unmapped Apple types), the optional TokenVerifier on the notification controller, and the PSR-6 certificateCache / httpHandler on GoogleOidcPushVerifier. Those three cover most cases where you'd otherwise be tempted to fork the controllers.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource