PHP Apache-2.0

store-receipt-validator

PHP receipt validator for Apple iTunes, Google Play and Amazon App Store

A

aporat

Dernière activité 23 sept. 2026
aporat/store-receipt-validator

653

étoiles

148

forks

0

issues ouvertes

amazonapp-storeapplein-app-purchasesitunesphpreceiptreceipt-validationvalidation

Ce README est souvent en anglais.

store-receipt-validator

Latest Stable Version Downloads Codecov GitHub Actions License

A modern PHP library for validating in-app purchases from the Apple App Store (including legacy iTunes), Google Play and Amazon Appstore. Supports both production and sandbox environments with detailed response parsing.

Using Laravel? Use aporat/laravel-appstore-purchases instead. It is a Laravel package built on top of this library, and adds Laravel events for App Store and Google Play server notifications, config-based setup and container bindings.


✨ Features

  • ✅ Apple App Store Server API (v2) support
  • ✅ Apple StoreKit 2 signed transaction (jwsRepresentation) verification, offline
  • ✅ Apple iTunes Legacy API support (deprecated by Apple, still available here)
  • ✅ Google Play Developer API (Android Publisher v3) support: subscriptions, one-time products, voided purchases
  • ✅ Google Play Real-time Developer Notifications parsing (Pub/Sub envelope included)
  • ✅ Amazon Appstore receipt validation
  • ✅ App Store Server Notifications v1 & v2 parsing
  • ✅ Strong typing (PHP 8.4+), enums, and modern error handling
  • ✅ PSR-3 compatible logging support
  • ✅ Built-in test suite with 100% coverage

📦 Requirements

  • PHP >= 8.4

📥 Installation

composer require aporat/store-receipt-validator

🚀 Quick Start

📲 Apple App Store Server API

use ReceiptValidator\AppleAppStore\ReceiptUtility;
use ReceiptValidator\AppleAppStore\Validator as AppleValidator;
use ReceiptValidator\Environment;

// Credentials
$signingKey = file_get_contents($root . '/examples/SubscriptionKey_RA9DAYVX3X.p8');
$keyId = 'RA9DAYVX3X';
$issuerId = 'xxxxxx-xxxx-xxxx-xxxx-xxxxxxx';
$bundleId = 'com.myapp';

$receiptBase64Data = '...'; // your app receipt here

// 🔑 Tip: Apple's Server API does not accept the full app receipt.
// Use ReceiptUtility to extract the latest transaction ID.
$transactionId = ReceiptUtility::extractTransactionIdFromAppReceipt($receiptBase64Data);

$validator = new AppleValidator(
    signingKey: $signingKey,
    keyId: $keyId,
    issuerId: $issuerId,
    bundleId: $bundleId,
    environment: Environment::PRODUCTION
);

try {
    $response = $validator->getTransactionHistory($transactionId);
} catch (ValidationException $e) {
    if ($e->getCode() === APIError::INVALID_TRANSACTION_ID->value) {
        echo "Invalid Transaction ID: {$e->getMessage()}" . PHP_EOL;
    } else {
        echo "Validation failed: {$e->getMessage()}" . PHP_EOL;
    }

    exit(1);
} catch (Exception $e) {
    echo 'Error validating transaction: ' . $e->getMessage() . PHP_EOL;
    exit(1);
}

echo 'Validation successful.' . PHP_EOL;
echo 'Bundle ID: ' . $response->getBundleId() . PHP_EOL;
echo 'App Apple ID: ' . $response->getAppAppleId() . PHP_EOL;

foreach ($response->getTransactions() as $transaction) {
    echo 'Product ID: ' . $transaction->getProductId() . PHP_EOL;
    echo 'Transaction ID: ' . $transaction->getTransactionId() . PHP_EOL;

    if ($transaction->getPurchaseDate() !== null) {
        echo 'Purchase Date: ' . $transaction->getPurchaseDate()->toIso8601String() . PHP_EOL;
    }
}

ℹ️ Validator::validate() is deprecated as of v9. Use getTransactionHistory() for paginated history, or getTransactionInfo() for a single signed transaction.

📚 Other App Store Server API endpoints

The AppleAppStore\Validator now covers Apple's full API surface:

Area Methods
Transactions getTransactionHistory(), getTransactionInfo(), getAppTransactionInfo(), finishTransaction(), setAppAccountToken(), sendConsumptionInformation()
Order / refunds lookUpOrderId(), getRefundHistory()
Subscriptions getAllSubscriptionStatuses(), extendSubscriptionRenewalDate(), extendSubscriptionRenewalDatesForAllActiveSubscribers(), getStatusOfSubscriptionRenewalDateExtensions()
Notifications requestTestNotification(), getTestNotificationStatus(), getNotificationHistory()

sendConsumptionInformation() targets Apple's v2 endpoint and requires a delivery status:

use ReceiptValidator\AppleAppStore\ConsumptionRequest;
use ReceiptValidator\AppleAppStore\DeliveryStatus;
use ReceiptValidator\AppleAppStore\RefundPreference;

$request = new ConsumptionRequest(
    customerConsented: true,
    sampleContentProvided: false,
    deliveryStatus: DeliveryStatus::DELIVERED,
);
$request->refundPreference = RefundPreference::GRANT_PRORATED;
$request->setConsumptionPercent(40); // sent as 40000 milliunits

$validator->sendConsumptionInformation($transactionId, $request);

Pass a ConsumptionRequestV1 instead to call the deprecated v1 endpoint.

getAllSubscriptionStatuses() accepts an optional status filter, so Apple returns only subscriptions in any of the given states:

use ReceiptValidator\AppleAppStore\SubscriptionStatus;

$statuses = $validator->getAllSubscriptionStatuses($transactionId, [
    SubscriptionStatus::Active,
    SubscriptionStatus::InBillingGracePeriod,
]);

Each returns a typed response object (Transaction, AppTransaction, SubscriptionStatusResponse, RefundHistoryResponse, NotificationHistoryResponse, …). See the App Store Server API docs for endpoint semantics.

🔏 StoreKit 2 (jwsRepresentation)

StoreKit 2 apps no longer send an app receipt. Each VerificationResult carries a signed transaction (JWS) in jwsRepresentation, which your app can send to your server.

Verify it offline — checks the signature, the Apple certificate chain, and that the payload's bundle ID and environment match the validator:

use ReceiptValidator\AppleAppStore\Validator as AppleValidator;
use ReceiptValidator\Environment;
use ReceiptValidator\Exceptions\ValidationException;

$validator = new AppleValidator(
    signingKey: $signingKey,
    keyId: $keyId,
    issuerId: $issuerId,
    bundleId: 'com.myapp',
    environment: Environment::PRODUCTION
);

try {
    $transaction = $validator->verifySignedTransaction($jwsRepresentation);
} catch (ValidationException $e) {
    // Bad signature, untrusted chain, or bundle/environment mismatch
    exit(1);
}

echo $transaction->getProductId() . PHP_EOL;
echo $transaction->getTransactionId() . PHP_EOL;

verifySignedRenewalInfo() and verifySignedAppTransaction() do the same for a subscription's renewal info and for AppTransaction.shared.

A signed transaction reflects the purchase at the time it was signed. To see its current state (for example, a later refund), look it up with the App Store Server API:

$current = $validator->getTransactionInfo($transaction->getTransactionId());

🖊️ Signing offer requests for StoreKit

Some StoreKit features need a signature from your server before the app can use them. The signature creators take the same In-App Purchase key you use for the App Store Server API.

use ReceiptValidator\AppleAppStore\Signature\PromotionalOfferV2SignatureCreator;
use ReceiptValidator\AppleAppStore\Signature\IntroductoryOfferEligibilitySignatureCreator;
use ReceiptValidator\AppleAppStore\Signature\AdvancedCommerceInAppSignatureCreator;
use ReceiptValidator\AppleAppStore\Signature\PromotionalOfferSignatureCreator;

// StoreKit 2 promotional offer (JWS). transactionId is optional but recommended.
$creator = new PromotionalOfferV2SignatureCreator($signingKey, $keyId, $issuerId, 'com.myapp');
$jws     = $creator->createSignature('com.myapp.monthly', 'SPRING_PROMO', $transactionId);

// Introductory offer eligibility (JWS)
$creator = new IntroductoryOfferEligibilitySignatureCreator($signingKey, $keyId, $issuerId, 'com.myapp');
$jws     = $creator->createSignature('com.myapp.monthly', allowIntroductoryOffer: true, transactionId: $transactionId);

// Advanced Commerce API in-app request (JWS)
$creator = new AdvancedCommerceInAppSignatureCreator($signingKey, $keyId, $issuerId, 'com.myapp');
$jws     = $creator->createSignature($advancedCommerceRequest);

// StoreKit 1 promotional offer (Base64 ECDSA signature)
$creator   = new PromotionalOfferSignatureCreator($signingKey, $keyId, 'com.myapp');
$signature = $creator->createSignature('com.myapp.monthly', 'SPRING_PROMO', $appAccountToken, $nonce, $timestampMs);

Return the result to your app, which passes it to the matching StoreKit API. See Generating JWS to sign App Store requests and Generating a signature for promotional offers.

🍏 Apple iTunes (Legacy API - Deprecated)

use ReceiptValidator\Environment;
use ReceiptValidator\iTunes\Validator as iTunesValidator;

$validator = new ITunesValidator($sharedSecret, Environment::PRODUCTION);

try {
    $response = $validator->setReceiptData('BASE64_RECEIPT')->validate();
} catch (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
    echo $e->getTraceAsString() . PHP_EOL;
    exit;
}

echo 'Bundle ID: ' . $response->getBundleId() . PHP_EOL;
echo 'Original Purchase Date: ' . $response->getOriginalPurchaseDate()?->toIso8601String() . PHP_EOL;

foreach ($response->getTransactions() as $tx) {
    echo 'Product ID: ' . $tx->getProductId() . PHP_EOL;
    echo 'Transaction ID: ' . $tx->getTransactionId() . PHP_EOL;
    echo 'Original Transaction ID: ' . ($tx->getOriginalTransactionId() ?? 'N/A') . PHP_EOL;

    if ($tx->getPurchaseDate() !== null) {
        echo 'Purchase Date: ' . $tx->getPurchaseDate()?->toIso8601String() . PHP_EOL;
    }
    if ($tx->getExpiresDate() !== null) {
        echo 'Expires Date: ' . $tx->getExpiresDate()?->toIso8601String() . PHP_EOL;
    }
}

foreach ($response->getLatestReceiptInfo() as $tx) {
    echo 'Latest — Product ID: ' . $tx->getProductId() . PHP_EOL;
    echo 'Latest — Transaction ID: ' . $tx->getTransactionId() . PHP_EOL;

    if ($tx->getPurchaseDate() !== null) {
        echo 'Latest — Purchase Date: ' . $tx->getPurchaseDate()?->toIso8601String() . PHP_EOL;
    }
    if ($tx->getExpiresDate() !== null) {
        echo 'Latest — Expires Date: ' . $tx->getExpiresDate()?->toIso8601String() . PHP_EOL;
    }
}

🤖 Google Play

Authentication uses a Google Cloud service account that has been granted access to your app in the Play Console ("Users and permissions" → invite the service account email with View financial data / Manage orders). Download its JSON key and pass the contents to the validator. Tokens are minted with the OAuth 2.0 JWT bearer flow and cached in memory; no extra Google SDK is required.

use ReceiptValidator\Environment;
use ReceiptValidator\Exceptions\ValidationException;
use ReceiptValidator\GooglePlay\Validator as GooglePlayValidator;

$validator = new GooglePlayValidator(
    packageName: 'com.example.app',
    credentials: file_get_contents('/path/to/service-account.json'),
);

try {
    // The purchase token from BillingClient's Purchase.getPurchaseToken()
    $purchase = $validator->getSubscriptionPurchaseV2($purchaseToken);
} catch (ValidationException $e) {
    echo 'Validation failed: ' . $e->getMessage() . PHP_EOL;
    exit(1);
}

echo 'State: ' . $purchase->getSubscriptionState()->name . PHP_EOL;
echo 'Entitled: ' . ($purchase->isEntitled() ? 'yes' : 'no') . PHP_EOL;
echo 'Expires: ' . $purchase->getExpiryTime()?->toIso8601String() . PHP_EOL;
echo 'Test purchase: ' . ($purchase->isTestPurchase() ? 'yes' : 'no') . PHP_EOL; // Environment::SANDBOX
echo 'Obfuscated account ID: ' . $purchase->getObfuscatedExternalAccountId() . PHP_EOL;

foreach ($purchase->getLineItems() as $item) {
    echo 'Product ID: ' . $item->getProductId() . PHP_EOL;
    echo 'Base plan: ' . $item->getBasePlanId() . PHP_EOL;
    echo 'Order ID: ' . $item->getLatestSuccessfulOrderId() . PHP_EOL;
    echo 'Auto-renewing: ' . ($item->isAutoRenewEnabled() ? 'yes' : 'no') . PHP_EOL;
}

ℹ️ Google has no sandbox endpoint. Licence-tester purchases come back from the production API with a testPurchase marker, which the response exposes as isTestPurchase() and Environment::SANDBOX.

Non-2xx responses throw GooglePlay\APIException (a ValidationException) carrying the HTTP status, Google's reason string and the matching APIError, so you can retry or branch without parsing the message:

use ReceiptValidator\GooglePlay\APIError;
use ReceiptValidator\GooglePlay\APIException;

try {
    $purchase = $validator->getSubscriptionPurchaseV2($purchaseToken);
} catch (APIException $e) {
    if ($e->isRetryable()) {            // 429, 5xx, quota or backend errors
        // schedule a retry
    } elseif ($e->getError() === APIError::PURCHASE_TOKEN_NO_LONGER_VALID) {
        // the token was superseded; drop it
    }
}

Other Google Play endpoints

Area Methods
Subscriptions getSubscriptionPurchaseV2(), acknowledgeSubscription(), cancelSubscription(), deferSubscription(), revokeSubscription()
One-time products getProductPurchaseV2(), getProductPurchase(), acknowledgeProduct(), consumeProduct()
Orders getOrder(), getOrders(), refundOrder(), reviewRefund()
Refunds getVoidedPurchases()
use ReceiptValidator\GooglePlay\RevocationContext;
use ReceiptValidator\GooglePlay\SubscriptionCancellationType;
use ReceiptValidator\GooglePlay\VoidedPurchasesParams;
use ReceiptValidator\GooglePlay\VoidedPurchaseType;

// One-time products: the v2 lookup needs only the token and returns one line item per product
$product = $validator->getProductPurchaseV2($purchaseToken);
if ($product->isPurchased() && !$product->isAcknowledged()) {
    foreach ($product->getLineItems() as $item) {
        echo $item->getProductId() . ' x' . $item->getQuantity() . PHP_EOL;
        $validator->acknowledgeProduct($item->getProductId(), $purchaseToken);
    }
}

// Stop the next renewal without a refund; access continues until the period ends
$validator->cancelSubscription($purchaseToken, SubscriptionCancellationType::USER_REQUESTED_STOP_RENEWALS);

// Extend a subscription by a week (etag comes from the latest getSubscriptionPurchaseV2() call)
$deferred = $validator->deferSubscription($purchaseToken, $purchase->getEtag(), 7 * 24 * 3600);
echo 'New expiry: ' . $deferred->getExpiryTime()?->toIso8601String() . PHP_EOL;

// End access now and refund the unused part of the period
$validator->revokeSubscription($purchaseToken, RevocationContext::proratedRefund());

// Orders are the financial record: what was charged, tax, buyer country, service period, refund state
$order = $validator->getOrder($purchase->getLatestLineItem()?->getLatestSuccessfulOrderId());
echo 'Charged: ' . $order->getTotal() . ' (tax ' . $order->getTax() . ')' . PHP_EOL; // "10.89 USD (tax 0.9 USD)"
echo 'Period: ' . $order->getLineItems()[0]->getServicePeriodEndTime()?->toDateString() . PHP_EOL;
if ($order->isRefunded() && $order->isChargeback()) {
    // remove access
}

// Google's recommendation when a purchase fails your own validation: refund and revoke
$validator->refundOrder($order->getOrderId(), revoke: true);

$voided = $validator->getVoidedPurchases(new VoidedPurchasesParams(type: VoidedPurchaseType::INCLUDE_SUBSCRIPTIONS));
foreach ($voided->getVoidedPurchases() as $refund) {
    echo $refund->getOrderId() . ' voided at ' . $refund->getVoidedTime()?->toIso8601String() . PHP_EOL;
}

Bringing your own access tokens

If you already use google/auth (or want to share a token cache), implement GooglePlay\JWT\AccessTokenProvider or wrap a callable:

use Google\Auth\Credentials\ServiceAccountCredentials;
use ReceiptValidator\GooglePlay\JWT\CallbackAccessTokenProvider;

$credentials = new ServiceAccountCredentials(
    'https://www.googleapis.com/auth/androidpublisher',
    '/path/to/service-account.json'
);

$validator = new GooglePlayValidator('com.example.app');
$validator->setAccessTokenProvider(
    new CallbackAccessTokenProvider(fn () => $credentials->fetchAuthToken()['access_token'])
);

🛒 Amazon Appstore

use ReceiptValidator\Amazon\APIError;
use ReceiptValidator\Amazon\Validator as AmazonValidator;
use ReceiptValidator\Environment;
use ReceiptValidator\Exceptions\ValidationException;

// The shared secret from the Developer Console's Shared Key page.
$validator = new AmazonValidator('SHARED_SECRET', Environment::PRODUCTION);

try {
    // receiptId from PurchaseResponse.getReceipt().getReceiptId(),
    // userId from PurchaseResponse.getUserData().getUserId()
    $response = $validator->validate($receiptId, $userId);
} catch (ValidationException $e) {
    $error = APIError::fromException($e); // null for connection failures

    if ($error?->isCanceledReceipt()) {
        // HTTP 410: the receipt was valid once. Revoke what it granted.
    } elseif ($error?->isRetryable()) {
        // HTTP 429 or 500: back off and try again later.
    }

    echo 'Validation failed: ' . $e->getMessage() . PHP_EOL;
    exit(1);
}

echo 'Product: ' . $response->getProductId() . PHP_EOL;
echo 'Type: ' . $response->getProductType()?->name . PHP_EOL; // CONSUMABLE, ENTITLED or SUBSCRIPTION
echo 'Entitled: ' . ($response->isEntitled() ? 'yes' : 'no') . PHP_EOL;
echo 'Expires: ' . $response->getExpiresAt()?->toIso8601String() . PHP_EOL; // subscriptions only
echo 'Country: ' . $response->getCountryCode() . PHP_EOL;

$transaction = $response->getTransaction();

if ($transaction?->isSubscription()) {
    echo 'Auto-renewing: ' . ($transaction->isAutoRenewing() ? 'yes' : 'no') . PHP_EOL;
    echo 'Free trial: ' . ($transaction->isInFreeTrial() ? 'yes' : 'no') . PHP_EOL;
    echo 'Grace period: ' . ($transaction->isInGracePeriod() ? 'yes' : 'no') . PHP_EOL;
    echo 'Quick Subscribe: ' . ($transaction->isQuickSubscribe() ? 'yes' : 'no') . PHP_EOL;
    echo 'Cancel reason: ' . $transaction->getCancelReason()?->name . PHP_EOL;

    foreach ($transaction->getPromotions() as $promotion) {
        echo 'Promotion: ' . $promotion->getType()?->name . ' (' . $promotion->getStatus()?->name . ')' . PHP_EOL;
    }
}

A rejected receipt throws a ValidationException whose code is the HTTP status Amazon returned, and APIError maps the documented ones: 400 invalid receipt, 410 receipt no longer valid, 429 throttled, 496 invalid shared secret, 497 invalid user ID and 500 internal error.

ℹ️ The RVS sandbox (Environment::SANDBOX) accepts any non-empty shared secret and appends _term to termSku, which production does not. Test receipts come from Amazon's App Tester and carry testTransaction: true.

📋 Logging

All validators support PSR-3 compatible logging via setLogger(). By default, a NullLogger is used so no output is produced unless you inject a logger.

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$logger = new Logger('receipt-validator');
$logger->pushHandler(new StreamHandler('php://stdout'));

$validator = new AppleValidator($signingKey, $keyId, $issuerId, $bundleId);
$validator->setLogger($logger);

The method returns $this for fluent chaining:

$response = $validator
    ->setLogger($logger)
    ->getTransactionHistory($transactionId);

Log levels

Level Events
DEBUG Outgoing API request details (environment, URI, parameters)
INFO Successful responses; environment retries (e.g. production → sandbox)
WARNING API error responses, unexpected HTTP status codes
ERROR Network/connection failures

📬 Apple App Store Server Notifications

🔔 V2 Notifications (App Store Server API)

Verify the notification through your Validator so that, in addition to Apple's signature, the notification is checked to belong to your app and environment. In production, also pass your app's numeric Apple ID so it is checked too:

use ReceiptValidator\AppleAppStore\Validator as AppleValidator;
use ReceiptValidator\Environment;
use ReceiptValidator\Exceptions\ValidationException;

$validator = new AppleValidator(
    signingKey: $signingKey,
    keyId: $keyId,
    issuerId: $issuerId,
    bundleId: 'com.myapp',
    environment: Environment::PRODUCTION,
    appAppleId: 1234567890, // from App Store Connect; Apple omits it from sandbox payloads
);

public function subscriptions(Request $request): JsonResponse {
    try {
        $notification = $validator->verifyNotification($request->all());

        echo 'Type: ' . $notification->getNotificationType()->value . PHP_EOL;
        echo 'Subtype: ' . ($notification->getSubtype()?->value ?? 'N/A') . PHP_EOL;

        $tx = $notification->getTransaction();
        if ($tx !== null) {
            echo 'Transaction ID: ' . $tx->getTransactionId() . PHP_EOL;
        }

        $renewalInfo = $notification->getRenewalInfo();
        if ($renewalInfo !== null) {
            echo 'Auto-Renew Product ID: ' . $renewalInfo->getAutoRenewProductId() . PHP_EOL;
        }
    } catch (ValidationException $e) {
        echo 'Invalid notification: ' . $e->getMessage() . PHP_EOL;
    }
}

Constructing new ServerNotification($request->all()) directly still works and verifies Apple's signature only; compare getBundleId() and getEnvironment() yourself in that case.

🔔 V1 Notifications (iTunes - Deprecated)

use ReceiptValidator\iTunes\ServerNotification;
use ReceiptValidator\Exceptions\ValidationException;

public function subscriptions(Request $request): JsonResponse {
    $sharedSecret = 'your_shared_secret';

    try {
        $notification = new ServerNotification($request->all(), $sharedSecret);

        echo 'Type: ' . $notification->getNotificationType()->value . PHP_EOL;
        echo 'Bundle ID: ' . $notification->getBundleId() . PHP_EOL;

        $transactions = $notification->getLatestReceipt()->getTransactions();

        foreach ($transactions as $tx) {
            echo 'Transaction ID: ' . $tx->getTransactionId() . PHP_EOL;
        }
    } catch (ValidationException $e) {
        echo 'Invalid notification: ' . $e->getMessage() . PHP_EOL;
    }
}

🤖 Google Play Real-time Developer Notifications

Play publishes notifications to a Cloud Pub/Sub topic; a push subscription POSTs them to your endpoint wrapped in a Pub/Sub envelope. ServerNotification::fromPubSubMessage() unwraps the envelope and decodes the notification.

Unlike Apple's notifications, the payload is not signed and carries no purchase data: it only tells you which purchase token changed. Always re-read the purchase from the API before changing entitlement, and authenticate the push itself (Pub/Sub's OIDC bearer token) at the HTTP layer.

use ReceiptValidator\Exceptions\ValidationException;
use ReceiptValidator\GooglePlay\RefundPreference;
use ReceiptValidator\GooglePlay\ReviewRefundRequest;
use ReceiptValidator\GooglePlay\ServerNotification;
use ReceiptValidator\GooglePlay\SubscriptionNotificationType;

public function googlePlay(Request $request): JsonResponse {
    try {
        $notification = ServerNotification::fromPubSubMessage($request->all());
    } catch (ValidationException $e) {
        // Undecodable messages will never succeed: acknowledge them so Pub/Sub stops retrying.
        return response()->json(['status' => 'ignored']);
    }

    if ($notification->isTestNotification()) {
        return response()->json(['status' => 'test']);
    }

    if ($sub = $notification->getSubscriptionNotification()) {
        echo 'Type: ' . $sub->getNotificationType()->name . PHP_EOL;
        echo 'Product: ' . $sub->getSubscriptionId() . PHP_EOL;

        $purchase = $validator->getSubscriptionPurchaseV2($sub->getPurchaseToken());

        if ($sub->getNotificationType()->revokesEntitlement() || !$purchase->isEntitled()) {
            // remove access
        }
    }

    if ($voided = $notification->getVoidedPurchaseNotification()) {
        echo 'Refunded order: ' . $voided->getOrderId() . PHP_EOL;
    }

    if ($review = $notification->getPendingRefundReviewNotification()) {
        // A chargeback awaiting your decision. Tell Google whether to approve it.
        $validator->reviewRefund($review->getOrderId(), new ReviewRefundRequest(
            $review->getPendingRefundToken(),
            RefundPreference::DECLINE,
            sampleContentProvided: true,
        )->withConsumptionPercent(80));
    }

    return response()->json(['status' => 'handled']);
}

🧪 Testing

composer test        # Run tests with PHPUnit
composer lint        # Run code style checks with PHP_CodeSniffer
composer analyze     # Run static analysis with PHPStan

🙌 Contributing

Contributions are welcome!
To get started:

  1. Fork this repo
  2. Create a feature branch
  3. Submit a pull request

Found a bug or want a new feature? Open an issue

Notable changes in each release are listed in the CHANGELOG.


📄 License

Apache-2.0 License. See LICENSE.


Projets similaires

Reading and Validating In App Purchase Receipt Locally.

Swiftasn1catalystcocoapods
Ttikhop
718 étoiles96

go-iap verifies the purchase receipt via AppStore, GooglePlayStore, AmazonAppStore and Huawei HMS.

Goamazonappstroreappstorebilling
Aawa
1,1 k étoiles257

A Node.js module for in-App-Purchase for iOS, Android, Amazon and Windows.

JavaScriptamazonamazon-apiandroid
Vvoltrue2
1,1 k étoiles290