Подключаем boosty к мобильному приложению

50 minute read

Boosty и подписки

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

Весь код доступен на странице gitflic.ru/company/getapp. Код сервиса getapp-service и код getapp-sdk-android. И если вам понравилась идея, то можете поддержать развитие проекта и подписаться на блог на boosty.

Некоторые независимые разработчики уже используют boosty как краудфандинг платформу. Но мы пойдем дальше и интегрируем boosty прям в наше приложение. Такой подход позволит:

  • Не зависеть от площадок дистрибуции
  • Гибко настраивать уровни подписок для пользователей
  • Формировать сообщество вокруг приложений

Почему не стандартные инструменты?

Сейчас сложилась неприятная ситуация на рынке мобильных приложений. Если у вас даже очень качественное приложение, вам все равно будет очень сложно пробиться через Google Play к вашим пользователям. Магазины забиты приложениями и вам придется заплатить слишком много за установку именно вашего приложения. Большинство приложений в сторах - сомнительного качества, очень много адварных приложений. И никто не торопится исправлять эту проблему. Такая ситуация выгодна для самого Google, потому что он зарабатывает на рекламе в приложениях. Google, на самом деле, не важно качество приложения, а только сколько на нем можно заработать в AdMob.

Поэтому boosty - это не только альтернативный способ реализовать подписки. Это возможность изменить подход к взаимодействию с пользователями. Вы можете продвигать блог наравне с продвижением страницы приложения. У вас появляется возможность очень тесного общения с пользователями, вам проще слышать их запросы и проблемы. C boosty вы не просто публикуете новую версию, вы растите сообщество вокруг вашего приложения.

Почему только сейчас?

Сейчас в Google Play в России нельзя ничего купить. Это послужило толчком, теперь необходимо задумываться про новые возможности и системы приема платежей. Уже есть необходимость публиковать приложения во всех доступных магазинах и/или напрямую, например, как apk для android. В некоторых альтернативных магазинах можно использовать сторонние способы приема платежей.

Apple совсем недавно разрешил использовать альтернативные системы оплаты для России. Уверен, скоро это сделает и Google. Сейчас альтернативная оплата в Google Play уже работает для некоторых стран.

Кроме того, под андроид всегда можно было распространять приложения в виде apk файла. И в таком случае мы можем творить что хотим.

И не стоит забывать про десктопные приложения и веб. Описанный подход для них тоже подойдет.

Ограничения

Самое большое ограничение - это отсутствие вменяемого публичного API у boosty. Но с этим можно жить. Я запилил небольшую библиотеку для работы с boosty на Go. Посмотреть можно тут: gitflic.ru/project/getapp/boosty

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

Юридические моменты

Чтобы работать с boosty, вам достаточно быть самозанятым. Сейчас статус самозанятого очень просто оформить через “мой налог”. Вывод денег очень простой, можно подключить карту или другой удобный способ.

Реализация

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

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

Вот так это выглядит визуально:

Бекенд

Реализацию задумки начнем с бекенд части. Нам нужно научится парсить подписчиков и подписки в нашу базу и реализовать на бекенде свой сервис со своими ручками. Сервис необходим, потому что у boosty нет своего API и придется делать надстройку, с помощью которой будем работать с приватным API boosty. А приватное API доступно только по авторизационному токену.

План такой:

  • Получаем статистику по нашему блогу
  • Сохраняем себе все подписки, которые есть на нашем блоге
  • Сохраняем всех подписчиков, которые подписались на наш блог

Этого должно быть достаточно, чтобы предоставить API для мобильного SDK, которое будет использоваться в приложении. Для работы с boosty на бекенде будет использоваться библиотека на go. Пример использования библиотеки

1import "kovardin.ru/projects/boosty"
2
3func main() {
4    b := boosty.New("blog", "token")
5}
  • blog - имя вашего блога. Будет использоваться для формирования URL в большинстве запросов
  • token - token для авторизации

Начнем с получения статистики по нашему блогу. Для этого есть вот такой метод у boosty:

1GET https://api.boosty.to/v1/blog/stat/getapp/current
2Authorization: Bearer {{token}}

Ответ выглядит так

1{
2  "hold": 0,
3  "payoutSum": 9459,
4  "income": 9468,
5  "paidCount": 4,
6  "balance": 9,
7  "followersCount": 0
8}

Кроме общей статистики, обратите внимание на paidCount - это количество платных подписчиков. Это поле нам понадобиться для получения списка всех подписчиков.

В гошной либе есть метод b.Stats() который возвращает структуру *boosty.Stats со всеми указанными выше полями.

Более подробную стату можно получить с помощью запроса на blog/getapp/stat/data

1GET https://api.boosty.to/v1/blog/getapp/stat/data?last_time=1695081599&limit=30
2Authorization: Bearer {{token}}

Этот запрос вернет большой JSON с подробной статистикой по вашему блогу.

Следующий нужный нам метод - это получение списка подписок.

1GET https://api.boosty.to/v1/blog/getapp/subscription_level/?show_free_level=true
2Authorization: Bearer {{token}}

Это вызов отдает JSON вида

 1{
 2  "currentId": null,
 3  "nextId": null,
 4  "subscriptions": [],
 5  "data": [
 6    {
 7      "id": 1091770,
 8      "price": 0,
 9      // ...
10      "currencyPrices": {
11        "USD": 0,
12        "RUB": 0
13      },
14      "ownerId": 10435460,
15      "data": [],
16      "name": "Follower",
17      "externalApps": {
18        // ...
19      },
20      "deleted": false
21    },
22    {
23      "id": 1091773,
24      "price": 300,
25      // ...
26      "currencyPrices": {
27        "USD": 3.3,
28        "RUB": 300
29      },
30      "ownerId": 10435460,
31      "externalApps": {
32        // ...
33      },
34      "deleted": false,
35      "createdAt": 1664319534,
36      "data": [],
37      "name": "Стандартная подписка"
38    },
39    {
40      "id": 1122632,
41      "price": 700,
42      // ...
43      "currencyPrices": {
44        "USD": 7.7,
45        "RUB": 700
46      },
47      "ownerId": 10435460,
48      "externalApps": {
49        // ...
50      },
51      "deleted": false,
52      "createdAt": 1665493091,
53      "data": [],
54      "name": "Расширенная подписка"
55    }
56  ]
57}

JSON я немного сократил для более простого восприятия.

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

Для получения списка уровней добавлен метод b.Subscriptions(offset, limit int). Обратите внимание, что в запросе нужно указывать сколько уровней вы хотите получить за один запрос. Результат работы этого метода - слайс []boosty.Subscription

И последний самый важный запрос - это получение списка подписчиков

1GET https://api.boosty.to/v1/blog/getapp/subscribers?sort_by=on_time&offset=0&limit=10&order=gt
2Authorization: Bearer {{token}}

Результат работы этого запроса

 1{
 2  "offset": 4,
 3  "limit": 10,
 4  "total": 2,
 5  "data": [
 6    {
 7      "email": "horechek@gmail.com",
 8      "level": {
 9        // ...
10        "name": "Тестовая подписка"
11      },
12      // ...
13      "name": "Артем Ковардин",
14      "id": 15004836,
15      "isBlackListed": false,
16      "subscribed": true
17    },
18    {
19      "email": "artem.kovardin@gmail.com",
20      "level": {
21        // ...
22        "name": "Стандартная подписка",
23      },
24      "name": "getapp.store",
25      "isBlackListed": false,
26      "id": 3684586,
27      "subscribed": true,
28    }
29  ]
30}

В этом JSON у каждого подписчика указан еще и уровень подписки. В библиотеке для получения всех подписчиков есть метод b.Subscribers(offset, limit int) который возвращает слайс с подписчиками []boosty.Subscriber.

Чтобы вытащить всех подписчиков, нужно знать сколько их. До этого мы получали статистику по блогу и теперь нам пригодиться поле paidCount. Если у вас пока не очень много подписчиков, то вы можете получить их всех просто указав значение paidCount в параметре limit и не заморачиваться с пагинацией.

Теперь мы можем определиться с нашим API и структурой данных. В сервисе getapp я реализовал три метода для работы с подписками. Первый метод - это получение информации о блоге по ид приложения:

1GET /v1/boosty/{id}/blog
2Accept: application/json
3Content-Type: application/json
  • id - идентификатор приложения в сервисе getapp

Этот запрос отдает очень простой JSON

1{
2  "id": 1,
3  "name": "getapp",
4  "title": "Getapp",
5  "url": "https://boosty.to/getapp"
6}

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

Для показа списка подписок в приложении нужна еще один запрос

1GET /v1/boosty/1/subscriptions
2Accept: application/json
3Content-Type: application/json

Этот запрос вернет JSON со списоком подписок

 1{
 2  "items": [
 3    {
 4      "id": 4,
 5      "external": 2192136,
 6      "name": "Тестовая подписка",
 7      "title": "Тестовая подписка",
 8      "blog": "Getapp",
 9      "amount": 1000,
10      "active": true
11    },
12    {
13      "id": 3,
14      "external": 1122632,
15      "name": "Расширенная подписка",
16      "title": "Расширенная подписка",
17      "blog": "Getapp",
18      "amount": 70000,
19      "active": true
20    },
21    {
22      "id": 2,
23      "external": 1091773,
24      "name": "Стандартная подписка",
25      "title": "Стандартная подписка",
26      "blog": "Getapp",
27      "amount": 30000,
28      "active": true
29    }
30  ]
31}

Два важных параметра

  • id - это внутренний id в системе getapp
  • external - это идентификатор подписки в boosty

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

1GET /v1/boosty/1/subscriber/23651380
2Accept: application/json
3Content-Type: application/json

23651380 - это идентификатор подписчика в boosty. В этом запросе будем использовать именно этот идентификатор, потому что его можно будет просто получить на веб-вью

И этот запрос вернет подробную информацию о подписчике

 1{
 2  "id": 3,
 3  "external": 23651380,
 4  "name": "Artem Kovardin",
 5  "active": true,
 6  "amount": 1000,
 7  "subscription": {
 8    "id": 4,
 9    "external": 2192136,
10    "name": "Тестовая подписка",
11    "title": "Тестовая подписка",
12    "blog": "getapp",
13    "amount": 1000,
14    "active": true
15  }
16}

Тут с подход как с подписками

  • id - это внутренний id в системе getapp
  • external - это идентификатор подписчика в boosty

В итоге у нас есть все ручки для реализации SDK.

SDK

Интегрировать оплату через boosty будем, в первую очередь, под Android. Для этого напишу библиотеку, которую можно будет использовать и для Flutter плагинов, и в Godot.

В библиотеке будет всего несколько публичных методов

  • Boosty.init(app: String) - метод, который инициализирует всю нашу библиотеку. В параметре он принимает id приложения из сервиса getapp
  • Boosty.blog(handler: BlogHandler) - этот метод возвращает информацию о блоге: название, url, идентификатор.
  • Boosty.subscriber(external: String, handler: SubscribeHandler) - метод принимает идентификатор пользователя boosty и возвращает информацию о нем с сервиса getapp
  • Boosty.subscriptions(handler: SubscriptionsHandler) - нужен для получения списка всех подписок
  • Boosty.subscribe(external: String? = null, handler: SubscribeHandler) - самый главный метод, который используется, собственно, для реализации логики подписки

Как видите, ничего сложно. Никаких запусков ракет. Для получения информации о подписке и подписчике - нам нужно получить идентификатор пользователя в boosty и тут нужен запуск веб-вью, с помощью которого получим id пользователя.

Самый интересный метод - это subscribe(). Именно тут будет запускаться веб-вью и определяться ид пользователя. В итоге, получим информацию о подписчике после закрытия диалогового окна с веб-вью.

Весь процесс совершения покупки можно разбить на 6 шагов:

  1. Начинать подписку нужно с очистки всего, что мы знаем о подписке на данные момент
1fun subscribe(external: String? = null, handler: SubscribeHandler) {
2    subscriber = null
3}

Если параметр external передан, то запустившийся диалог покажет страничку сразу с конкретной подпиской. А если оставить это поле пустым, то отобразится весь блог. Оба варианта могу быть полезны в разных ситуациях. Например, отправлять пользователя на главную страницу блога имеет смысла если вы хотите познакомить пользователя с тем, что вы предлагаете взамен на подписку. Если в приложении и так все подробно описано и рассказано, то имеет смысл сразу отправить пользователя на страницу с конкретной подпиской, не заставляя его выбирать

  1. Получаем информацию о блоге с помощью метода blog()
 1subscriber = null
 2blog(object : BlogHandler {
 3    override fun onFailure(e: Throwable) {
 4        handler.onFailure(e)
 5    }
 6
 7    override fun onSuccess(resp: Blog) {
 8        // TODO
 9    }
10})
  1. Запускаем веб-вью и указываем ссылку на блог или сразу на экран с нужной подпиской. Ссылку на блог мы получили на шаге 1 resp.url. А чтобы получить ссылку на конкретную подписку - надо добавить несколько параметров
1val u = if (external.isNullOrEmpty()) {
2    resp.url
3} else {
4    "${resp.url}/purchase/$external"
5}

И теперь открываем диалог с нужной ссылкой

 1val dialog = Dialog(context = context, url = u)
 2dialog.setOnDismissListener {
 3    // TODO
 4}
 5dialog.client = object : WebViewClient() {
 6    override fun shouldInterceptRequest(w: WebView?, request: WebResourceRequest?): WebResourceResponse? {
 7        val resp = super.shouldInterceptRequest(w, request)
 8        return resp
 9    }
10
11    override fun shouldOverrideUrlLoading(w: WebView, u: String): Boolean {
12        w.loadUrl(u)
13        return true
14    }
15
16    override fun onPageFinished(w: WebView?, u: String?) {
17        // TODO
18    }
19}
20dialog.open()
  1. Нагло забираем куки, чтоб найти ид подписчика. И дополнительно делаем небольшое изменение на странички boosty, чтобы скрыть попап с предложением установки приложения.
 1override fun onPageFinished(w: WebView?, u: String?) {
 2    // убираем попап про скачивание приложения
 3    w?.evaluateJavascript("sessionStorage.setItem(\"preventShowAppBanner\", \"true\");document.querySelectorAll('[ class^=\"NativeAppBanner_root_\" ]')[0].style.display=\"none\";", null);
 4
 5    // нагло забираем куки чтоб найти ид подписчика
 6    val cookies = CookieManager.getInstance().getCookie(u) ?: return
 7
 8    val user = User.parse(cookies)
 9
10    // TODO
11})

Тут нужно разобраться с классом User. Этот класс представляет пользователя boosty. В методе parse мы достаем из кук информацию о последнем логине пользователя. В информации о последнем логине также указывается ссылка на аватарку пользователя и из этой ссылки можно узнать ид пользователя на boosty.

Для лучшего понимания работы с кукой, приведу тут весь класс User целиком

 1package ru.kovardin.boosty
 2
 3import android.net.Uri
 4import android.util.Log
 5import com.google.gson.Gson
 6import java.net.HttpCookie
 7import java.net.URLDecoder
 8
 9const val UserCookieKey = "last_acc"
10
11class User(val name: String, val avatarUrl: String, val provider: String) {
12    fun external(): String {
13        val u = Uri.parse(avatarUrl)
14        if (u.pathSegments.count() > 1) {
15            return u.pathSegments[1]
16        }
17
18        return ""
19    }
20
21    companion object {
22        fun parse(cookie: String): User?  {
23            for (cc in cookie.split(";")) {
24                val cookies = HttpCookie.parse(cc)
25                for (c in cookies) {
26                    if (c.name == UserCookieKey) {
27                        val auth = URLDecoder.decode(c.value, "UTF-8")
28                        var resp: User
29
30                        try {
31                            resp = Gson().fromJson(auth, User::class.java)
32                        } catch (e: Exception) {
33                            Log.e("User", e.message.orEmpty())
34                            return null
35                        }
36
37                        return resp
38                    }
39                }
40            }
41
42            return null
43        }
44    }
45}
  1. По полученному ид забираем информацию о подписчике с бека если она там есть. Информацию о подписчике сохраняем в параметре Boosty.subscriber
1subscriber(user?.external() ?: "", object : SubscribeHandler {
2    override fun onFailure(e: Throwable) {
3        // skip
4    }
5
6    override fun onSuccess(resp: Subscriber) {
7        subscriber = resp
8    }
9})
  1. А сейчас нужно вернуться к моменту, где инициализировался диалог и добавить реализацию в коллбек setOnDismissListener
1dialog.setOnDismissListener {
2    // 5. при закрытии окна отправляем onSuccess если есть subscriber
3    if (subscriber == null) {
4        handler.onFailure(Exception("error on subscribe"))
5        return@setOnDismissListener
6    }
7
8    handler.onSuccess(subscriber!!)
9}

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

Описанной реализации достаточно для использования в приложении. Вы можете посмотреть реализацию андроид части тут (gitflic.ru/project/getapp/getapp-sdk-android)[https://gitflic.ru/project/getapp/getapp-sdk-android] а бековой части тут gitflic.ru/project/getapp/getapp-service. Но предупреждаю, что все это пока на уровне концепции и использовать можно на свой страх и риск.

Что дальше

Мы все привыкли к стандартным подпискам в AppStore и Google Play. Но подписки через boosty дают намного больше возможностей. Например, пользователи могут выбирать уровни и в зависимости от этого получать разный доступ к приложению.

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

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

В самом начале статьи я писал, что Apple разрешили использовать кастомные платежные системы в приложениях в России. И вот тут использование boosty для подписок будет работать вообще отлично. Логика та же самая - использование веб-вью и сервиса getapp.

В следующих статьях опишу подробней как использовать SDK в реальном приложении. А пока вы можете подписаться на мой блог на boosty