如何将iOS订阅与现有的订阅网络服务一起使用?

时间:2018-09-26 16:15:48

标签: ios in-app-purchase subscription storekit auto-renewing

虽然目前有很多有关如何实现iOS订阅的信息,但我没有找到有关如何在现有订阅Web服务中使用它们的信息。

假设我们正在运行一家报纸网站,用户可以在其中创建一个帐户来访问付费内容:

  • 一次性付款将在一段时间内解锁内容访问权限,例如3个月
  • 订阅的功能相同,但如果未取消,则会自动续订

OneTime付款和订阅都在服务器上出售和管理。

应用程序:

当然,让用户从我们的iOS应用程序中访问付费内容同时仍然仅管理网站上的购买和订阅是没有问题的。但是我们都知道,苹果公司几乎已经破产了,因此迫切需要从开发者那里得到的所有钱。因此,严格禁止从应用程序内部发布订阅并从网站出售订阅的简单解决方案。 我们必须从该应用中删除所有指向网站购买的链接,而要使用应用内购买。

我们该怎么做?

问题1-用户拥有一个帐户吗?

让我们假设iOS应用免费提供了一些基本功能,这些功能不需要与Web服务的任何连接。仅当使用Web服务且用户拥有帐户时,提供应用内购买来购买Web服务的订阅才有意义。

是否可以检查用户是否具有Web服务帐户并将其发送到网页以创建一个?是否可以隐藏/停用应用内购买选项,直到用户登录到Web服务为止?

问题2-已经有有效的订阅了吗?

如果用户已将iOS应用连接到Web服务,并且用户帐户已经有从网站购买的有效订阅,该怎么办?

向用户提供应用内购买订阅是没有意义的,因为他会为相同的服务支付两次费用。在这种情况下可以停用应用内购买功能吗?

问题3-已经有一个活动的OneTime软件包吗?

如果用户已将iOS应用连接到Web服务,并且用户帐户已经从网站购买了有效的OneTime包,该怎么办?

像以前一样,向用户提供应用内购买订阅不会有太大的意义。当然,Web服务可以将订阅期添加到OneTime包的末尾,但是iOS订阅将立即开始。因此,可能会发生iOS订阅和Web服务订阅之间存在重大偏移的情况。

避免这种情况的唯一方法是,在没有活动的Web应用程序订阅或OneTime程序包的情况下,仅提供iOS订阅。

可以吗?

...

最重要的是,iOS订阅与现有Web服务订阅之间存在许多潜在的问题和冲突。是否有有关如何解决和解决这些问题的信息?

1 个答案:

答案 0 :(得分:1)

我已经处理了您正在描述的相同问题。我有一些应用程序,我们可以直接从网站上出售订阅,也可以通过应用程序内购买来提供订阅。

我们通过以下方式解决了这一问题:通过网站向网络访问者出售订阅,并通过应用出售应用内购买订阅。您可以同时支持这两种订阅,但是不能将您的应用程序用户引导到您的网站进行订阅。

首先,我将根据我们的处理方式来解决您列举的问题,然后告诉您我们做了什么。

问题1:

  • 要确定用户是否拥有一个帐户,您需要提供一个登录屏幕,如果他们拥有一个您良好的帐户,则只需要提供内容即可。如果订阅已过期,您将无法隐藏应用购买选项和/或将其定向到网站进行订阅。为此,您的应用将被拒绝。

问题2:

  • 如果用户登录并且已经拥有通过网站付费的帐户,则只需提供订阅内容。如果他们已经订阅并且拥有有效的帐户,则无需让他们通过应用程序订阅。不过,新用户需要选择创建帐户并通过应用内购买进行订阅。

问题3:

  • 您的后端API应该跟踪用户拥有的订阅类型以及该订阅类型是否有效。如果有效,则将授予他们访问内容的权限,否则,将向他们显示续订/订阅流程。

从Apple订阅页面(在页面底部附近-参见此答案底部的链接

  

在应用之外购买的订阅   在您的应用之外获得的订阅者可以通过该应用阅读或播放内容。但是,您可能无法在应用程序中提供允许用户在应用程序外部购买订阅的外部链接。

您需要在应用程序中处理的主要事情是:

  • 提供登录到现有用户的权限,无论是通过Web还是应用程序订阅的。如果他们具有基于Web或基于应用程序购买的有效订阅,请提供内容。如果没有,提示他们通过应用程序内购买进行订阅。
  • 通过该应用为新用户提供注册。通过该应用进行注册的用户将在应用内购买中支付订阅费用。
  • 后端API应该跟踪/验证通过IAP购买的订阅。启动应用程序后,您可以连接到api以使用用户的收据来验证用户订阅仍然有效。如果有效,请提供内容,否则请显示您的订阅续订UI。

如果您决定提供应用程序内订阅,则需要使用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;
    }
}

?>

在您的应用中,您可以像这样购买后获得收据:

雨燕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)....

此外,这里还有一些可能对您有用的文档链接: