我处理过你描述的同样的问题。我有一些应用程序,我们直接从网站上出售订阅,还通过应用内购买提供订阅。
我们通过网站向网络访问者出售订阅和通过应用程序购买订阅来处理这一问题。您可以同时支持这两种订阅,但不能将应用程序用户定向到您的网站进行订阅。
首先,我将根据我们的处理方式来解决您列举的问题,然后告诉您我们做了什么。
-
要确定用户是否有帐户,您需要提供一个登录屏幕,如果他们有帐户,您很好,您只需要提供内容。如果订阅已过期,则无法隐藏应用内购买选项和/或将其指向网站进行订阅。你的应用程序将因此被拒绝。
问题2:
-
如果用户登录并已通过网站支付了帐户,则只需提供订阅内容即可。你不需要让他们通过应用程序订阅,如果他们已经订阅,并有一个有效的帐户。新用户将需要有一个选项来创建一个帐户,并通过应用程序内购买订阅。
问题3:
-
您的后端API应该跟踪用户拥有的订阅类型以及它是否有效。如果有效,则授予他们对内容的访问权限;如果无效,则应向他们提供续订/订阅流。
从苹果订阅页面(
靠近页面底部-请参阅此答案底部的链接
)
在应用程序之外购买的订阅
在你的应用程序之外获得的订户可以通过应用程序阅读或播放内容。但是,您不能在应用程序中提供允许用户在应用程序之外购买订阅的外部链接。
你需要在应用程序中处理的主要事情是:
-
为现有用户提供登录,无论是通过web还是应用程序订阅。如果他们有基于web或基于应用内购买的有效订阅,请提供内容。如果没有,请提示他们通过应用内购买进行订阅。
-
通过应用程序为新用户提供注册。通过应用程序注册的用户将使用应用程序内购买来支付订阅费用。
-
后端API应该跟踪/验证通过IAP购买的订阅。当应用程序启动时,您可以连接到您的api以使用用户的收据验证用户订阅是否仍然有效。如果有效,请提供内容,否则显示订阅续订UI。
<?php
/*
This is an overview of fields found in validated receipts
validated response fields include
- status - 0 if receipt is valid, otherwise error code
- receipt (In app purchase receipt fields)
- quantity - (the qty of items purchased)
- product_id - (the product id of the purchased item)
- transaction_id - (the transaction id for the purchased item)
- original_transaction_id - (the original transactions transaction id. All renewal receipts for auto renew subscriptions have the same value for this field)
- purchase_date - (the most recent purchase/restore date, for auto-renewing subs it's always the date the subscription was purchased or renewed, regardless of restoration)
- original_purchase_date - (the original transactions transactionDate property. For auto-renewing subscriptions its the beginning of the subscription period)
- expires_date - (only present for auto renew purchases, subscription expiration date)
- cancellation_date - (transaction cancelled by Apple support - treat as if no purchase made)
- app_item_id - (uniquely identifies the app that created the transaction, use to differentiate which app gets access)
- version_external_identifier - (uniquely identifies a revision of the application)
- web_order_line_item_id - (primary key for identifying subscription purchases)
// see receipt validation programming guide pg 22 at the bottom for this
- latest_receipt
if receipt being validated is for latest renewal, this value is the same as receipt-data (in the request)
- latest_receipt_info
value is the same as receipt (below, received in validation response) if receipt being validated is for the latest renewal
"latest_receipt_info":[
{
"quantity":"1",
"product_id":"myProductId",
"transaction_id":"transaction_id_goes_here",
"original_transaction_id":"original_id",
"purchase_date":"2015-06-19 13:08:37 Etc/GMT",
"purchase_date_ms":"1434719317000",
"purchase_date_pst":"2015-06-19 06:08:37 America/Los_Angeles",
"original_purchase_date":"2015-06-19 13:08:38 Etc/GMT",
"original_purchase_date_ms":"1434719318000",
"original_purchase_date_pst":"2015-06-19 06:08:38 America/Los_Angeles",
"expires_date":"2015-06-19 13:11:37 Etc/GMT",
"expires_date_ms":"1434719497000",
"expires_date_pst":"2015-06-19 06:11:37 America/Los_Angeles",
"web_order_line_item_id":"line_item_id_here",
"is_trial_period":"true"
},
]
- receipt (App Receipt Fields)
- bundle_id - the apps bundle id
- application_version - the apps version number
- in_app - array of in-app purchase receipts (see receipt validation programming guide p. 24 for more info)
- original_application_version - version of app that was originally purchased (in sandbox always 1.0)
- expiration_date - only for apps in volume purchase program, otherwise receipt does not expire
*/
class ReceiptValidation
{
public $receipt;
public $response_json;
public $response_array;
private $password;
private $request_data;
private $request_json;
private $live_url;
private $sand_url;
public $user;
public $db;
private $debugString;
private $latestReceipt;
public $error;
function __construct($receipt, $user, $db)
{
$this->receipt = $receipt;
$this->db = $db;
$this->user = $user;
// set apples validation urls
$this->live_url = 'https://buy.itunes.apple.com/verifyReceipt';
$this->sand_url = 'https://sandbox.itunes.apple.com/verifyReceipt';
}
public function setupReceiptRequest()
{
// setup in itc as shared secret (this value should be outside the document root)
$password = '';
$this->request_json = '{"receipt-data":"'.$this->receipt.'", "password":"'.$password.'"}';
}
/*!
Sends the receipt to Apple to verify that it's valid.
(Called when user first subscribes and inserts data into db)
*/
function validateIosReceipt($dbProductId)
{
$this->setupReceiptRequest();
$this->validateReceiptOnLive();
$this->verifyResponseStatus();
// get the array of latest receipts
$receipts = $this->response_array['latest_receipt_info'];
// get the most recent one
$this->latestReceipt = end(array_values($receipts));
$productId = $this->latestReceipt['product_id'];
$purchaseDate = $this->latestReceipt['purchase_date'];
$purchaseDateMs = $this->latestReceipt['purchase_date_ms'];
$expiresDate = $this->latestReceipt['expires_date'];
$expiresDateMs = $this->latestReceipt['expires_date_ms'];
$isTrialPeriod = $this->latestReceipt['is_trial_period'];
$transactionId = $this->latestReceipt['transaction_id'];
// get the receipt details we're interested in storing
$tableData = array(
'user_id' => $this->user->uid,
'is_active' => 1,
'product' => $dbProductId,
'product_id' => $productId,
'receipt' => $this->receipt,
'purchase_date' => $purchaseDate,
'purchase_date_ms' => $purchaseDateMs,
'transaction_id' => $transactionId,
'expires_date' => $expiresDate,
'expires_date_ms' => $expiresDateMs,
'is_trial_period' => $isTrialPeriod,
);
// save receipt details to db table (this does initial insert to database for purchase)
$saveStatus = $this->db->saveSubscription($tableData);
// return the status of our save
return $saveStatus;
}
// returns 0 (no change to report), 20 (user has admin provided bonus acct), or 30 (subscription expired)
function validateSubscriptionStatus()
{
// check if they have a bonus status from being granted a free member account
$acctTypeFetch = $this->db->fetchCurrentUserAccountTypeForUser($this->user->uid);
// only run this if the fetch was successful
if (!empty($acctTypeFetch) && $acctTypeFetch != false)
{
// get our result row
$row = $acctTypeFetch[0];
// check for validity
if (isset($row))
{
// get the account type for this user
$currentAcctType = $row['acct_type'];
// '20' is the account type flag for a user that has our promo account
if ($currentAcctType == 20)
{
// this user has a free acct provided by us, no sub needed, return 20 instead of 0 because if we mark an account as promo
// we want the users account to be updated on their device when they close and reopen the app without having to re-login.
return 20;
}
// this user is currently a subscriber, so get their receipt and make sure they're still subscribed
else if ($currentAcctType > 5 && $currentAcctType <= 15)
{
// they don't have a bonus acct & they were at one point subscribed so pull purchase data from db for user
$subscriptionData = $this->db->retrieveSubscriptionDataForUserWithID($this->user->uid);
// the user actually has purchased a subscription in the past so check if they are still subscribed
if (!empty($subscriptionData) && $subscriptionData != false)
{
// get our row of data
$subInfo = $subscriptionData[0];
// set $this->receipt with fetched receipt
$this->receipt = $subInfo['receipt'];
// setup our request data to verify with Apple
$this->setupReceiptRequest();
// validate receipt and check expires date
$this->validateReceiptOnLive();
$this->verifyResponseStatus();
# get the array of latest receipts
$receipts = $this->response_array['latest_receipt_info'];
if (!empty($receipts) && $receipts != NULL)
{
# get the most recent one
$this->latestReceipt = end(array_values($receipts));
$productId = $this->latestReceipt['product_id'];
$purchaseDate = $this->latestReceipt['purchase_date'];
$purchaseDateMs = $this->latestReceipt['purchase_date_ms'];
$expiresDate = $this->latestReceipt['expires_date'];
$expiresDateMs = $this->latestReceipt['expires_date_ms'];
$isTrialPeriod = $this->latestReceipt['is_trial_period'];
$transactionId = $this->latestReceipt['transaction_id'];
# get current time in ms
$now = time();
// check if user cancelled subscription, if they did update appropriate tables with account status
if ($now > $expiresDateMs)
{
// subscription expired, update database
$updateDB = $this->db->updateAccountSubscriptionStatusAsExpired($this->user->uid);
// return expired acct_type key
return 30;
}
}
}
}
}
}
// user never subscribed or their subscription is current
// no action needed
return 0;
}
function validateReceiptOnLive()
{
$this->response_json = $this->remote_request($this->live_url, $this->request_json);
$this->response_array = json_decode($this->response_json, true);
}
function validateReceiptOnSandbox()
{
$this->response_json = $this->remote_request($this->sand_url, $this->request_json);
$this->response_array = json_decode($this->response_json, true);
}
/*!
Checks for error 21007 or 21008, meaning that we sent it to the wrong verification server, if we sent to the wrong server it retries by sending to the other server
for verification
*/
function verifyResponseStatus()
{
if (! (isset($this->response_array['status'])))
{
// something went wrong,
// TODO: set an error and bail
return;
}
switch ($this->response_array['status'])
{
case 0:
# receipt is valid
break;
case 21000:
# App store could not read json object provided
$this->error = "App store couldn't read json.";
break;
case 21002:
# data in receipt-data was malformed or missing
$this->error = "Receipt data malformed or missing.";
break;
case 21003:
# receipt could not be authenticated
$this->error = "Receipt could not be authenticated";
break;
case 21004:
# shared secret does not match secret on file
$this->error = "Shared secret error";
break;
case 21005:
# receipt server is not currently available
$this->error = "Receipt server unavailable";
break;
case 21006:
# receipt is valid but subscription has expired
$this->error = "Subscription expired";
break;
case 21007:
# receipt is a sandbox receipt but sent to production server. Resubmit receipt verification to sandbox
$this->validateReceiptOnSandbox();
break;
case 21008:
# receipt is a production receipt but sent to the sandbox server. Resubmit receipt verification to production
$this->validateReceiptOnLive();
break;
default:
# unknown error code
break;
}
}
function remote_request($url, $data)
{
$curl_handle = curl_init($url);
if(!$curl_handle) return false;
curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl_handle, CURLOPT_POST, true);
curl_setopt($curl_handle, CURLOPT_POSTFIELDS, $data);
// curl_setopt($curl_handle, CURLOPT_SSL_VERIFYHOST, 0);
// curl_setopt($curl_handle, CURLOPT_SSL_VERIFYPEER, false);
$output = curl_exec($curl_handle);
curl_close($curl_handle);
return $output;
}
}
?>
在您的应用程序中,您可以在购买后获得收据,如下所示:
private func loadReceipt() -> Data? {
guard let url = Bundle.main.appStoreReceiptURL else {
return nil
}
do {
let data = try Data(contentsOf: url)
return data
} catch {
print("\(self) Error loading receipt data: \(error.localizedDescription)")
return nil
}
}
// get your receipt data
guard let data = loadReceipt() else {
// nil response and error
completion(nil, MyError.receiptLoadError)
return
}
// create body data object for the request
let body = [
"receipt-data": data.base64EncodedString()
]
// serialize to Data
guard let bodyData = try? JSONSerialization.data(withJSONObject: body, options: []), let url = URL(string: myServerUrl) else {
// nil response and error
completion(nil, MyError.serializationError)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = bodyData
// send request with receipt to server
let task = URLSession.shared.dataTask(with: request)....