jinseokoh/purchase-webhooks

Handling App Purchase Server-to-Server Webhooks

0.2.7 2021-05-25 00:12 UTC

This package is auto-updated.

Last update: 2024-04-25 07:18:18 UTC


README

Background

At the time of writing API servers for a new mobile application, I've found this aporat/store-receipt-validator package very useful to validate in-app purchase responses from Apple App Store and Google Play Store. But, there're occasions that anyone can ask refund their purchases directly to the stores without notifying us of the cancellation. We could end up letting them use the app for free.

I know there exists Google Play Store API that we can make use of to retrieve voided purchases information. But, that's not so 2021. The both platforms allow us to register webhook URL address to post back all the store events including INITIAL PURCHASE and CANCELLATION. As long as you have the webhooks in place, you can get almost realtime server-to-server notifications if someone's got refunded. That means you can deprive the user of any privileges within your app at your discretion.

This package handles the server to server notifications via Laravel Jobs. Note that subscription payment notifications are not fully supported as of yet since that's beyond my use-cases. If your application has subscription based payment options, this might not be what you are looking for.

Installation

You can install this package via composer

composer require jinseokoh/purchase-webhooks 

You must publish the config file with:

php artisan vendor:publish --provider="JinseokOh\PurchaseWebhooks\PurchaseWebhooksServiceProvider" --tag="config" 

This is the config that will be published.

<?php

return [
    // apple
    'appstore_sandbox' => (bool) env('APPLE_IAP_SANDBOX', true),
    'appstore_password' => env('APPLE_SHARED_SECRET'),

    // google
    'play_package_name' => env('GOOGLE_PLAY_PACKAGE_NAME', 'com.what.ever'),

    /*
     * Uncomment the events that should be handeled by your application.
     *
     * as for the apple app store server notifications, refer the following docs
     * https://developer.apple.com/documentation/storekit/in-app_purchase/enabling_server-to-server_notifications
     * as for the google play store server notifications, refer the following docs
     * https://developer.android.com/google/play/billing/rtdn-reference
     */

    'jobs' => [
        // 'CANCEL' => \App\Jobs\Apple\WebhookCancel::class,
        // 'DID_CHANGE_RENEWAL_PREF' => \App\Jobs\Apple\WebhookDidChangeRenewalPref::class,
        // 'DID_CHANGE_RENEWAL_STATUS' => \App\Jobs\Apple\WebhookDidChangeRenewalStatus::class,
        // 'DID_FAIL_TO_RENEW' => \App\Jobs\Apple\WebhookDidFailToRenew::class,
        // 'DID_RECOVER' => \App\Jobs\Apple\WebhookDidRecover::class,
        // 'DID_RENEW' => \App\Jobs\Apple\WebhookDidRenew::class,
        // 'INITIAL_BUY' => \App\Jobs\Apple\WebhookInitialBuy::class,
        // 'INTERACTIVE_RENEWAL' => \App\Jobs\Apple\WebhookInteractiveRenewal::class,
        // 'PRICE_INCREASE_CONSENT' => \App\Jobs\Apple\WebhookPriceIncreaseConsent::class,
        // 'REFUND' => \App\Jobs\Apple\WebhookRefund::class,

        // 'ONE_TIME_PRODUCT_PURCHASED' => \App\Jobs\Google\WebhookOneTimeProductPurchased::class,
        // 'ONE_TIME_PRODUCT_CANCELED' => \App\Jobs\Google\WebhookOneTimeProductCanceled::class,
        // 'SUBSCRIPTION_RECOVERED' => \App\Jobs\Google\WebhookRecovered::class,
        // 'SUBSCRIPTION_RENEWED' => \App\Jobs\Google\WebhookRenewed::class,
        // 'SUBSCRIPTION_CANCELED' =>  \App\Jobs\Google\WebhookCanceled::class,
        // 'SUBSCRIPTION_PURCHASED' => \App\Jobs\Google\WebhookPurchased::class,
        // 'SUBSCRIPTION_ON_HOLD' => \App\Jobs\Google\WebhookOnHold::class,
        // 'SUBSCRIPTION_IN_GRACE_PERIOD' => \App\Jobs\Google\WebhookInGracePeriod::class,
        // 'SUBSCRIPTION_RESTARTED' => \App\Jobs\Google\WebhookRestarted::class,
        // 'SUBSCRIPTION_PRICE_CHANGE_CONFIRMED' => \App\Jobs\Google\WebhookPriceChangeConfirmed::class,
        // 'SUBSCRIPTION_DEFERRED' => \App\Jobs\Google\WebhookDeferred::class,
        // 'SUBSCRIPTION_PAUSED' => \App\Jobs\Google\WebhookPaused::class,
        // 'SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED' => \App\Jobs\Google\WebhookPauseScheduleChanged::class,
        // 'SUBSCRIPTION_REVOKED' => \App\Jobs\Google\WebhookRevoked::class,
        // 'SUBSCRIPTION_EXPIRED' => \App\Jobs\Google\WebhookExpired::class
    ],
];

This package registers the following POST routes

  • /webhooks/apple/purchase
  • /webhooks/google/purchase

Usage

Uncomment any events of your interest in config/purchase.php, and let each Job class handle the payload from Apple/Googe server.

A sample Laravel job to handle Apple Server Notifications.

<?php

namespace App\Jobs\Apple;

use App\Events\VoidProductPurchase;
use App\Handlers\OrderHandler;
use JinseokOh\PurchaseWebhooks\ServerNotifications\Apple\ReceiptResponse;

class WebhookRefund
{
    private ReceiptResponse $response;
    private OrderHandler $orderHandler;

    public function __construct(
        ReceiptResponse $response,
        OrderHandler $orderHandler
    ) {
        $this->response = $response;
        $this->orderHandler = $orderHandler;
    }

    public function handle()
    {
        foreach ($this->response->getLatestReceiptInfo() as $receiptInfo) {
            $transactionId = $receiptInfo->getTransactionId();
            $order = $this->orderHandler->findByPurchaseToken($transactionId);
            if ($order) {
                event(new VoidProductPurchase($order));
            }
        }
    }
}

A sample Laravel job to handle Google Server Notifications.

<?php

namespace App\Jobs\Google;

use App\Events\VoidProductPurchase;
use App\Handlers\OrderHandler;

class WebhookOneTimeProductCanceled
{
    private string $purchaseToken;
    private string $sku;
    private OrderHandler $orderHandler;

    public function __construct(
        string $purchaseToken,
        string $sku,
        OrderHandler $orderHandler
    ) {
        $this->purchaseToken = $purchaseToken;
        $this->sku = $sku;
        $this->orderHandler = $orderHandler;
    }

    public function handle()
    {
        $order = $this->orderHandler
            ->findByPurchaseToken($this->purchaseToken);
        if ($order) {
            event(new VoidProductPurchase($order));
        }
    }
}

Credits

This package is heavily based on app-vise/laravel-appstore-notifications and imdhemy/laravel-in-app-purchases A big thanks to the authors of their great packages.

License

The MIT License (MIT). Please see License File for more information.