- iOS/Objective-C 元类和类别
- objective-c - -1001 错误,当 NSURLSession 通过 httpproxy 和/etc/hosts
- java - 使用网络类获取 url 地址
- ios - 推送通知中不播放声音
虽然有很多关于如何实现 iOS 订阅的信息,但我没有找到关于如何将它们与现有订阅网络服务一起使用的信息。
假设我们正在运营一个报纸网站,用户可以在其中创建一个帐户来访问付费内容:
一次性付款和订阅均在服务器上销售和管理。
应用:
当然,让用户从我们的 iOS 应用程序中访问付费内容,同时仍然只在网站上管理购买和订阅是没有问题的。然而我们都知道,Apple 几乎破产了,因此迫切需要他们能从开发商那里得到的所有钱。因此,严禁使用在应用程序内宣传订阅并从网站销售的简单解决方案。 我们必须从应用中删除所有指向网站购买的链接,并改为使用应用内购买。
我们如何做到这一点?
问题 1 - 用户有帐户吗?
让我们假设 iOS 应用程序免费提供一些基本功能,不需要任何网络服务连接。只有在使用网络服务且用户拥有帐户时,提供应用内购买以购买网络服务的订阅才有意义。
是否允许检查用户是否有网络服务帐户并将其发送到网页以创建一个?允许隐藏/停用应用内购买选项直到用户登录到网络服务?
问题 2 - 是否已有有效订阅?
如果用户已将 iOS 应用程序连接到网络服务并且用户帐户已经有从网站购买的有效订阅怎么办?
向用户提供应用内购买订阅是没有意义的,因为他会为相同的服务支付两次。在这种情况下可以停用应用内购买吗?
问题 3 - 是否已经有一个事件的 OneTime 包?
如果用户已将 iOS 应用程序连接到 Web 服务并且用户帐户已经从网站购买了有效的 OneTime 包怎么办?
和以前一样,向用户提供应用内购买订阅没有多大意义。当然,Web 服务可以将订阅期添加到 OneTime 包的末尾,但 iOS 订阅将立即开始。因此,iOS 订阅期和 Web 服务订阅期之间可能存在明显的偏移。
避免这种情况的唯一方法是在没有事件的网络应用程序订阅或 OneTime 包时仅提供 iOS 订阅。
这是允许的吗?
...
归根结底,iOS 订阅与现有网络服务订阅之间存在很多潜在问题和冲突。有没有关于如何解决和解决这些问题的信息?
最佳答案
我已经处理过您所描述的相同问题。我有一些应用程序,我们直接从网站销售订阅,也通过应用内购买提供订阅。
我们通过网站向网络访问者销售订阅以及通过应用程序内购订阅来解决这个问题。您可以同时支持这两种订阅,但不能将您的应用用户引导至您的网站进行订阅。
首先,我将根据我们的处理方式解决您列举的问题,然后告诉您我们做了什么。
问题 1:
问题 2:
问题 3:
来自 Apple 订阅页面(靠近页面底部 - 请参阅此答案底部的链接)
Subscriptions Purchased Outside of an App Subscribers who were acquired outside of your app can read or play content through the app. However, you may not provide external links in your app that allow users to purchase subscriptions outside of the app.
您需要在应用中处理的主要事情是:
如果您决定提供应用内订阅,您将需要使用 Apple 的服务器(服务器端以获得最佳安全性)验证收据,以验证订阅是否有效并提供内容。
以下是我用于服务器端收据验证的 php 脚本。您可能会发现它很有用,或者能够根据您的用例对其进行调整。
<?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;
}
}
?>
在您的应用中,您可以像这样在购买后获得收据:
swift 4
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)....
另外,这里有一些您可能会觉得有用的文档链接:
关于ios - 如何将 iOS 订阅与现有的订阅网络服务一起使用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52522162/
我在网上搜索但没有找到任何合适的文章解释如何使用 javascript 使用 WCF 服务,尤其是 WebScriptEndpoint。 任何人都可以对此给出任何指导吗? 谢谢 最佳答案 这是一篇关于
我正在编写一个将运行 Linux 命令的 C 程序,例如: cat/etc/passwd | grep 列表 |剪切-c 1-5 我没有任何结果 *这里 parent 等待第一个 child (chi
所以我正在尝试处理文件上传,然后将该文件作为二进制文件存储到数据库中。在我存储它之后,我尝试在给定的 URL 上提供文件。我似乎找不到适合这里的方法。我需要使用数据库,因为我使用 Google 应用引
我正在尝试制作一个宏,将下面的公式添加到单元格中,然后将其拖到整个列中并在 H 列中复制相同的公式 我想在 F 和 H 列中输入公式的数据 Range("F1").formula = "=IF(ISE
问题类似于this one ,但我想使用 OperatorPrecedenceParser 解析带有函数应用程序的表达式在 FParsec . 这是我的 AST: type Expression =
我想通过使用 sequelize 和 node.js 将这个查询更改为代码取决于在哪里 select COUNT(gender) as genderCount from customers where
我正在使用GNU bash,版本5.0.3(1)-发行版(x86_64-pc-linux-gnu),我想知道为什么简单的赋值语句会出现语法错误: #/bin/bash var1=/tmp
这里,为什么我的代码在 IE 中不起作用。我的代码适用于所有浏览器。没有问题。但是当我在 IE 上运行我的项目时,它发现错误。 而且我的 jquery 类和 insertadjacentHTMl 也不
我正在尝试更改标签的innerHTML。我无权访问该表单,因此无法编辑 HTML。标签具有的唯一标识符是“for”属性。 这是输入和标签的结构:
我有一个页面,我可以在其中返回用户帖子,可以使用一些 jquery 代码对这些帖子进行即时评论,在发布新评论后,我在帖子下插入新评论以及删除 按钮。问题是 Delete 按钮在新插入的元素上不起作用,
我有一个大约有 20 列的“管道分隔”文件。我只想使用 sha1sum 散列第一列,它是一个数字,如帐号,并按原样返回其余列。 使用 awk 或 sed 执行此操作的最佳方法是什么? Accounti
我需要将以下内容插入到我的表中...我的用户表有五列 id、用户名、密码、名称、条目。 (我还没有提交任何东西到条目中,我稍后会使用 php 来做)但由于某种原因我不断收到这个错误:#1054 - U
所以我试图有一个输入字段,我可以在其中输入任何字符,但然后将输入的值小写,删除任何非字母数字字符,留下“。”而不是空格。 例如,如果我输入: 地球的 70% 是水,-!*#$^^ & 30% 土地 输
我正在尝试做一些我认为非常简单的事情,但出于某种原因我没有得到想要的结果?我是 javascript 的新手,但对 java 有经验,所以我相信我没有使用某种正确的规则。 这是一个获取输入值、检查选择
我想使用 angularjs 从 mysql 数据库加载数据。 这就是应用程序的工作原理;用户登录,他们的用户名存储在 cookie 中。该用户名显示在主页上 我想获取这个值并通过 angularjs
我正在使用 autoLayout,我想在 UITableViewCell 上放置一个 UIlabel,它应该始终位于单元格的右侧和右侧的中心。 这就是我想要实现的目标 所以在这里你可以看到我正在谈论的
我需要与 MySql 等效的 elasticsearch 查询。我的 sql 查询: SELECT DISTINCT t.product_id AS id FROM tbl_sup_price t
我正在实现代码以使用 JSON。 func setup() { if let flickrURL = NSURL(string: "https://api.flickr.com/
我尝试使用for循环声明变量,然后测试cols和rols是否相同。如果是,它将运行递归函数。但是,我在 javascript 中执行 do 时遇到问题。有人可以帮忙吗? 现在,在比较 col.1 和
我举了一个我正在处理的问题的简短示例。 HTML代码: 1 2 3 CSS 代码: .BB a:hover{ color: #000; } .BB > li:after {
我是一名优秀的程序员,十分优秀!