Как использовать подписки iOS с существующим веб-сервисом подписки? - PullRequest
0 голосов
/ 26 сентября 2018

Несмотря на то, что существует много информации о том, как реализовывать подписки iOS в целом, я не нашел информации о , как использовать их с существующим веб-сервисом подписки.

Предположим,у нас есть веб-сайт газеты, где пользователи могут создать учетную запись для доступа к платному контенту:

  • Платежи OneTime разблокируют доступ к контенту на фиксированный период, например, 3 месяца
  • Подписка делает то же самоено будет автоматически продлен, если не будет отменен

Платежи и подписки OneTime продаются и управляются на сервере.

Приложение:

Конечно, было бы не проблема разрешить пользователям доступ к платному контенту из нашего приложения для iOS, но при этом управлять покупками и подписками только на веб-сайте.Однако мы все знаем, что Apple почти обанкротилась и поэтому отчаянно нуждается во всех деньгах, которые они могут получить от разработчиков.Из-за этого простое решение рекламировать подписку из приложения и продавать ее с веб-сайта строго запрещено. Мы должны удалить все ссылки на покупки на сайте из приложения и использовать вместо этого покупки в приложении.

Как мы можем это сделать?

Проблема 1- Имеет ли пользователь учетную запись?

Предположим, что приложение iOS предлагает некоторые базовые функции бесплатно, которые не требуют подключения к веб-службе.Предложение покупок в приложении для покупки подписок на веб-службу имеет смысл только тогда, когда веб-служба используется и у пользователя есть учетная запись.

Разрешено ли проверять, есть ли у пользователя учетная запись веб-службы, и отправлять их на веб-страницу для ее создания?Разрешено скрывать / деактивировать опцию покупки в приложении до тех пор, пока пользователь не войдет в веб-сервис?

Проблема 2. Активная подписка уже есть?

Что, если пользователь подключил приложение iOS к веб-службе, и у учетной записи пользователя уже есть активная подписка, которая была приобретена с веб-сайта?

Не имеет смысла предлагать пользователю подписку на покупку в приложении, поскольку он будет платить дважды за одну и ту же услугу.Можно ли отключить покупку In-App в этом случае?

Проблема 3 - уже есть активный пакет OneTime?

Что если пользователь подключилсяприложение iOS для веб-службы и учетной записи пользователя уже имеет активный пакет OneTime, купленный на веб-сайте?

Как и прежде, было бы бессмысленно предлагать пользователю подписку на покупки в приложении.Конечно, веб-служба может добавить период подписки в конец пакета OneTime, но подписка для iOS начнется немедленно.Таким образом, может случиться так, что между периодом подписки iOS и подпиской на веб-сервис будет существенное смещение.

Единственный способ избежать этого - предлагать подписку на iOS только при отсутствии активной подписки на веб-приложение или пакета OneTime.

Это разрешено?

...

Суть в том, что существует множество потенциальных проблем и конфликтов между подписками iOS и существующими подписками веб-служб.Есть ли какая-либо информация о том, как их решить и решить?

1 Ответ

0 голосов
/ 27 сентября 2018

Я имел дело с той же проблемой, которую вы описываете.У меня было несколько приложений, в которых мы продавали подписки прямо с веб-сайта, а также предлагали подписки через покупку приложений.

Мы справились с этим, продавая подписки через веб-сайт веб-посетителям и подписывая покупки через приложение.Вы можете поддерживать обе подписки, но не можете направить пользователей своего приложения на свой веб-сайт для подписки.

Сначала я расскажу о перечисленных вами проблемах на основе того, как мы их обработали, а затем расскажу, что мы сделали.

Задача 1:

  • Чтобы определить, есть ли у пользователя учетная запись, вам нужно будет предоставить экран входа в систему, если у него есть учетная запись, вы хороши, и выпросто нужно предоставить контент.Если срок действия подписки истек, вы не сможете спрятаться в опциях покупки приложения и / или направить их на веб-сайт для подписки.Ваше приложение будет отклонено.

Проблема 2:

  • Если пользователь входит в систему и уже имеет аккаунт, оплаченный через веб-сайтвам просто нужно предоставить подписку на контент.Вам не нужно, чтобы они подписывались через приложение, если они уже подписаны и имеют действующий аккаунт.Новые пользователи должны будут иметь возможность создать учетную запись и подписаться через нее при покупке приложения.

Проблема 3:

  • Ваш внутренний APIдолжен отслеживать тип подписки, которую имеет пользователь, и если она действительна.Если они действительны, им предоставляется доступ к контенту, в противном случае они должны быть представлены с потоком продления / подписки.

Со страницы подписки Apple ( в нижней части страницы -см. ссылку внизу этого ответа )

Подписки, приобретенные за пределами приложения Подписчики, приобретенные за пределами вашего приложения, могут читать или воспроизводить контент через приложение.Однако вы не можете предоставлять внешние ссылки в вашем приложении, которые позволяют пользователям приобретать подписки вне приложения.

Основные вещи, которые вам нужно обработать в приложении:

  • Предоставьте логин существующим пользователям, независимо от того, подписаны они через Интернет или через приложение.Если у них есть действующая подписка, основанная либо на веб-платформе, либо на покупке приложения, предоставьте контент.Если нет, попросите их подписаться через приложение при покупке.
  • Предоставьте регистрацию новым пользователям через приложение.Пользователи, регистрирующиеся через приложение, будут использовать при покупке приложения для оплаты подписок.
  • Внутренний API должен отслеживать / проверять подписки, приобретенные через IAP.Когда приложение запущено, вы можете подключиться к своему API, чтобы убедиться, что подписка пользователя все еще действительна с помощью их квитанции.Если действительный контент предоставлен, в противном случае покажите пользовательский интерфейс продления подписки.

Если вы решите предлагать подписку на приложения, вам нужно будет подтвердить квитанцию ​​на серверах 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)....

Кроме того, вот несколько ссылок на документацию, которые могут оказаться полезными:

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...