iTunes In-App Purchases со стороны сервера

    Платежи через iTunes фактические лидеры по монетизации контента, предоставляемого мобильными приложениями. В одном из известных мне приложений доход от них в 3 раза превышает доход от Google Play пользователей при том, что посещаемость последних в 1.5 раза выше. Таким образом, с одного пользователя iTunes можно получить вплоть до 5 раз больше денег, чем с одного пользователя Google Play. Данный аргумент достаточен для интеграции платежей iTunes в мобильные приложения.

    В данной статье описываются некоторые особенности верификации платежей iTunes (в т.ч. и подписок) с серверной стороны, которые, как мне показалось, не достаточно освещены в существующих статьях.



    В соответствии с руководством разработчика предлагается две схемы верификации платежных транзакций: простая, при которой подтверждение транзакции происходит в результате взаимодействия мобильного приложения и App Store, и сложная. Во втором случае вводится дополнительный этап подтверждения с собственного сервера посредством обращения к сервису iTunes Connect. Факт успешного подтверждения платежной транзакции через iTunes Connect считается достаточным для верификации платежа.
    К минусам простой верификации можно отнести подорванное доверие. К плюсам сложной относятся удобство работы с подписками, возможность начисления мирских благ и хранения перечня продуктов на стороне сервера. Последние два пункта особенно актуальны, когда приходится ждать обновления приложения в App Store неделю. А может и несколько недель, если вдруг решите порадовать пользователей соблазнительным продуктом в предверии иноверного рождества. О безопасности я даже не говорю — всё достаточно наглядно на следующем графике:



    Так в системе мониторинга платежных запросов абстрактного приложения могут выглядеть вполне рядовые сутки. Синим цветом представлено общее количество запросов на верификацию платежа. Зеленым — запросы, которые реально прошли через App Store. А красным — вредоносные запросы. Страшно представить, какую упущенную выгоду может получить приложение, если будет игнорировать серверное подтверждение платежа. Процентое отношение данных из графика представлено в следующей таблице:

    Особенность запроса Процент
    Неподтверждаемые. Фальшивые платежи, состоящие из данных, похожих на корректные, но, возможно, в них поле какое отсутствует, число строкой представлено или присутсвует ещё какая-нибудь отличительная особенность, никак не позволяющая верифицировать платеж 0.7%
    Повторы. Запросы со стороны клиента с верифицированным платежем, но присланные повторно через какое-то время 1%
    Платежи крекеров (типа, iAP Cracker и т.п.). Посылают на верификацию платежи, сформулированные для подтверждения ими же самими 9.3%
    Поддельные. Верифицируемые через iTunes платежи других приложений 79%
    Подтвержденные. Реально честные покупки. Их цифры сходятся с цифрами покупок через аккаунт 10%


    На самом деле, большинство вредоносных запросов можно определить собственными силами без траты траффика на обращение к сервису верификации. Платеж iTunes предсталвляется т.н. рецепт. Рецепт — это кодированный в base64 JSON-объект данных платежной транзакции. Для верификации платежа или подписки через сервис App Store нужно передать их рецепт, который сообщает клиентское приложение. В ответ получите статус рецепта и некоторые данные платежа.

    Рассмотрим корректный рецепт (здесь и далее данные корректных рецептов слегка изменены):

    $ php -r "var_dump(base64_decode('Re4LRece1PT='));"
    string(2453) "{
    	"signature" = "8iN4rY5iGNaTUrE==";
    	"purchase-info" = "PuRCh45e1nf0RM4tIoN==";
    	"pod" = "22";
    	"signing-status" = "0";
    }"
    $ php -r "var_dump(base64_decode('PuRCh45e1nf0RM4tIoN=='));"
    string(784) "{
    	"original-purchase-date-pst" = "2013-02-18 10:05:51 America/Los_Angeles";
    	"purchase-date-ms" = "1361210751012";
    	"unique-identifier" = "aun1que1dent1f1er";
    	"original-transaction-id" = "1234567890";
    	"bvrs" = "220";
    	"app-item-id" = "123";
    	"transaction-id" = "1234567890";
    	"quantity" = "1";
    	"original-purchase-date-ms" = "1361210751012";
    	"unique-vendor-identifier" = "VEND0R-1DENT1F1ER";
    	"item-id" = "456";
    	"version-external-identifier" = "789";
    	"product-id" = "com.example.application.product.1";
    	"purchase-date" = "2013-02-18 18:05:51 Etc/GMT";
    	"original-purchase-date" = "2013-02-18 18:05:51 Etc/GMT";
    	"bid" = "com.example.application";
    	"purchase-date-pst" = "2013-02-18 10:05:51 America/Los_Angeles";
    }"
    


    Рецепт состоит из данных покупки, подписи и пары служебных полей. Подпись бинарна и закодирована base64. Данные покупки также закодированы и представляют собой JSON-объект с множеством полей. Наиболее интересными считаю два поля: product-id — идентификатор приобретаемого продукта и bid — идентификатор приложения.

    Лидеры выборки вредоносных запросов — поддельные запросы — выглядят примерно так:

    $ php -r "var_dump(base64_decode('CHuZH0iRECE1pt=='));"
    string(2281) "{
    	"signature" = "8iN4rY5iGNaTUrE==";
    	"purchase-info" = "4n0THeRPuRCh45e1nf0RM4tIoN==";
    	"pod" = "17";
    	"signing-status" = "0";
    }"
    $ php -r "var_dump(base64_decode('4n0THeRPuRCh45e1nf0RM4tIoN=='));"
    string(656) "{
    	"original-purchase-date-pst" = "2012-07-12 05:54:35 America/Los_Angeles";
    	"purchase-date-ms" = "1342097675882";
    	"original-transaction-id" = "170000029449420";
    	"bvrs" = "1.4";
    	"app-item-id" = "450542233";
    	"transaction-id" = "170000029449420";
    	"quantity" = "1";
    	"original-purchase-date-ms" = "1342097675882";
    	"item-id" = "534185042";
    	"version-external-identifier" = "9051236";
    	"product-id" = "com.zeptolab.ctrbonus.superpower1";
    	"purchase-date" = "2012-07-12 12:54:35 Etc/GMT";
    	"original-purchase-date" = "2012-07-12 12:54:35 Etc/GMT";
    	"bid" = "com.zeptolab.ctrexperiments";
    	"purchase-date-pst" = "2012-07-12 05:54:35 America/Los_Angeles";
    }"
    


    Вполне приличный рецепт. Только не от нашего приложения. Если выполнить обращение к iTunes Connect, получим подтверждение данного платежа:

    $ wget 'https://buy.itunes.apple.com/verifyReceipt' -q --post-data='{"receipt-data":"CHuZH0iRECE1pt=="}' -O -
    {"receipt":{"original_purchase_date_pst":"2012-07-12 05:54:35 America/Los_Angeles", "purchase_date_ms":"1342097675882", "original_transaction_id":"170000029449420", "original_purchase_date_ms":"1342097675882", "app_item_id":"450542233", "transaction_id":"170000029449420", "quantity":"1", "bvrs":"1.4", "version_external_identifier":"9051236", "bid":"com.zeptolab.ctrexperiments", "product_id":"com.zeptolab.ctrbonus.superpower1", "purchase_date":"2012-07-12 12:54:35 Etc/GMT", "purchase_date_pst":"2012-07-12 05:54:35 America/Los_Angeles", "original_purchase_d
    


    В принципе, могли бы не проверять. Можно съэкономить 80% траффика к iTunes путем сравнения product-id и bid с допустимыми в нашем приложении ещё на стадии получения рецепта от клиентского приложения.

    Рецепты, создаваемые крекерами довольно-таки примитивны: Y29tLnVydXMuaWFwLjk2NjU3Mjkw. Дешифруем, получаем com.urus.iap.96657290. Очевидно, что здесь ни о какой структуре рецепта даже речи не идет — ни подписи, ни данных покупки. Подобные рецепты можно смело отвергать. iTunes на такой рецепт вернет ошибку 21002.

    Если в случае получения рецептов, созданных крекерами можно верифицировать рецепт как своими силами, так и с помощью сервиса iTunes, то дупликаты можно обнаружить только на своей стороне. Достаточно хранить все идентификаторы транзакций, по которым были начислены блага, и проверять, не было ли уже такого идентификатора среди прошедших. Согласно документации все идентификаторы транзакций уникально определяют платеж.

    Cамое малое зло из выборки — неподтверждаемые рецепты. Ниже представлен пример одного:
    $ php -r "var_dump(base64_decode('P0dDe1NyRECE1pt=='));"
    string(613) "{"signing-status"="0";"purchase-info"="P0dDe1N0e1NF0==";"pid"="143";"signature"="1POdP1sD4jEe5t=";}"
    $ php -r "var_dump(base64_decode('P0dDe1N0e1NF0=='));"
    string(388) "{"unique-identifier"="an0theru1que1dent1f1er";"purchase-date"="2012-02-18 19:23:27 Etc/GMT";"original-transaction-id"="0123456789";"quantity"="1";"original-purchase-date"="2012-02-18 19:23:27 Etc/GMT";"bvrs"="123";"product-id"="com.example.application.product.1";"item-id"="456";"transaction-id"="0123456789";"bid"="com.example.application";}"
    


    По сравнению с корректным рецептом, в данном случае замента экономия на пробелах, но это не повод отвергать рецепт — ведь его составляет и кодирует клиентское приложение. А так рецепт выглядит корректно: есть правильные идентификаторы продукта и приложения, правдоподобные данные платежа, подпись. Нужно посылать запрос в iTunes (хорошо, что таких запросов всего 0.7% от общего числа и 7% от числа полезных запросов). iTunes ответит кодом 21002.

    На картинке ниже представлен алгоритм верификации рецептов, полученных от клиентского приложения, на стороне собственного сервера:



    Непосредственно для верификации через iTunes предлагаю для использования небольшую и удобную библиотеку. Данная библиотека позволяет верифицировать в том числе и обновляемые подписки. Запросить информацию по подписке можно тем же запросом верификации, указав при инициализации клиента секретный пароль.

    $AppStore = new \AppStore\Client\AppStoreClient();
    $AppStore->setPassword('secret shared password')
        ->setSandbox((bool) mt_rand(0,1));
    $Status = $AppStore->verifyReceipt('5t4TUs==');
    


    iTunes вернет нам данные ответа в следующем виде

    object(AppStore\Client\Response\RenewableStatus)#7 (4) {
      ["latestReceipt":"AppStore\Client\Response\RenewableStatus":private]=>
      string(3460) "5t4TUs=="
      ["LatestReceiptInfo":"AppStore\Client\Response\RenewableStatus":private]=>
      object(AppStore\Client\Response\RenewableReceipt)#8 (11) {
        ["expiresDate":"AppStore\Client\Response\RenewableReceipt":private]=>
        string(13) "1363547483000"
        ["quantity":"AppStore\Client\Response\Receipt":private]=>
        int(1)
        ["productId":"AppStore\Client\Response\Receipt":private]=>
        string(35) "com.example.application.product.2"
        ["transactionId":"AppStore\Client\Response\Receipt":private]=>
        string(15) "0987654321"
        ["purchaseDate":"AppStore\Client\Response\Receipt":private]=>
        string(27) "2013-02-18 20:11:23 Etc/GMT"
        ["originalTransactionId":"AppStore\Client\Response\Receipt":private]=>
        string(15) "9078563412"
        ["originalPurchaseDate":"AppStore\Client\Response\Receipt":private]=>
        string(27) "2013-01-18 20:11:25 Etc/GMT"
        ["appItemId":"AppStore\Client\Response\Receipt":private]=>
        string(9) "456"
        ["versionExternalIdentifier":"AppStore\Client\Response\Receipt":private]=>
        string(0) ""
        ["bid":"AppStore\Client\Response\Receipt":private]=>
        string(19) "com.example.application"
        ["bvrs":"AppStore\Client\Response\Receipt":private]=>
        string(3) "123"
      }
      ["status":"AppStore\Client\Response\Status":private]=>
      int(0)
      ["Receipt":"AppStore\Client\Response\Status":private]=>
      object(AppStore\Client\Response\RenewableReceipt)#9 (11) {
        ["expiresDate":"AppStore\Client\Response\RenewableReceipt":private]=>
        string(13) "1363547483000"
        ["quantity":"AppStore\Client\Response\Receipt":private]=>
        int(1)
        ["productId":"AppStore\Client\Response\Receipt":private]=>
        string(35) "com.example.application.product.2"
        ["transactionId":"AppStore\Client\Response\Receipt":private]=>
        string(15) "0987654321"
        ["purchaseDate":"AppStore\Client\Response\Receipt":private]=>
        string(27) "2013-02-18 20:11:23 Etc/GMT"
        ["originalTransactionId":"AppStore\Client\Response\Receipt":private]=>
        string(15) "9078563412"
        ["originalPurchaseDate":"AppStore\Client\Response\Receipt":private]=>
        string(27) "2013-01-18 20:11:25 Etc/GMT"
        ["appItemId":"AppStore\Client\Response\Receipt":private]=>
        string(9) "456"
        ["versionExternalIdentifier":"AppStore\Client\Response\Receipt":private]=>
        string(0) ""
        ["bid":"AppStore\Client\Response\Receipt":private]=>
        string(19) "com.example.application"
        ["bvrs":"AppStore\Client\Response\Receipt":private]=>
        string(3) "123"
      }
    }
    


    В отличие от подписок Google Play, iTunes создает новый рецепт подписки на каждый период оплаты. Примерно за сутки до начала следующего платежного периода, iTunes пытается снять деньги со счета пользователя, хотя я видел жалобу, что деньги за продление подписки были списаны за 48 часов до начала нового платежного периода. Если попытка ещё не проводилась и пока не прошла успешно данные, представленные в latest_receipt совпадают с данными исходного рецепта, как в примере выше. В случае успешного продления подписки, данные автоматической покупки будут представлены в поле latest_receipt_info, закодированный рецепт в поле latest_receipt

    object(AppStore\Client\Response\RenewableStatus)#7 (4) {
      ["latestReceipt":"AppStore\Client\Response\RenewableStatus":private]=>
      string(3460) "ReNEW481E5t4TUs=="
      ["LatestReceiptInfo":"AppStore\Client\Response\RenewableStatus":private]=>
      object(AppStore\Client\Response\RenewableReceipt)#8 (11) {
        ["expiresDate":"AppStore\Client\Response\RenewableReceipt":private]=>
        string(13) "1363547483000"
        ["quantity":"AppStore\Client\Response\Receipt":private]=>
        int(1)
        ["productId":"AppStore\Client\Response\Receipt":private]=>
        string(35) "com.example.application.product.2"
        ["transactionId":"AppStore\Client\Response\Receipt":private]=>
        string(15) "0987654321"
        ["purchaseDate":"AppStore\Client\Response\Receipt":private]=>
        string(27) "2013-02-18 20:11:23 Etc/GMT"
        ["originalTransactionId":"AppStore\Client\Response\Receipt":private]=>
        string(15) "9078563412"
        ["originalPurchaseDate":"AppStore\Client\Response\Receipt":private]=>
        string(27) "2013-01-18 20:11:25 Etc/GMT"
        ["appItemId":"AppStore\Client\Response\Receipt":private]=>
        string(9) "456"
        ["versionExternalIdentifier":"AppStore\Client\Response\Receipt":private]=>
        string(0) ""
        ["bid":"AppStore\Client\Response\Receipt":private]=>
        string(19) "com.example.application"
        ["bvrs":"AppStore\Client\Response\Receipt":private]=>
        string(3) "123"
      }
      ["status":"AppStore\Client\Response\Status":private]=>
      int(0)
      ["Receipt":"AppStore\Client\Response\Status":private]=>
      object(AppStore\Client\Response\RenewableReceipt)#9 (11) {
        ["expiresDate":"AppStore\Client\Response\RenewableReceipt":private]=>
        string(13) "1361131883894"
        ["quantity":"AppStore\Client\Response\Receipt":private]=>
        int(1)
        ["productId":"AppStore\Client\Response\Receipt":private]=>
        string(35) "com.example.application.product.2"
        ["transactionId":"AppStore\Client\Response\Receipt":private]=>
        string(15) "0987654312"
        ["purchaseDate":"AppStore\Client\Response\Receipt":private]=>
        string(27) "2013-01-18 20:11:23 Etc/GMT"
        ["originalTransactionId":"AppStore\Client\Response\Receipt":private]=>
        string(15) "9078563412"
        ["originalPurchaseDate":"AppStore\Client\Response\Receipt":private]=>
        string(27) "2013-01-18 20:11:25 Etc/GMT"
        ["appItemId":"AppStore\Client\Response\Receipt":private]=>
        string(9) "456"
        ["versionExternalIdentifier":"AppStore\Client\Response\Receipt":private]=>
        string(0) ""
        ["bid":"AppStore\Client\Response\Receipt":private]=>
        string(19) "com.example.application"
        ["bvrs":"AppStore\Client\Response\Receipt":private]=>
        string(3) "123"
      }
    }
    


    В случае, если продлить подписку не представилось возможным, возвращается статус ответа 21006

    $AppStore = new \AppStore\Client\AppStoreClient();
    $AppStore->setPassword('secret shared password')
        ->setSandbox((bool) mt_rand(0,1));
    try {
        $Status = $AppStore->verifyReceipt('ExP1ReD5t4TUs==');
    } catch (\AppStore\Client\Response\ExpiredSubscriptionException $ex) {
        var_dump($ex->getStatus());
    }
    


    object(AppStore\Client\Response\RenewableStatus)#7 (4) {
      ["latestReceipt":"AppStore\Client\Response\RenewableStatus":private]=>
      string(0) ""
      ["LatestReceiptInfo":"AppStore\Client\Response\RenewableStatus":private]=>
      NULL
      ["status":"AppStore\Client\Response\Status":private]=>
      int(21006)
      ["Receipt":"AppStore\Client\Response\Status":private]=>
      object(AppStore\Client\Response\RenewableReceipt)#8 (11) {
        ["expiresDate":"AppStore\Client\Response\RenewableReceipt":private]=>
        string(13) "1361208738953"
        ["quantity":"AppStore\Client\Response\Receipt":private]=>
        int(1)
        ["productId":"AppStore\Client\Response\Receipt":private]=>
        string(35) "com.example.application.product.2"
        ["transactionId":"AppStore\Client\Response\Receipt":private]=>
        string(15) "2143658709"
        ["purchaseDate":"AppStore\Client\Response\Receipt":private]=>
        string(27) "2013-01-18 17:32:18 Etc/GMT"
        ["originalTransactionId":"AppStore\Client\Response\Receipt":private]=>
        string(15) "2143658709"
        ["originalPurchaseDate":"AppStore\Client\Response\Receipt":private]=>
        string(27) "2013-01-18 17:32:19 Etc/GMT"
        ["appItemId":"AppStore\Client\Response\Receipt":private]=>
        string(9) "456"
        ["versionExternalIdentifier":"AppStore\Client\Response\Receipt":private]=>
        string(0) ""
        ["bid":"AppStore\Client\Response\Receipt":private]=>
        string(19) "com.example.application"
        ["bvrs":"AppStore\Client\Response\Receipt":private]=>
        string(3) "123"
      }
    }
    


    Предлагаю следующую схему обработки подписок iTunes на стороне сервера:



    Описание:

    • buy — покупка подписки на стороне клиента
    • verify — верификация данных подписки на стороне сервера по алгоритму, предложенному выше
    • queue — очередь данных верифицированных подписок
    • periodical verification — периодическая проверка подписок. Если подписка была продлена, записываем обновленный рецепт обратно в очередь для последующих проверок


    По моим данным ~60% подписок iTunes продлевается. Для подписок Google Play эта величина составляет ~40%. А подавляющим большинством случаев невозможности продления подписки являются случаи отсутсвия денежных средств на счетах пользователей
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 33
    • 0
      РЕЦЕПТ? ААААААААААААААААаааааааа *убежал, бешено вращая глазами*
      • +1
        <оффтоп>
        Вот это да! Я тоже это заметил!
        Рецепт, рецепт, рецепт… сделайте меня это развидеть! Теперь я всегда буду называть «receipt» рецептом :')
        </оффтоп>
        А за статью спасибо автору! Довольно интересная :)
        Да и ситуация с «рецептом» интересная :) вроде бы абсолютно неправильный перевод, и в то же время здесь что-то есть. Что-то умиляющее, то, почему не хочется писать «исправьте!» :)
    • –3
      «Страшно представить, какую упущенную выгоду может получить приложение, если будет игнорировать серверное подтверждение платежа.»

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

      Но все ровно, по-моему, серверная верификация нужна.
      • +2
        Упущенная выгода в том, что вы товар отдали, а денег не получили
        • –1
          Простите, какой товар? Пользователь уже купил/скачал приложение, а свистоперделки и монетки — не товар.
          • 0
            Я, допустим, продаю дополнительные фичи, использование которых идет через мой сервер и нагружает его. Больше юзеров => больше нагрузка => больше я плачу.
            • 0
              Ну, это издержки на дистрибьюшн, да.
              • 0
                Издержки надо покрывать доходами
                • –2
                  Для этого есть Apple, которая раздает ваше приложение желающим. (имею ввиду для раздачи фич)
                  • 0
                    Пользователь платить за толику счастья, которую ему приносит фича. Каждая проданная фича нагружает сервер, за который плачу я. А Apple получает свой процент с каждой проданной фичи за доставку
            • 0
              Очередной номер журнала, например. Другой цифровой именно что контент.
        • –14
          Не знаю как для кого, но когда речь заходит об iTunes, хочется бежать всеравно куда. Более непонятной, нелогичной, тяжелой и тормозной программы я еще не видел. iTunes вываливает ошибки xxxx, причем рекомендации всегда одни и теже: или переустановите iTunes на более новую версию, а если не помогло — переустановите Windows.
          • +3
            Причем тут Windows, если речь об in-app покупках в iOS-приложениях?
            • 0
              Предлагаю вам переустановить себе руки.
              • –3
                видимо Вы их себе уже переустановили. Google -> запрос -> itunes errors => 9 миллионов страниц
                • 0
                  Выпал в осадок с показателя.
            • 0
              А как проверять валидность подписи (той, что signature в receipt'е)? На первый взгляд это простой base64, но он декодится во что-то невообразимое.
              • 0
                Подпись двоичная и закодирована в base64. Через iTunes можно проверить рецепт (извините) целиком. Как проверить, ну или создать подпись я не знаю
                • +1
                  Окей, значит картинку в посте можно поправить. Все равно пост классный, несмотря на косячный русский местами. Сподвиг меня дописать iap-верификацию в своем приложении. Пишите еще :).
                • +1
                  Как формулировать подпись описано в посте у ZonD80
              • +1
                А платежи какой страны у вас фигурируют в статистике?
                Это я к тому, что в Японии, например, процент receipt'ов, которые не прошли валидацию всегда лежит ниже 1% (по нашей статистике). И с ними тоже не всё так ясно, большинство ошибок 21002 приходят из-за проблем на стороне сервера валидации Apple (проверено повторной проверкой одного и того же receipt'а). А примерно месяц назад это вообще превратилось в катастрофу, Apple стал возвращать 21002 на каждый третий receipt, хотя все они были валидные. Пришлось как всегда извиняться перед клиентами и временно (на 2 недели!) выключить проверку на сервере, а Apple, опять же, как всегда, вышел из воды сухим и пушистым…
                • 0
                  Бывшие советы, страны развитой демократии, Турция, Эмираты — основные страны. Результаты верификаций логгируются и при жалобе пользователя, мы перепроверяем платеж. Често скажу — не сталкивался с описанной вами ситуацией. Бывает, возникают нереальные таймауты на выполнении запросов к App Store, но не могу однозначно сказать, проблема на стороне серверов валидации Apple или где-то по сети между нами
                • 0
                  Жаль, у нас почти есть все, даже подпись. Только вот придется подстраиваться индивидуально под API ваших личных серверов.
                  • 0
                    А никто не знает стороннего сервиса для всех этих заморочек с In-App Purchases?

                    Вокруг iOS уже выросло множество стартапов и сервисов, как грибов. Многие очень популярны и облегчают разработку.
                    Для аналитики есть AppAnnie, Flurry, AppFigures, и др.
                    Для push уведомлений есть UrbanAirship и пр.
                    TestFlight, Hockey App для бета-тестирования.

                    Почему нет подобного сервися для встроенных покупок и всего что с ними связано?
                    Зарегистрировался, добавил данные о своем приложении, о встроенных покупках, получил секретные ключи, скачал SDK, вставил необходимые пару строк кода и все, веривикация покупок проходит через сервер, который не ты сам поднимал, настраивал и тратил время. На этом же сервисе предложить услуги по учету расходуемых покупок (consumables), подписок и т.д.

                    Я, например, слабо предстваляю как я стал бы реализовывать игру с внутриигровой валютой. Это же все нужно учитывать, хранить всю информацию на своем собственном серваке, быть уверенным, что юзер не потеряет свои покупки, отслеживать как эта внутриигровая валюта тратится, учитывать то, что юзер может использовать несколько устройств, и т.д. Или если юзер запустил приложение офлайн, потатил внутиигровую валюту, нужно же закешировать все эти изменения, синхронизировать их с базой на сервере, когда приложение будет онлайн. Это ж дофига работы. Как по мне вокруг этого дела можно спокойно поднять сервис.
                    • 0
                      Есть Urban Airship.
                      Я пользуюсь его API. Они берут по 10центов за каждое скачивание IAP.
                      IAP сервис они закрывают в июне, так что мне придется нанимать кого-то на написание скоего IAP сервера…
                      • 0
                        Я как раз занимаюсь разработкой такого сервиса. Планирую запустить в мае-июне этого года. Вот ссылка на него если интересно: in-app-purchase-server.com/
                        • 0
                          Супер, записался! Только перед запуском обязательно английский проверьте, ошибок там навалом.
                          • 0
                            Да, такая проблема есть :) Обязательно проверю.
                      • –3
                        вся правда о AppStore :)
                        lurkmore.so/images/0/00/Apple_store.jpg
                        • 0
                          А айтюнз часом не планирет развоорачивать свою платежную систему для оплаты услуг? Вот к примеру человек оплачивает какието услуги сервиса через инапы, но есть у сервиса и браузерный клиент — можно ли проводить аналогичные транзакции через него?
                          • 0
                            Хороший пример начального уровня на Swift (хотя это не важно, важнее php код) http://www.brianjcoleman.com/tutorial-receipt-validation-in-swift/

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