Google Play, App Store and App Gallery provide the In-App Purchase (IAP) services. IAP can be used to sell a variety of content, including subscriptions, new features, and services. The purchase event and the payment process occurs on and handled by the mobile application (iOS and Android), then your backend needs to be informed about this purchase event to deliver the purchased product or update the user's subscription state.
Laravel In-App purchase comes to help you to parse and validate the purchased products and handle the different states of a subscription, like New subscription , auto-renew, cancellation, expiration and etc.
- Installation
- Configuration
- Sell Products
- Sell Subscriptions
- Purchase Events
- Testing
Install the package via composer:
composer require imdhemy/laravel-purchases
Publish the config file:
php artisan vendor:publish --provider="Imdhemy\Purchases\PurchaseServiceProvider" --tag="config"
The published config file config/purchase.php
looks like:
return [
'routing' => [],
'google_play_package_name' => env('GOOGLE_PLAY_PACKAGE_NAME', 'com.example.name'),
'appstore_password' => env('APPSTORE_PASSWORD', ''),
'eventListeners' => [
/**
* --------------------------------------------------------
* Google Play Events
* --------------------------------------------------------
*/
SubscriptionPurchased::class => [],
SubscriptionRenewed::class => [],
SubscriptionInGracePeriod::class => [],
SubscriptionExpired::class => [],
SubscriptionCanceled::class => [],
SubscriptionPaused::class => [],
SubscriptionRestarted::class => [],
SubscriptionDeferred::class => [],
SubscriptionRevoked::class => [],
SubscriptionOnHold::class => [],
SubscriptionRecovered::class => [],
SubscriptionPauseScheduleChanged::class => [],
SubscriptionPriceChangeConfirmed::class => [],
/**
* --------------------------------------------------------
* Appstore Events
* --------------------------------------------------------
*/
Cancel::class => [],
DidChangeRenewalPref::class => [],
DidChangeRenewalStatus::class => [],
DidFailToRenew::class => [],
DidRecover::class => [],
DidRenew::class => [],
InitialBuy::class => [],
InteractiveRenewal::class => [],
PriceIncreaseConsent::class => [],
Refund::class => [],
/**
* --------------------------------------------------------
* AppGallery Events
* --------------------------------------------------------
*/
AppGalleryCancel::class => [],
AppGalleryDeferred::class => [],
AppGalleryInitialBuy::class => [],
AppGalleryInteractiveRenewal::class => [],
AppGalleryNewRenewalPref::class => [],
AppGalleryOnHold::class => [],
AppGalleryPaused::class => [],
AppGalleryPausePlanChanged::class => [],
AppGalleryPriceChangeConfirmed::class => [],
AppGalleryRenewal::class => [],
AppGalleryRenewalRecurring::class => [],
AppGalleryRenewalRestored::class => [],
AppGalleryRenewalStopped::class => [],
],
];
Each configuration option is illustrated in the configuration section.
The generic configurations are not specific to a particular provider of the three supported providers (Google, Apple and Huawei).
This package adds two POST
endpoints to receive the Real-Time Developer Notifications, and the The App Store Server Notifications.
Provider | URI | Name |
---|---|---|
Google Play | /purchases/subscriptions/google |
purchase.serverNotifications.google |
App Store | /purchases/subscriptions/apple |
purchase.serverNotifications.apple |
App Gallery | /purchases/subscriptions/huawei |
purchase.serverNotifications.huawei |
These routes can be configured through the routing
key in the config file. For example:
[
// ..
'routing' => [
'middleware' => 'api',
'prefix' => 'my_prefix'
],
// ..
];
Your application should handle the different states of a subscription life. Each state update triggers a specified event. You can create an event listener to update your backend on each case.
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionRenewed;
class AutoRenewSubscription
{
/**
* @param SubscriptionRenewed $event
*/
public function handle(SubscriptionRenewed $event)
{
// do some stuff
}
}
Add the created listener to the associated event key.
// ..
SubscriptionRenewed::class => [AutoRenewSubscription::class],
// ..
All events extend the \Imdhemy\Purchases\Events\PurchaseEvent
abstract class, which implements the \Imdhemy\Purchases\Contracts\PurchaseEventContract
interface. Check the Purchase Events section for more information.
The following set of configurations are specific to google play:
Requests to the Google Play Developer API, requires authentication and scopes. To authenticate your machine create a service account, then upload the downloaded JSON file google-app-credentials.json
to your server, and finally add GOOGLE_APPLICATION_CREDENTIALS
key to your .env
file and set it to the path of JSON file.
- In the Cloud Console, go to the Create service account key page.
- From the Service account list, select New service account.
- In the Service account name field, enter a name.
- From the Role list, select Project > Owner.
- Click Create. A JSON file that contains your key downloads to your computer.
- Upload the JSON file to your storage directory, or any other protected directory.
- Set the
.env
keyGOOGLE_APPLICATION_CREDENTIALS
to the JSON file path.
The package name of the application for which this subscription was purchased (for example, 'com.some.thing').
The following set of configurations are specific to the App Store.
The configuration key appstore_sandbox
is a boolean value that determines whether the requests sent to the App Store are in the sandbox or not.
Request to the App Store requires the key appstore_password
to be set. Your app’s shared secret, which is a hexadecimal string.
The following set of configurations are specific to the App Gallery.
The app_gallery_app_id
config key is your app ID, which you can get in AppGallery Connect -> Project Settings -> General information in section App information field App ID.
Request to the App Gallery requires OAuth 2.0 credentials, including the key app_gallery_app_key
. Your app’s client secret, which is needed to get jwt.
To verify the signature of server notifications, you need key app_gallery_public_key
. The IAP request result will be signed by the private key of your app. This public key used to verify the signature with SHA256WithRSA or SHA256WithRSA/PSS.
You can use the \Imdhemy\Purchases\Facades\Product
facade to acknowledge or to get the receipt data from Google Play as follows:
use \Imdhemy\Purchases\Facades\Product;
use \Imdhemy\GooglePlay\Products\ProductPurchase;
$itemId = 'product_id';
$token = 'purchase_token';
Product::googlePlay()->id($itemId)->token($token)->acknowledge();
// You can optionally submit a developer payload
Product::googlePlay()->id($itemId)->token($token)->acknowledge("your_developer_payload");
/** @var ProductPurchase $productReceipt */
$productReceipt = Product::googlePlay()->id($itemId)->token($token)->get();
Each key has a getter method prefixed with get
, for example: getKind()
to get the kind
value.
For more information check:
You can use the \Imdhemy\Purchases\Facades\Product
to send a verifyReceipt request to the App Store. as follows:
use \Imdhemy\AppStore\Receipts\ReceiptResponse;
use \Imdhemy\Purchases\Facades\Product;
$receiptData = 'the_base64_encoded_receipt_data';
/** @var ReceiptResponse $receiptResponse */
$receiptResponse = Product::appStore()->receiptData($receiptData)->verifyReceipt();
As usual each key has a getter method.
For more information check:
You can use the \Imdhemy\Purchases\Facades\Product
to validate purchase and get receipt data as follows:
use \Huawei\IAP\Response\OrderResponse;
use \Imdhemy\Purchases\Facades\Product;
$token = 'purchase_token';
$productId = 'product_id';
/** @var OrderResponse $orderResponse */
$orderResponse = Product::appGallery()->appGalleryValidatePurchase($product_id, $token);
As usual each key has a getter method.
For more information check:
You can use the \Imdhemy\Purchases\Facades\Subscription
facade to acknowledge or to get the receipt data from Google Play as follows:
use Imdhemy\GooglePlay\Subscriptions\SubscriptionPurchase;
use Imdhemy\Purchases\Facades\Subscription;
$itemId = 'product_id';
$token = 'purchase_token';
Subscription::googlePlay()->id($itemId)->token($token)->acknowledge();
// You can optionally submit a developer payload
Subscription::googlePlay()->id($itemId)->token($token)->acknowledge("your_developer_payload");
/** @var SubscriptionPurchase $subscriptionReceipt */
$subscriptionReceipt = Subscription::googlePlay()->id($itemId)->token($token)->get();
// You can optionally override the package name
Subscription::googlePlay()->packageName('com.example.name')->id($itemId)->token($token)->get();
The SubscriptionPurchase
resource indicates the status of a user's inapp product purchase. This is its JSON Representation:
For more information check:
You can use the \Imdhemy\Purchases\Facades\Subscription
to send a verifyReceipt request to the App Store. as follows:
use Imdhemy\Purchases\Facades\Subscription;
// To verify a subscription receipt
$receiptData = 'the_base64_encoded_receipt_data';
$receiptResponse = Subscription::appStore()->receiptData($receiptData)->verifyReceipt();
// If the subscription is an auto-renewable one,
//call the renewable() method before the trigger method verifyReceipt()
$receiptResponse = Subscription::appStore()->receiptData($receiptData)->renewable()->verifyReceipt();
// or you can omit the renewable() method and use the verifyRenewable() method instead
$receiptResponse = Subscription::appStore()->receiptData($receiptData)->verifyRenewable();
For more information check:
You can use the \Imdhemy\Purchases\Facades\Subscription
to validate subscription purchase token and get receipt data. as follows:
use Imdhemy\Purchases\Facades\Subscription;
use \Huawei\IAP\Response\SubscriptionResponse;
$token = 'purchase_token';
$productId = 'product_id';
$subscriptionId = 'subscription_id';
/** @var SubscriptionResponse $subscriptionResponse */
$subscriptionResponse = Subscription::appGallery()
->appGalleryValidateSubscription($subscriptionId, $productId, $token);
For more information check:
As mentioned the configuration section, Your application should handle the different states of a subscription life. Each state update triggers a specified event. You can create an event listener to update your backend on each case.
All triggered events implement Imdhemy\Purchases\Contracts\PurchaseEventContract
interface, which allows you to get a standard representation of the received notification through the getServerNotification()
method.
The retrieved notification is of type \Imdhemy\Purchases\Contracts\ServerNotificationContract
which allows you to get a standard representation of the subscription using the getSubscription()
method.
The subscription object provides the following methods:
getExpiryTime()
returns aTime
object that tells the expiration time of the subscription.getItemId()
returns the purchased subscription id.getProvider()
returns the provider of this subscription, eithergoogle_play
orapp_store
.getUniqueIdentifier()
returns a unique identifier for this subscription.getProviderRepresentation()
returns eitherSubscriptionPurchase
orReceiptResponse
based on the provider.
Here is an example of an auto-renewed subscription:
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionRenewed;
class AutoRenewSubscription
{
/**
* @param SubscriptionRenewed $event
*/
public function handle(SubscriptionRenewed $event)
{
// The following data can be retrieved from the event
$notification = $event->getServerNotification();
$subscription = $notification->getSubscription();
$uniqueIdentifier = $subscription->getUniqueIdentifier();
$expirationTime = $subscription->getExpiryTime();
// The following steps are up to you, this is only an imagined scenario.
$user = $this->findUserBySubscriptionId($uniqueIdentifier);
$user->getSubscription()->setExpirationTime($expirationTime);
$user->save();
$this->notifyUserAboutUpdate($user);
}
}
TODO: add testing examples.