PHP code example of kkxdev / laravel-apple-iap

1. Go to this page and download the library: Download kkxdev/laravel-apple-iap library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

kkxdev / laravel-apple-iap example snippets


use Kkxdev\AppleIap\Facades\AppleIap;

// routes/api.php
use Illuminate\Http\Request;
use Kkxdev\AppleIap\Facades\AppleIap;

Route::post('/webhooks/apple', function (Request $request) {
    AppleIap::processServerNotification($request->input('signedPayload'));
    return response()->noContent();
})->middleware('apple-iap.verify-notification');

use Kkxdev\AppleIap\Events\SubscriptionPurchased;
use Kkxdev\AppleIap\Events\SubscriptionRenewed;
use Kkxdev\AppleIap\Events\SubscriptionExpired;
use Kkxdev\AppleIap\Events\SubscriptionCancelled;
use Kkxdev\AppleIap\Events\RefundIssued;

protected $listen = [
    SubscriptionPurchased::class  => [HandleSubscriptionPurchased::class],
    SubscriptionRenewed::class    => [HandleSubscriptionRenewed::class],
    SubscriptionExpired::class    => [HandleSubscriptionExpired::class],
    SubscriptionCancelled::class  => [HandleSubscriptionCancelled::class],
    RefundIssued::class           => [HandleRefundIssued::class],
];

namespace App\Listeners;

use Kkxdev\AppleIap\Events\SubscriptionRenewed;

class HandleSubscriptionRenewed
{
    public function handle(SubscriptionRenewed $event): void
    {
        $tx         = $event->transaction;   // JwsTransaction
        $renewal    = $event->renewalInfo;   // JwsRenewalInfo|null
        $notification = $event->notification; // ServerNotification

        // Update your database — the package does not touch it
        \App\Models\Subscription::where(
            'apple_original_transaction_id',
            $tx->originalTransactionId
        )->update([
            'product_id'         => $tx->productId,
            'expires_at'         => $tx->expiresDateAsDateTime(),
            'auto_renews'        => $renewal?->willAutoRenew(),
            'environment'        => $tx->environment,
        ]);
    }
}

use Kkxdev\AppleIap\DTO\ServerApi\TransactionHistoryRequest;
use Kkxdev\AppleIap\Facades\AppleIap;

$history = AppleIap::getTransactionHistory($originalTransactionId);

foreach ($history->transactions as $tx) {
    echo $tx->productId;
    echo $tx->expiresDateAsDateTime()?->format('Y-m-d');
}

// With filters
$request = new TransactionHistoryRequest(
    productTypes: ['Auto-Renewable Subscription'],
    sort: 'DESCENDING',
);
$history = AppleIap::getTransactionHistory($originalTransactionId, $request);

// Paginate — hasMore indicates additional pages
while ($history->hasMore) {
    $next = new TransactionHistoryRequest(revision: $history->revision);
    $history = AppleIap::getTransactionHistory($originalTransactionId, $next);
}

$statuses = AppleIap::getAllSubscriptionStatuses($originalTransactionId);

if ($statuses->hasActiveSubscription()) {
    // At least one subscription is active
}

foreach ($statuses->data as $group) {
    foreach ($group->subscriptions as $sub) {
        echo match(true) {
            $sub->isActive()        => 'Active',
            $sub->isInGracePeriod() => 'Grace period',
            $sub->isInBillingRetry()=> 'Billing retry',
            $sub->isExpired()       => 'Expired',
            $sub->isRevoked()       => 'Revoked',
            default                 => 'Unknown',
        };
    }
}

$result = AppleIap::lookUpOrderId($orderId);

$refunds = AppleIap::getRefundHistory($originalTransactionId);

foreach ($refunds->transactions as $tx) {
    echo "Refunded: {$tx->productId} on {$tx->purchaseDateAsDateTime()->format('Y-m-d')}";
}

use Kkxdev\AppleIap\DTO\ServerApi\ExtendRenewalDateRequest;

$request = new ExtendRenewalDateRequest(
    extendByDays:      30,
    extendReasonCode:  1,           // 1 = customer satisfaction issue
    requestIdentifier: 'unique-id-for-idempotency',
    productId:         'com.example.app.premium',
);

$result = AppleIap::extendSubscriptionRenewalDate($originalTransactionId, $request);

if ($result->success) {
    echo "New expiry: " . (new DateTime())->setTimestamp($result->effectiveDate / 1000)->format('Y-m-d');
}

$token = AppleIap::sendTestNotification();

// Check delivery status
$status = AppleIap::getTestNotificationStatus($token);

use Kkxdev\AppleIap\Facades\AppleIap;
use Kkxdev\AppleIap\Exceptions\JwsVerificationException;

try {
    $transaction = AppleIap::decodeTransaction($jwsTransactionFromApp);

    echo $transaction->transactionId;
    echo $transaction->originalTransactionId;
    echo $transaction->productId;
    echo $transaction->type; // "Auto-Renewable Subscription", "Consumable", etc.
    echo $transaction->environment; // "Production" or "Sandbox"

    // Always verify the bundle ID matches your app to prevent cross-app replay attacks.
    if (!$transaction->matchesBundleId()) {
        abort(400, 'Bundle ID mismatch.');
    }

    if (!$transaction->isExpired()) {
        // Grant entitlement
    }
} catch (JwsVerificationException $e) {
    // Token did not pass Apple CA chain verification — reject it
}

$renewalInfo = AppleIap::decodeRenewalInfo($jwsRenewalInfoFromApp);

echo $renewalInfo->autoRenewStatus;      // 1 = will renew, 0 = won't
echo $renewalInfo->isInBillingRetryPeriod ? 'Retrying' : 'OK';

use Kkxdev\AppleIap\Facades\AppleIap;
use Kkxdev\AppleIap\Exceptions\AppleIapException;

$signature = AppleIap::generatePromotionalOfferSignature(
    productIdentifier:   'com.example.app.pro.monthly',
    offerIdentifier:     'monthly_winback_50_off',   // the code you set in App Store Connect
    applicationUsername: $user->apple_account_token ?? '',
);

// Return this to the iOS app:
return response()->json($signature->toArray());

use Illuminate\Http\Request;
use Kkxdev\AppleIap\Facades\AppleIap;

class PromotionalOfferController extends Controller
{
    public function generate(Request $request)
    {
        $request->validate([
            'product_id' => 'id), 403, 'Not eligible for this offer.');

        $signature = AppleIap::generatePromotionalOfferSignature(
            productIdentifier:   $request->product_id,
            offerIdentifier:     $request->offer_id,
            applicationUsername: $user->apple_account_token ?? '',
        );

        return response()->json($signature->toArray());
    }

    private function isEligible($user, string $productId): bool
    {
        // Only offer to users who have previously subscribed.
        return $user->subscriptions()
            ->where('apple_product_id', $productId)
            ->whereNotNull('expired_at')
            ->exists();
    }
}

use Kkxdev\AppleIap\Facades\AppleIap;
use Kkxdev\AppleIap\Exceptions\ReceiptValidationException;

try {
    $response = AppleIap::validateReceipt($base64EncodedReceiptData);

    if ($response->isValid()) {
        foreach ($response->latestReceiptInfo as $purchase) {
            echo $purchase->productId;
            echo $purchase->expiresDateAsDateTime()?->format('Y-m-d');

            if (!$purchase->isExpired() && !$purchase->isCancelled()) {
                // Active purchase
            }
        }

        // Find most recent purchase for a specific product
        $latest = $response->latestPurchaseFor('com.example.app.premium');
    }
} catch (ReceiptValidationException $e) {
    echo "Status {$e->getStatusCode()}: {$e->getMessage()}";
}

use Kkxdev\AppleIap\Contracts\AppStoreServerApiInterface;
use Kkxdev\AppleIap\Contracts\JwsVerifierInterface;
use Kkxdev\AppleIap\Contracts\NotificationVerifierInterface;
use Kkxdev\AppleIap\Contracts\ReceiptValidatorInterface;

class SubscriptionService
{
    public function __construct(
        private AppStoreServerApiInterface $serverApi,
        private JwsVerifierInterface $jwsVerifier,
    ) {}
}

use Kkxdev\AppleIap\Exceptions\AppleIapException;
use Kkxdev\AppleIap\Exceptions\CircuitBreakerOpenException;
use Kkxdev\AppleIap\Exceptions\NetworkException;

try {
    $statuses = AppleIap::getAllSubscriptionStatuses($originalTransactionId);
} catch (CircuitBreakerOpenException $e) {
    // Apple API is temporarily unavailable — return cached state
    return Cache::get("subscription:{$userId}");
} catch (NetworkException $e) {
    // Transient network failure
    Log::warning('Apple IAP network failure', ['error' => $e->getMessage()]);
} catch (AppleIapException $e) {
    // Any other package exception
    Log::error('Apple IAP error', ['error' => $e->getMessage()]);
}

use Illuminate\Support\Facades\Http;

Http::fake([
    '*/verifyReceipt' => Http::response([
        'status'      => 0,
        'environment' => 'Sandbox',
        'receipt'     => ['in_app' => []],
        'latest_receipt_info' => [
            [
                'product_id'              => 'com.example.premium',
                'transaction_id'          => 'tx-001',
                'original_transaction_id' => 'tx-001',
                'purchase_date_ms'        => (string)(time() * 1000),
                'original_purchase_date_ms' => (string)(time() * 1000),
                'expires_date_ms'         => (string)((time() + 2592000) * 1000),
                'quantity'                => '1',
                'is_trial_period'         => 'false',
                'is_in_intro_offer_period' => 'false',
            ],
        ],
    ], 200),
]);

use Illuminate\Support\Facades\Event;
use Kkxdev\AppleIap\Events\SubscriptionRenewed;

Event::fake();

// ... trigger notification processing ...

Event::assertDispatched(SubscriptionRenewed::class, function ($event) use ($originalTransactionId) {
    return $event->transaction->originalTransactionId === $originalTransactionId;
});

use Kkxdev\AppleIap\Contracts\NotificationVerifierInterface;
use Kkxdev\AppleIap\DTO\Notification\ServerNotification;

$mock = $this->mock(NotificationVerifierInterface::class);
$mock->shouldReceive('verify')
     ->once()
     ->andReturn($this->makeTestNotification());

use Kkxdev\AppleIap\DTO\Enums\ProductType;

ProductType::AUTO_RENEWABLE_SUBSCRIPTION  // 'Auto-Renewable Subscription'
ProductType::NON_CONSUMABLE               // 'Non-Consumable'
ProductType::CONSUMABLE                   // 'Consumable'
ProductType::NON_RENEWING_SUBSCRIPTION    // 'Non-Renewing Subscription'

ProductType::isSubscription($type); // true for auto-renewable and non-renewing
bash
php artisan vendor:publish --tag=apple-iap-config
bash
php artisan apple-iap:verify-receipt <base64-receipt-data>

# Against sandbox
php artisan apple-iap:verify-receipt <receipt> --env=sandbox

# Override shared secret
php artisan apple-iap:verify-receipt <receipt> --shared-secret=xxxx