Daan Geurts
5 years ago
commit
7f4b49d98d
28 changed files with 1340 additions and 0 deletions
@ -0,0 +1,15 @@ |
|||
; This file is for unifying the coding style for different editors and IDEs. |
|||
; More information at http://editorconfig.org |
|||
|
|||
root = true |
|||
|
|||
[*] |
|||
charset = utf-8 |
|||
indent_size = 4 |
|||
indent_style = space |
|||
end_of_line = lf |
|||
insert_final_newline = true |
|||
trim_trailing_whitespace = true |
|||
|
|||
[*.md] |
|||
trim_trailing_whitespace = false |
@ -0,0 +1,11 @@ |
|||
# Path-based git attributes |
|||
# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html |
|||
|
|||
# Ignore all test and documentation with "export-ignore". |
|||
/.gitattributes export-ignore |
|||
/.gitignore export-ignore |
|||
/.travis.yml export-ignore |
|||
/phpunit.xml.dist export-ignore |
|||
/.scrutinizer.yml export-ignore |
|||
/tests export-ignore |
|||
/.editorconfig export-ignore |
@ -0,0 +1,5 @@ |
|||
build |
|||
composer.lock |
|||
vendor |
|||
coverage.clover |
|||
.phpunit.result.cache |
@ -0,0 +1,18 @@ |
|||
filter: |
|||
excluded_paths: [tests/*] |
|||
|
|||
checks: |
|||
php: |
|||
remove_extra_empty_lines: true |
|||
remove_php_closing_tag: true |
|||
remove_trailing_whitespace: true |
|||
fix_use_statements: |
|||
remove_unused: true |
|||
preserve_multiple: false |
|||
preserve_blanklines: true |
|||
order_alphabetically: true |
|||
fix_php_opening_tag: true |
|||
fix_linefeed: true |
|||
fix_line_ending: true |
|||
fix_identation_4spaces: true |
|||
fix_doc_comments: true |
@ -0,0 +1,4 @@ |
|||
preset: laravel |
|||
|
|||
disabled: |
|||
- single_class_element_per_statement |
@ -0,0 +1,35 @@ |
|||
language: php |
|||
|
|||
cache: |
|||
directories: |
|||
- $HOME/.composer/cache |
|||
|
|||
matrix: |
|||
fast_finish: true |
|||
include: |
|||
- php: 7.2 |
|||
env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' |
|||
- php: 7.2 |
|||
env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' |
|||
- php: 7.3 |
|||
env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' |
|||
- php: 7.3 |
|||
env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' |
|||
- php: 7.2 |
|||
env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' |
|||
- php: 7.2 |
|||
env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' |
|||
- php: 7.3 |
|||
env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' |
|||
- php: 7.3 |
|||
env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' |
|||
|
|||
before_install: |
|||
- travis_retry composer self-update |
|||
- travis_retry composer require --no-update --no-interaction "illuminate/support:${LARAVEL}" "orchestra/testbench:${TESTBENCH}" |
|||
|
|||
install: |
|||
- travis_retry composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction --no-suggest |
|||
|
|||
script: |
|||
- vendor/bin/phpunit |
@ -0,0 +1,21 @@ |
|||
The MIT License (MIT) |
|||
|
|||
Copyright (c) App-vise V.O.F. <info@app-vise.nl> |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in |
|||
all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|||
THE SOFTWARE. |
@ -0,0 +1,47 @@ |
|||
# Handle Appstore server-to-server notifications for auto-renewable subscriptions |
|||
|
|||
[![Latest Version on Packagist](https://img.shields.io/packagist/v/app-vise/laravel-appstore-notifications.svg?style=flat-square)](https://packagist.org/packages/app-vise/laravel-appstore-notifications) |
|||
[![Build Status](https://img.shields.io/travis/app-vise/laravel-appstore-notifications/master.svg?style=flat-square)](https://travis-ci.org/app-vise/laravel-appstore-notifications) |
|||
[![StyleCI](https://styleci.io/repos/105920179/shield?branch=master)](https://styleci.io/repos/105920179) |
|||
[![Quality Score](https://img.shields.io/scrutinizer/g/app-vise/laravel-appstore-notifications.svg?style=flat-square)](https://scrutinizer-ci.com/g/app-vise/laravel-appstore-notifications) |
|||
[![Total Downloads](https://img.shields.io/packagist/dt/app-vise/laravel-appstore-notifications.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-appstore-notifications) |
|||
|
|||
## Installation |
|||
You can install this package via composer |
|||
|
|||
```bash |
|||
composer require app-vise/laravel-appstore-server-notifications |
|||
``` |
|||
|
|||
The service provider will register itself. |
|||
You have to publish the config file with: |
|||
|
|||
```bash |
|||
php artisan vendor:publish --provider="Appvise\AppStoreNotifications\NotificationsServiceProvider" --tag="config" |
|||
``` |
|||
|
|||
## Usage |
|||
|
|||
## Changelog |
|||
|
|||
Please see CHANGELOG for more information about what has changed recently. |
|||
|
|||
## Testing |
|||
|
|||
```bash |
|||
composer test |
|||
``` |
|||
|
|||
## Security |
|||
|
|||
If you discover any security related issues, please email [email protected] instead of using the issue tracker. |
|||
|
|||
## Credits |
|||
|
|||
- [Daan Geurts](https://github.com/DaanGeurts) |
|||
- [All Contributors](../../contributors) |
|||
|
|||
A big thanks to [Spatie's](https://spatie.be) laravel-stripe-webhooks which was a huge inspiration and starting point for this package |
|||
## License |
|||
|
|||
The MIT License (MIT). Please see [License File](LICENSE.md) for more information. |
@ -0,0 +1,53 @@ |
|||
{ |
|||
"name": "app-vise/laravel-appstore-server-notifications", |
|||
"description": "Handling Appstore server to server notifications", |
|||
"keywords": [ |
|||
"app-vise", |
|||
"appvise", |
|||
"laravel-appstore-server-to-server-notifications", |
|||
"laravel-appstore-server-notifications", |
|||
"laravel in app subscriptions", |
|||
"laravel-appstore-server-notifications" |
|||
], |
|||
"license": "MIT", |
|||
"authors": [ |
|||
{ |
|||
"name": "Daan Geurts", |
|||
"email": "[email protected]", |
|||
"homepage": "https://www.app-vise.nl", |
|||
"role": "Developer" |
|||
} |
|||
], |
|||
"require": { |
|||
"php": "^7.2", |
|||
"illuminate/support": "~5.7.0|^6.0", |
|||
"bensampo/laravel-enum": "^1.0" |
|||
}, |
|||
"require-dev": { |
|||
"orchestra/testbench": "~3.8.0|^4.0", |
|||
"phpunit/phpunit": "^8.2" |
|||
}, |
|||
"autoload": { |
|||
"psr-4": { |
|||
"Appvise\\AppStoreNotifications\\": "src" |
|||
} |
|||
}, |
|||
"autoload-dev": { |
|||
"psr-4": { |
|||
"Appvise\\AppStoreNotifications\\Tests\\": "tests" |
|||
} |
|||
}, |
|||
"scripts": { |
|||
"test": "vendor/bin/phpunit --verbose" |
|||
}, |
|||
"config": { |
|||
"sort-packages": true |
|||
}, |
|||
"extra": { |
|||
"laravel": { |
|||
"providers": [ |
|||
"Appvise\\AppStoreNotifications\\NotificationsServiceProvider" |
|||
] |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,24 @@ |
|||
<?php |
|||
return [ |
|||
/* |
|||
* Apple will send the shared secret with the request that should match |
|||
* the one you use when validating receipts. |
|||
* https://developer.apple.com/documentation/storekit/in-app_purchase/enabling_server-to-server_notifications?language=objc#overview |
|||
*/ |
|||
'shared_secret' => env('APPLE_SHARED_SECRET'), |
|||
/* |
|||
* All the events that should be handeled by your application. |
|||
* Typically you should uncomment all jobs |
|||
* |
|||
* You can find a list of all notification types here: |
|||
* https://developer.apple.com/documentation/storekit/in-app_purchase/enabling_server-to-server_notifications?language=objc#3162176 |
|||
*/ |
|||
'jobs' => [ |
|||
// 'initial_buy' => \App\Jobs\AppstoreNotifications\HandleInitialBuy::class, |
|||
// 'cancel' => \App\Jobs\AppstoreNotifications\HandleCancellation::class, |
|||
// 'renewal' => \App\Jobs\AppstoreNotifications\HandleRenewal::class, |
|||
// 'interactive_renewal' => \App\Jobs\AppstoreNotifications\HandleInteractiveRenewal::class, |
|||
// 'did_change_renewal_pref' => \App\Jobs\AppstoreNotifications\HandleDidChangeRenewalPreferences::class, |
|||
// 'did_change_renewal_status' => \App\Jobs\AppstoreNotifications\HandleDidChangeRenewalStatus::class, |
|||
], |
|||
]; |
@ -0,0 +1,25 @@ |
|||
<?php |
|||
|
|||
use Illuminate\Database\Schema\Blueprint; |
|||
use Illuminate\Database\Migrations\Migration; |
|||
|
|||
class CreateAppleNotificationsTable extends Migration |
|||
{ |
|||
public function up() |
|||
{ |
|||
Schema::create('apple_notifications', function (Blueprint $table) { |
|||
$table->bigIncrements('id'); |
|||
|
|||
$table->string('type'); |
|||
$table->text('payload')->nullable(); |
|||
$table->text('exception')->nullable(); |
|||
|
|||
$table->timestamps(); |
|||
}); |
|||
} |
|||
|
|||
public function down() |
|||
{ |
|||
Schema::dropIfExists('apple_notifications'); |
|||
} |
|||
} |
@ -0,0 +1,29 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<phpunit bootstrap="vendor/autoload.php" |
|||
backupGlobals="false" |
|||
backupStaticAttributes="false" |
|||
colors="true" |
|||
verbose="true" |
|||
convertErrorsToExceptions="true" |
|||
convertNoticesToExceptions="true" |
|||
convertWarningsToExceptions="true" |
|||
processIsolation="true" |
|||
stopOnFailure="false"> |
|||
<testsuites> |
|||
<testsuite name="App-vise Test Suite"> |
|||
<directory>tests</directory> |
|||
</testsuite> |
|||
</testsuites> |
|||
<filter> |
|||
<whitelist> |
|||
<directory suffix=".php">src/</directory> |
|||
</whitelist> |
|||
</filter> |
|||
<logging> |
|||
<log type="tap" target="build/report.tap"/> |
|||
<log type="junit" target="build/report.junit.xml"/> |
|||
<log type="coverage-html" target="build/coverage"/> |
|||
<log type="coverage-text" target="build/coverage.txt"/> |
|||
<log type="coverage-clover" target="build/logs/clover.xml"/> |
|||
</logging> |
|||
</phpunit> |
@ -0,0 +1,30 @@ |
|||
<?php |
|||
namespace Appvise\AppStoreNotifications; |
|||
|
|||
use Illuminate\Support\ServiceProvider; |
|||
|
|||
class NotificationsServiceProvider extends ServiceProvider |
|||
{ |
|||
public function boot() |
|||
{ |
|||
if ($this->app->runningInConsole()) { |
|||
$this->publishes([ |
|||
__DIR__.'/../config/appstore-server-notifications.php' => config_path('appstore-server-notifications.php'), |
|||
], 'config'); |
|||
} |
|||
|
|||
if (! class_exists('CreateAppleNotificationsTable')) { |
|||
$timestamp = date('Y_m_d_His', time()); |
|||
$this->publishes([ |
|||
__DIR__.'/../database/migrations/create_apple_notifications_table.php.stub' => database_path("migrations/{$timestamp}_create_apple_notifications_table.php"), |
|||
], 'migrations'); |
|||
} |
|||
|
|||
$this->loadRoutesFrom(__DIR__.'/routes.php'); |
|||
} |
|||
|
|||
public function register() |
|||
{ |
|||
$this->mergeConfigFrom(__DIR__.'/../config/appstore-server-notifications.php', 'appstore-server-notifications'); |
|||
} |
|||
} |
@ -0,0 +1,43 @@ |
|||
<?php |
|||
|
|||
namespace Appvise\AppStoreNotifications; |
|||
|
|||
use Appvise\AppStoreNotifications\Exceptions\WebhookFailed; |
|||
use Appvise\AppStoreNotifications\Model\AppleNotification; |
|||
use Appvise\AppStoreNotifications\Model\NotificationPayload; |
|||
use Appvise\AppStoreNotifications\Model\NotificationType; |
|||
use Illuminate\Http\Request; |
|||
|
|||
class WebhooksController |
|||
{ |
|||
public function __invoke(Request $request) |
|||
{ |
|||
$jobConfigKey = NotificationType::{$request->input('notification_type')}(); |
|||
$payload = NotificationPayload::createFromRequest($request); |
|||
|
|||
$this->determineValidRequest($payload); |
|||
|
|||
AppleNotification::storeNotification($jobConfigKey, $payload); |
|||
|
|||
$jobClass = config("appstore-server-notifications.jobs.{$jobConfigKey}", null); |
|||
|
|||
if (is_null($jobClass)) { |
|||
throw WebhookFailed::jobClassDoesNotExist($jobConfigKey); |
|||
} |
|||
|
|||
|
|||
$job = new $jobClass($payload); |
|||
dispatch($job); |
|||
|
|||
return response()->json(); |
|||
} |
|||
|
|||
private function determineValidRequest(NotificationPayload $notificationPayload): bool |
|||
{ |
|||
if ($notificationPayload->getPassword() !== config('appstore-server-notifications.shared_secret')) { |
|||
throw WebhookFailed::nonValidRequest(); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
@ -0,0 +1,23 @@ |
|||
<?php |
|||
|
|||
namespace Appvise\AppStoreNotifications\Exceptions; |
|||
|
|||
use Exception; |
|||
|
|||
class WebhookFailed extends Exception |
|||
{ |
|||
public static function nonValidRequest() |
|||
{ |
|||
return new static("Your shared secret does not match password in Apple's request", 400); |
|||
} |
|||
|
|||
public static function jobClassDoesNotExist(string $jobClass) |
|||
{ |
|||
return new static("Could not process webhook because the configured job `$jobClass` does not exist.", 501); |
|||
} |
|||
|
|||
public function render($request) |
|||
{ |
|||
return response(['error' => $this->getMessage()], 400); |
|||
} |
|||
} |
@ -0,0 +1,19 @@ |
|||
<?php |
|||
|
|||
namespace Appvise\AppStoreNotifications\Model; |
|||
|
|||
use Illuminate\Database\Eloquent\Model; |
|||
|
|||
class AppleNotification extends Model |
|||
{ |
|||
public $guarded = []; |
|||
|
|||
public static function storeNotification(String $notificationType, NotificationPayload $notificationPayload): AppleNotification |
|||
{ |
|||
return self::create([ |
|||
'type' => $notificationType, |
|||
'payload' => serialize($notificationPayload), |
|||
]); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,193 @@ |
|||
<?php |
|||
|
|||
namespace Appvise\AppStoreNotifications\Model; |
|||
|
|||
use Illuminate\Http\Request; |
|||
|
|||
class NotificationPayload |
|||
{ |
|||
private $environment; |
|||
private $notificationType; |
|||
private $password; |
|||
private $cancellationDate; |
|||
private $cancellationDatePst; |
|||
private $cancellationDateMs; |
|||
private $webOrderLineItemId; |
|||
private $latestReceipt; |
|||
private $latestReceiptInfo; |
|||
private $latestExpiredReceipt; |
|||
private $latestExpiredReceiptInfo; |
|||
private $autoRenewStatus; |
|||
private $autoRenewProductId; |
|||
private $autoRenewStatusChangeDate; |
|||
private $autoRenewStatusChangeDatePst; |
|||
private $autoRenewStatusChangeDateMs; |
|||
private $pendingRenewalInfo; |
|||
|
|||
public function __construct() |
|||
{ |
|||
|
|||
} |
|||
|
|||
static function createFromRequest(Request $request) |
|||
{ |
|||
$instance = new self(); |
|||
$instance->environment = $request->input('environment'); |
|||
$instance->password = $request->input('password'); |
|||
$instance->notificationType = $request->input('notification_type'); |
|||
$instance->cancellationDate = $request->input('cancellation_date'); |
|||
$instance->cancellationDatePst = $request->input('cancellation_date_pst'); |
|||
$instance->cancellationDateMs = $request->input('cancellation_date_ms'); |
|||
$instance->webOrderLineItemId = $request->input('web_order_line_item_id'); |
|||
$instance->latestReceipt = $request->input('latest_receipt'); |
|||
$instance->latestReceiptInfo = Receipt::createFromArray($request->input('latest_receipt_info')); |
|||
$instance->latestExpiredReceipt = $request->input('latest_expired_receipt'); |
|||
$instance->latestExpiredReceiptInfo = Receipt::createFromArray($request->input('latest_expired_receipt_info')); |
|||
$instance->autoRenewStatus = $request->input('auto_renew_status'); |
|||
$instance->autoRenewProductId = $request->input('auto_renew_product_id'); |
|||
$instance->autoRenewStatusChangeDate = $request->input('auto_renew_status_change_date'); |
|||
$instance->autoRenewStatusChangeDatePst = $request->input('auto_renew_status_change_date_pst'); |
|||
$instance->autoRenewStatusChangeDateMs = $request->input('auto_renew_status_change_date_ms'); |
|||
foreach ($request->input('pending_renewal_info') as $pendingRenewalInfo) { |
|||
$instance->pendingRenewalInfo[] = RenewalInfo::createFromRequest($pendingRenewalInfo); |
|||
} |
|||
|
|||
return $instance; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of environment |
|||
*/ |
|||
public function getEnvironment() |
|||
{ |
|||
return $this->environment; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of notificationType |
|||
*/ |
|||
public function getNotificationType() |
|||
{ |
|||
return $this->notificationType; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of pendingRenewalInfo |
|||
*/ |
|||
public function getPendingRenewalInfo() |
|||
{ |
|||
return $this->pendingRenewalInfo; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of autoRenewStatusChangeDateMs |
|||
*/ |
|||
public function getAutoRenewStatusChangeDateMs() |
|||
{ |
|||
return $this->autoRenewStatusChangeDateMs; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of autoRenewStatusChangeDatePst |
|||
*/ |
|||
public function getAutoRenewStatusChangeDatePst() |
|||
{ |
|||
return $this->autoRenewStatusChangeDatePst; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of autoRenewStatusChangeDate |
|||
*/ |
|||
public function getAutoRenewStatusChangeDate() |
|||
{ |
|||
return $this->autoRenewStatusChangeDate; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of autoRenewProductId |
|||
*/ |
|||
public function getAutoRenewProductId() |
|||
{ |
|||
return $this->autoRenewProductId; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of autoRenewStatus |
|||
*/ |
|||
public function getAutoRenewStatus() |
|||
{ |
|||
return $this->autoRenewStatus; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of latestExpiredReceiptInfo |
|||
*/ |
|||
public function getLatestExpiredReceiptInfo() |
|||
{ |
|||
return $this->latestExpiredReceiptInfo; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of latestExpiredReceipt |
|||
*/ |
|||
public function getLatestExpiredReceipt() |
|||
{ |
|||
return $this->latestExpiredReceipt; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of latestReceiptInfo |
|||
*/ |
|||
public function getLatestReceiptInfo() |
|||
{ |
|||
return $this->latestReceiptInfo; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of latestReceipt |
|||
*/ |
|||
public function getLatestReceipt() |
|||
{ |
|||
return $this->latestReceipt; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of webOrderLineItemId |
|||
*/ |
|||
public function getWebOrderLineItemId() |
|||
{ |
|||
return $this->webOrderLineItemId; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of cancellationDateMs |
|||
*/ |
|||
public function getCancellationDateMs() |
|||
{ |
|||
return $this->cancellationDateMs; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of cancellationDatePst |
|||
*/ |
|||
public function getCancellationDatePst() |
|||
{ |
|||
return $this->cancellationDatePst; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of cancellationDate |
|||
*/ |
|||
public function getCancellationDate() |
|||
{ |
|||
return $this->cancellationDate; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of password |
|||
*/ |
|||
public function getPassword() |
|||
{ |
|||
return $this->password; |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
<?php |
|||
|
|||
namespace Appvise\AppStoreNotifications\Model; |
|||
|
|||
use BenSampo\Enum\Enum; |
|||
|
|||
class NotificationType extends Enum |
|||
{ |
|||
const INITIAL_BUY = 'initial_buy'; |
|||
const CANCEL = 'cancel'; |
|||
const RENEWAL = 'renewal'; |
|||
const INTERACTIVE_RENEWAL = 'interactive_renewal'; |
|||
const DID_CHANGE_RENEWAL_PREF = 'did_change_renewal_pref'; |
|||
const DID_CHANGE_RENEWAL_STATUS = 'did_change_renewal_status'; |
|||
const DID_FAIL_TO_RENEW = 'did_fail_to_renew'; |
|||
const DID_RECOVER = 'did_recover'; // replaces RENEWAL |
|||
const PRICE_INCREASE_CONSENT = 'price_increase_consent'; |
|||
} |
@ -0,0 +1,301 @@ |
|||
<?php |
|||
|
|||
namespace Appvise\AppStoreNotifications\Model; |
|||
|
|||
|
|||
|
|||
class Receipt |
|||
{ |
|||
private $originalTransactionId; |
|||
private $webOrderLineItemId; |
|||
private $productId; |
|||
private $purchaseDateMs; |
|||
private $purchaseDate; |
|||
private $purchaseDatePst; |
|||
private $originalPurchaseDate; |
|||
private $originalPurchaseDateMs; |
|||
private $originalPurchaseDatePst; |
|||
private $cancellationReason; |
|||
private $cancellationDate; |
|||
private $cancellationDateMs; |
|||
private $cancellationDatePst; |
|||
private $expiresDate; |
|||
private $expiresDateMs; |
|||
private $expiresDateFormatted; |
|||
private $expiresDateFormattedPst; |
|||
private $quantity; |
|||
private $uniqueIdentifier; |
|||
private $uniqueVendorIdentifier; |
|||
private $isInIntroOfferPeriod; |
|||
private $isTrialPeriod; |
|||
private $itemId; |
|||
private $appItemId; |
|||
private $versionExternalIdentifier; |
|||
private $transactionId; |
|||
private $bvrs; |
|||
private $bid; |
|||
|
|||
public function __construct() |
|||
{ |
|||
|
|||
} |
|||
|
|||
static function createFromArray(array $receiptInfo) |
|||
{ |
|||
$instance = new self(); |
|||
$instance->originalTransactionId = $receiptInfo['original_transaction_id'] ?? null; |
|||
$instance->webOrderLineItemId = $receiptInfo['web_order_line_item_id'] ?? null; |
|||
$instance->productId = $receiptInfo['product_id'] ?? null; |
|||
$instance->purchaseDateMs = $receiptInfo['purchase_date_ms'] ?? null; |
|||
$instance->purchaseDate = $receiptInfo['purchase_date'] ?? null; |
|||
$instance->purchaseDatePst = $receiptInfo['purchase_date_pst'] ?? null; |
|||
$instance->originalPurchaseDate = $receiptInfo['original_purchase_date'] ?? null; |
|||
$instance->originalPurchaseDateMs = $receiptInfo['original_purchase_date_ms'] ?? null; |
|||
$instance->originalPurchaseDatePst = $receiptInfo['original_purchase_date_pst'] ?? null; |
|||
$instance->cancellationReason = $receiptInfo['cancellation_reason'] ?? null; |
|||
$instance->cancellationDate = $receiptInfo['cancellation_date'] ?? null; |
|||
$instance->cancellationDateMs = $receiptInfo['cancellation_date_ms'] ?? null; |
|||
$instance->cancellationDatePst = $receiptInfo['cancellation_date_pst'] ?? null; |
|||
$instance->expiresDate = $receiptInfo['expires_date'] ?? null; |
|||
$instance->expiresDateMs = $receiptInfo['expires_date_ms'] ?? null; |
|||
$instance->expiresDateFormatted = $receiptInfo['expires_date_formatted'] ?? null; |
|||
$instance->expiresDateFormattedPst = $receiptInfo['expires_date_formatted_pst'] ?? null; |
|||
$instance->quantity = $receiptInfo['quantity'] ?? null; |
|||
$instance->uniqueIdentifier = $receiptInfo['unique_identifier'] ?? null; |
|||
$instance->uniqueVendorIdentifier = $receiptInfo['unique_vendor_identifier'] ?? null; |
|||
$instance->isInIntroOfferPeriod = $receiptInfo['is_in_intro_offer_period'] ?? null; |
|||
$instance->isTrialPeriod = $receiptInfo['is_trial_period'] ?? null; |
|||
$instance->itemId = $receiptInfo['item_id'] ?? null; |
|||
$instance->appItemId = $receiptInfo['app_item_id'] ?? null; |
|||
$instance->versionExternalIdentifier = $receiptInfo['version_external_identifier'] ?? null; |
|||
$instance->transactionId = $receiptInfo['transaction_id'] ?? null; |
|||
$instance->bvrs = $receiptInfo['bvrs'] ?? null; |
|||
$instance->bid = $receiptInfo['bid'] ?? null; |
|||
return $instance; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Get the value of bid |
|||
*/ |
|||
public function getBid() |
|||
{ |
|||
return $this->bid; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of bvrs |
|||
*/ |
|||
public function getBvrs() |
|||
{ |
|||
return $this->bvrs; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of transactionId |
|||
*/ |
|||
public function getTransactionId() |
|||
{ |
|||
return $this->transactionId; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of versionExternalIdentifier |
|||
*/ |
|||
public function getVersionExternalIdentifier() |
|||
{ |
|||
return $this->versionExternalIdentifier; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of appItemId |
|||
*/ |
|||
public function getAppItemId() |
|||
{ |
|||
return $this->appItemId; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of itemId |
|||
*/ |
|||
public function getItemId() |
|||
{ |
|||
return $this->itemId; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of isTrialPeriod |
|||
*/ |
|||
public function getIsTrialPeriod() |
|||
{ |
|||
return $this->isTrialPeriod; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of isInIntroOfferPeriod |
|||
*/ |
|||
public function getIsInIntroOfferPeriod() |
|||
{ |
|||
return $this->isInIntroOfferPeriod; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of uniqueVendorIdentifier |
|||
*/ |
|||
public function getUniqueVendorIdentifier() |
|||
{ |
|||
return $this->uniqueVendorIdentifier; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of uniqueIdentifier |
|||
*/ |
|||
public function getUniqueIdentifier() |
|||
{ |
|||
return $this->uniqueIdentifier; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of quantity |
|||
*/ |
|||
public function getQuantity() |
|||
{ |
|||
return $this->quantity; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of expiresDateFormattedPst |
|||
*/ |
|||
public function getExpiresDateFormattedPst() |
|||
{ |
|||
return $this->expiresDateFormattedPst; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of expiresDateFormatted |
|||
*/ |
|||
public function getExpiresDateFormatted() |
|||
{ |
|||
return $this->expiresDateFormatted; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of expiresDateMs |
|||
*/ |
|||
public function getExpiresDateMs() |
|||
{ |
|||
return $this->expiresDateMs; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of expiresDate |
|||
*/ |
|||
public function getExpiresDate() |
|||
{ |
|||
return $this->expiresDate; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of cancellationDatePst |
|||
*/ |
|||
public function getCancellationDatePst() |
|||
{ |
|||
return $this->cancellationDatePst; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of cancellationDateMs |
|||
*/ |
|||
public function getCancellationDateMs() |
|||
{ |
|||
return $this->cancellationDateMs; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of cancellationDate |
|||
*/ |
|||
public function getCancellationDate() |
|||
{ |
|||
return $this->cancellationDate; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of cancellationReason |
|||
*/ |
|||
public function getCancellationReason() |
|||
{ |
|||
return $this->cancellationReason; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of originalPurchaseDatePst |
|||
*/ |
|||
public function getOriginalPurchaseDatePst() |
|||
{ |
|||
return $this->originalPurchaseDatePst; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of originalPurchaseDateMs |
|||
*/ |
|||
public function getOriginalPurchaseDateMs() |
|||
{ |
|||
return $this->originalPurchaseDateMs; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of originalPurchaseDate |
|||
*/ |
|||
public function getOriginalPurchaseDate() |
|||
{ |
|||
return $this->originalPurchaseDate; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of purchaseDatePst |
|||
*/ |
|||
public function getPurchaseDatePst() |
|||
{ |
|||
return $this->purchaseDatePst; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of purchaseDate |
|||
*/ |
|||
public function getPurchaseDate() |
|||
{ |
|||
return $this->purchaseDate; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of purchaseDateMs |
|||
*/ |
|||
public function getPurchaseDateMs() |
|||
{ |
|||
return $this->purchaseDateMs; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of productId |
|||
*/ |
|||
public function getProductId() |
|||
{ |
|||
return $this->productId; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of webOrderLineItemId |
|||
*/ |
|||
public function getWebOrderLineItemId() |
|||
{ |
|||
return $this->webOrderLineItemId; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of originalTransactionId |
|||
*/ |
|||
public function getOriginalTransactionId() |
|||
{ |
|||
return $this->originalTransactionId; |
|||
} |
|||
} |
@ -0,0 +1,116 @@ |
|||
<?php |
|||
|
|||
namespace Appvise\AppStoreNotifications\Model; |
|||
|
|||
|
|||
class RenewalInfo |
|||
{ |
|||
private $autoRenewProductId; |
|||
private $autoRenewStatus; |
|||
private $expirationIntent; |
|||
private $originalTransactionId; |
|||
private $isInBillingRetryPeriod; |
|||
private $productId; |
|||
private $priceConsentStatus; |
|||
private $gracePeriodExpiresDate; |
|||
private $gracePeriodExpiresDateMs; |
|||
private $gracePeriodExpiresDatePst; |
|||
|
|||
|
|||
public function __construct() |
|||
{ |
|||
} |
|||
|
|||
public static function createFromRequest(array $pendingRenewalInfo) { |
|||
$instance = new self(); |
|||
$instance->autoRenewProductId = $pendingRenewalInfo['auto_renew_product_id'] ?? null; |
|||
$instance->autoRenewStatus = $pendingRenewalInfo['auto_renew_status'] ?? null; |
|||
$instance->expirationIntent = $pendingRenewalInfo['expiration_intent'] ?? null; |
|||
$instance->originalTransactionId = $pendingRenewalInfo['original_transaction_id'] ?? null; |
|||
$instance->isInBillingRetryPeriod = $pendingRenewalInfo['is_in_billing_retry_period'] ?? null; |
|||
$instance->productId = $pendingRenewalInfo['product_id'] ?? null; |
|||
$instance->priceConsentStatus = $pendingRenewalInfo['price_consent_status'] ?? null; |
|||
$instance->gracePeriodExpiresDate = $pendingRenewalInfo['grace_period_expires_date'] ?? null; |
|||
$instance->gracePeriodExpiresDatePst = $pendingRenewalInfo['grace_period_expires_date_pst'] ?? null; |
|||
return $instance; |
|||
} |
|||
/** |
|||
* Get the value of autoRenewProductId |
|||
*/ |
|||
public function getAutoRenewProductId() |
|||
{ |
|||
return $this->autoRenewProductId; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of autoRenewStatus |
|||
*/ |
|||
public function getAutoRenewStatus() |
|||
{ |
|||
return $this->autoRenewStatus; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of expirationIntent |
|||
*/ |
|||
public function getExpirationIntent() |
|||
{ |
|||
return $this->expirationIntent; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of originalTransactionId |
|||
*/ |
|||
public function getOriginalTransactionId() |
|||
{ |
|||
return $this->originalTransactionId; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of isInBillingRetryPeriod |
|||
*/ |
|||
public function getIsInBillingRetryPeriod() |
|||
{ |
|||
return $this->isInBillingRetryPeriod; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of productId |
|||
*/ |
|||
public function getProductId() |
|||
{ |
|||
return $this->productId; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of priceConsentStatus |
|||
*/ |
|||
public function getPriceConsentStatus() |
|||
{ |
|||
return $this->priceConsentStatus; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of gracePeriodExpiresDate |
|||
*/ |
|||
public function getGracePeriodExpiresDate() |
|||
{ |
|||
return $this->gracePeriodExpiresDate; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of gracePeriodExpiresDateMs |
|||
*/ |
|||
public function getGracePeriodExpiresDateMs() |
|||
{ |
|||
return $this->gracePeriodExpiresDateMs; |
|||
} |
|||
|
|||
/** |
|||
* Get the value of gracePeriodExpiresDatePst |
|||
*/ |
|||
public function getGracePeriodExpiresDatePst() |
|||
{ |
|||
return $this->gracePeriodExpiresDatePst; |
|||
} |
|||
} |
@ -0,0 +1,3 @@ |
|||
<?php |
|||
|
|||
Route::post('/apple/server/notifications', "\Appvise\AppStoreNotifications\WebhooksController"); |
@ -0,0 +1,27 @@ |
|||
<?php |
|||
|
|||
namespace Appvise\AppStoreNotifications\Tests; |
|||
|
|||
use Appvise\AppStoreNotifications\Model\NotificationPayload; |
|||
use Illuminate\Bus\Queueable; |
|||
use Illuminate\Contracts\Queue\ShouldQueue; |
|||
use Illuminate\Foundation\Bus\Dispatchable; |
|||
use Illuminate\Queue\InteractsWithQueue; |
|||
use Illuminate\Queue\SerializesModels; |
|||
|
|||
|
|||
class DummyJob implements ShouldQueue |
|||
{ |
|||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; |
|||
|
|||
public $payload; |
|||
|
|||
public function __construct(NotificationPayload $payload) |
|||
{ |
|||
$this->payload = $payload; |
|||
} |
|||
|
|||
function handle() |
|||
{ |
|||
} |
|||
} |
@ -0,0 +1,79 @@ |
|||
<?php |
|||
|
|||
namespace Appvise\AppStoreNotifications\Tests; |
|||
|
|||
use Appvise\AppStoreNotifications\Model\AppleNotification; |
|||
use Illuminate\Support\Facades\Queue; |
|||
use Illuminate\Support\Facades\Route; |
|||
|
|||
class IntegrationTest extends TestCase |
|||
{ |
|||
public function setUp(): void |
|||
{ |
|||
parent::setUp(); |
|||
|
|||
Queue::fake(); |
|||
|
|||
Route::post('/apple/server/notifications', "\Appvise\AppStoreNotifications\WebhooksController"); |
|||
|
|||
config( |
|||
[ |
|||
'appstore-server-notifications.jobs' => [ |
|||
'initial_buy' => DummyJob::class |
|||
], |
|||
'appstore-server-notifications.shared_secret' => 'VALID_APPLE_PASSWORD', |
|||
] |
|||
); |
|||
|
|||
} |
|||
|
|||
/** @test */ |
|||
public function it_can_handle_a_valid_request() |
|||
{ |
|||
$payload = include_once __DIR__ . '/__fixtures__/request.php'; |
|||
|
|||
$payload['password'] = "VALID_APPLE_PASSWORD"; |
|||
|
|||
$this |
|||
->postJson('/apple/server/notifications', $payload) |
|||
->assertSuccessful(); |
|||
|
|||
$this->assertCount(1, AppleNotification::get()); |
|||
|
|||
$notification = AppleNotification::first(); |
|||
|
|||
$this->assertEquals('initial_buy', $notification->type); |
|||
$this->assertInstanceOf(AppleNotification::class, $notification); |
|||
|
|||
Queue::assertPushed(DummyJob::class); |
|||
} |
|||
|
|||
/** @test */ |
|||
public function a_request_with_an_invalid_password_wont_be_logged() |
|||
{ |
|||
$payload = include_once __DIR__ . '/__fixtures__/request.php'; |
|||
$payload['password'] = "NON_VALID_APPLE_PASSWORD"; |
|||
|
|||
$this |
|||
->postJson('/apple/server/notifications', $payload) |
|||
->assertStatus(400); |
|||
|
|||
$this->assertCount(0, AppleNotification::get()); |
|||
$this->assertNull(AppleNotification::first()); |
|||
|
|||
Queue::assertNotPushed(DummyJob::class); |
|||
} |
|||
|
|||
/** @test */ |
|||
public function a_request_with_an_invalid_payload_will_be_logged_but_jobs_will_not_be_dispatched() |
|||
{ |
|||
$payload = ['payload' => 'INVALID']; |
|||
|
|||
$this |
|||
->postJson('/apple/server/notifications', $payload) |
|||
->assertStatus(500); |
|||
|
|||
Queue::assertNotPushed(DummyJob::class); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,75 @@ |
|||
<?php |
|||
|
|||
namespace Appvise\AppStoreNotifications\Tests; |
|||
|
|||
use Appvise\AppStoreNotifications\NotificationsServiceProvider; |
|||
use Exception; |
|||
use Illuminate\Foundation\Exceptions\Handler; |
|||
use Illuminate\Contracts\Debug\ExceptionHandler; |
|||
use Orchestra\Testbench\TestCase as OrchestraTestCase; |
|||
use CreateAppleNotificationsTable; |
|||
|
|||
abstract class TestCase extends OrchestraTestCase |
|||
{ |
|||
public function setUp(): void |
|||
{ |
|||
parent::setUp(); |
|||
|
|||
$this->setUpDatabase(); |
|||
} |
|||
|
|||
/** |
|||
* Set up the environment. |
|||
* |
|||
* @param \Illuminate\Foundation\Application $app |
|||
*/ |
|||
protected function getEnvironmentSetUp($app) |
|||
{ |
|||
$app['config']->set('database.default', 'sqlite'); |
|||
$app['config']->set('database.connections.sqlite', [ |
|||
'driver' => 'sqlite', |
|||
'database' => ':memory:', |
|||
'prefix' => '', |
|||
]); |
|||
|
|||
config(['appstore-server-notifications.shared_secret' => 'test_shared_secret']); |
|||
} |
|||
|
|||
protected function setUpDatabase() |
|||
{ |
|||
include_once __DIR__.'/../database/migrations/create_apple_notifications_table.php.stub'; |
|||
|
|||
(new CreateAppleNotificationsTable())->up(); |
|||
} |
|||
|
|||
/** |
|||
* @param \Illuminate\Foundation\Application $app |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getPackageProviders($app) |
|||
{ |
|||
return [ |
|||
NotificationsServiceProvider::class, |
|||
]; |
|||
} |
|||
|
|||
protected function disableExceptionHandling() |
|||
{ |
|||
$this->app->instance(ExceptionHandler::class, new class extends Handler { |
|||
public function __construct() |
|||
{ |
|||
} |
|||
|
|||
public function report(Exception $e) |
|||
{ |
|||
} |
|||
|
|||
public function render($request, Exception $exception) |
|||
{ |
|||
throw $exception; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,126 @@ |
|||
<?php |
|||
return json_decode('{ |
|||
"environment": "Sandbox", |
|||
"notification_type": "INITIAL_BUY", |
|||
"password": "TEST_SHARED_SECRET", |
|||
"cancellation_date": "2018-03-27 07:11:12 Etc/GMT", |
|||
"cancellation_date_pst": "2018-03-27 00:11:12 America/Los_Angeles", |
|||
"cancellation_date_ms": "1522134672000", |
|||
"web_order_line_item_id": "1000000047417113", |
|||
"latest_receipt": "BASE64ENCODED_RECEIPT_INFO", |
|||
"latest_receipt_info": {}, |
|||
"latest_expired_receipt" : "BASE64ENCODED_LATEST_RECEIPT_INFO", |
|||
"latest_expired_receipt_info": { |
|||
"purchase_date_ms": "1521893342000", |
|||
"original_transaction_id": "1000000577061006", |
|||
"web_order_line_item_id": "1000000047417113", |
|||
"product_id": "PRODUCT_ID", |
|||
"purchase_date": "2018-03-24 12:09:02 Etc/GMT", |
|||
"purchase_date_pst": "2018-03-24 05:09:02 America/Los_Angeles", |
|||
"original_purchase_date": "2018-03-17 12:09:03 Etc/GMT", |
|||
"original_purchase_date_ms": "1521288543000", |
|||
"original_purchase_date_pst": "2018-03-17 05:09:03 America/Los_Angeles", |
|||
"cancellation_reason": "0", |
|||
"cancellation_date": "2018-03-27 07:11:12 Etc/GMT", |
|||
"cancellation_date_ms": "1522134672000", |
|||
"cancellation_date_pst": "2018-03-27 00:11:12 America/Los_Angeles", |
|||
"expires_date": "2019-10-09 07:43:26 Etc/GMT", |
|||
"expires_date_ms": "1570607006000", |
|||
"expires_date_formatted": "2019-03-24 12:09:02 Etc/GMT", |
|||
"expires_date_formatted_pst": "2019-03-24 05:09:02 America/Los_Angeles", |
|||
"quantity": "1", |
|||
"unique_identifier": "UNIQUE_IDENTIFIER", |
|||
"unique_vendor_identifier": "UNIQUE_VENDOR_IDENTIFIER", |
|||
"is_in_intro_offer_period": "false", |
|||
"is_trial_period": "false", |
|||
"item_id": "ITEM_ID", |
|||
"app_item_id": "APP_ITEM_ID", |
|||
"version_external_identifier": "VERSION_EXTERNAL_IDENTIFIER", |
|||
"transaction_id": "1000000577069202", |
|||
"bvrs": "2", |
|||
"bid": "com.example.app.ios" |
|||
}, |
|||
"auto_renew_status": "false", |
|||
"auto_renew_product_id": "PRODUCT_ID", |
|||
"auto_renew_status_change_date": "", |
|||
"auto_renew_status_change_date_pst": "", |
|||
"auto_renew_status_change_date_ms": "", |
|||
"pending_renewal_info": [ |
|||
{ |
|||
"auto_renew_product_id": "PRODUCT_ID", |
|||
"auto_renew_status": "1", |
|||
"expiration_intent": "", |
|||
"original_transaction_id": "1000000577061006", |
|||
"is_in_billing_retry_period": "1", |
|||
"product_id": "PRODUCT_ID", |
|||
"price_consent_status": "0", |
|||
"grace_period_expires_date": "", |
|||
"grace_period_expires_date_ms": "", |
|||
"grace_period_expires_date_pst": "" |
|||
} |
|||
] |
|||
}', true |
|||
); |
|||
|
|||
|
|||
//{ |
|||
// "environment": "Sandbox", |
|||
// "notification_type": "INITIAL_BUY", |
|||
// "password": "TEST_SHARED_SECRET", |
|||
// "cancellation_date": "2018-03-27 07:11:12 Etc/GMT", |
|||
// "cancellation_date_pst": "2018-03-27 00:11:12 America/Los_Angeles", |
|||
// "cancellation_date_ms": "1522134672000", // important for cancel |
|||
// "web_order_line_item_id": "1000000047417113", |
|||
// "latest_receipt": "BASE64ENCODED_RECEIPT_INFO", |
|||
// "latest_receipt_info": "ARRAY_WITH_RECEIPT_INFO", |
|||
// "latest_expired_receipt" : "BASE64ENCODED_LATEST_RECEIPT_INFO", |
|||
// "latest_expired_receipt_info": { |
|||
// "purchase_date_ms": "1521893342000", // important for initial buy, interactive_renewal, did_recover |
|||
// "original_transaction_id": "1000000577061006", // important for initial buy, interactive_renewal, did_change_renewal_info, cancel, did_change_renewal_status, did_fail_to_renew, did_recover, price_increase_consent |
|||
// "web_order_line_item_id": "1000000047417113", // important for initial buy, interactive_renewal |
|||
// "product_id": "PRODUCT_ID", // important for initial buy, interactive_renewal, cancel, did_change_renewal_status |
|||
// "purchase_date": "2018-03-24 12:09:02 Etc/GMT", |
|||
// "purchase_date_pst": "2018-03-24 05:09:02 America/Los_Angeles", |
|||
// "original_purchase_date": "2018-03-17 12:09:03 Etc/GMT", |
|||
// "original_purchase_date_ms": "1521288543000", |
|||
// "original_purchase_date_pst": "2018-03-17 05:09:03 America/Los_Angeles", |
|||
// "cancellation_reason": "0", |
|||
// "cancellation_date": "2018-03-27 07:11:12 Etc/GMT", |
|||
// "cancellation_date_ms": "1522134672000", |
|||
// "cancellation_date_pst": "2018-03-27 00:11:12 America/Los_Angeles", |
|||
// "expires_date": "2019-10-09 07:43:26 Etc/GMT", |
|||
// "expires_date_ms": "1570607006000", // important for did_recover, price_increase_consent |
|||
// "expires_date_formatted": "2019-03-24 12:09:02 Etc/GMT", |
|||
// "expires_date_formatted_pst": "2019-03-24 05:09:02 America/Los_Angeles", |
|||
// "quantity": "1", |
|||
// "unique_identifier": "UNIQUE_IDENTIFIER", |
|||
// "unique_vendor_identifier": "UNIQUE_VENDOR_IDENTIFIER", |
|||
// "is_in_intro_offer_period": "false", |
|||
// "is_trial_period": "false", |
|||
// "item_id": "ITEM_ID", |
|||
// "app_item_id": "APP_ITEM_ID", |
|||
// "version_external_identifier": "VERSION_EXTERNAL_IDENTIFIER", |
|||
// "transaction_id": "1000000577069202", |
|||
// "bvrs": "2", |
|||
// "bid": "com.example.ios.app" |
|||
// }, |
|||
// "auto_renew_status": "false", // important for did_change_renewal_status |
|||
// "auto_renew_product_id": "PRODUCT_ID", // important for did_change_renewal_info |
|||
// "auto_renew_status_change_date": "", |
|||
// "auto_renew_status_change_date_pst": "", |
|||
// "auto_renew_status_change_date_ms": "", // important for did_change_renewal_status |
|||
// "pending_renewal_info": [ // important for did_fail_to_renew |
|||
// { |
|||
// "auto_renew_product_id": "PRODUCT_ID", |
|||
// "auto_renew_status": "1", |
|||
// "expiration_intent": "", |
|||
// "original_transaction_id": "1000000577061006", |
|||
// "is_in_billing_retry_period": "1", |
|||
// "product_id": "PRODUCT_ID", |
|||
// "price_consent_status": "0", // important for price_increase_consent |
|||
// "grace_period_expires_date": "", |
|||
// "grace_period_expires_date_ms": "", |
|||
// "grace_period_expires_date_pst": "" |
|||
// } |
|||
// ] |
|||
// } |
Loading…
Reference in new issue