Подключаем boosty к мобильному приложению
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 приложения из сервиса getappBoosty.blog(handler: BlogHandler)
- этот метод возвращает информацию о блоге: название, url, идентификатор.Boosty.subscriber(external: String, handler: SubscribeHandler)
- метод принимает идентификатор пользователя boosty и возвращает информацию о нем с сервиса getappBoosty.subscriptions(handler: SubscriptionsHandler)
- нужен для получения списка всех подписокBoosty.subscribe(external: String? = null, handler: SubscribeHandler)
- самый главный метод, который используется, собственно, для реализации логики подписки
Как видите, ничего сложно. Никаких запусков ракет. Для получения информации о подписке и подписчике - нам нужно получить идентификатор пользователя в boosty и тут нужен запуск веб-вью, с помощью которого получим id пользователя.
Самый интересный метод - это subscribe()
. Именно тут будет запускаться веб-вью и определяться ид пользователя. В итоге, получим информацию о подписчике после закрытия диалогового окна с веб-вью.
Весь процесс совершения покупки можно разбить на 6 шагов:
- Начинать подписку нужно с очистки всего, что мы знаем о подписке на данные момент
1fun subscribe(external: String? = null, handler: SubscribeHandler) {
2 subscriber = null
3}
Если параметр external
передан, то запустившийся диалог покажет страничку сразу с конкретной подпиской. А если оставить это поле пустым, то отобразится весь блог. Оба варианта могу быть полезны в разных ситуациях. Например, отправлять пользователя на главную страницу блога имеет смысла если вы хотите познакомить пользователя с тем, что вы предлагаете взамен на подписку. Если в приложении и так все подробно описано и рассказано, то имеет смысл сразу отправить пользователя на страницу с конкретной подпиской, не заставляя его выбирать
- Получаем информацию о блоге с помощью метода
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
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()
- Нагло забираем куки, чтоб найти ид подписчика. И дополнительно делаем небольшое изменение на странички 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}
- По полученному ид забираем информацию о подписчике с бека если она там есть. Информацию о подписчике сохраняем в параметре
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})
- А сейчас нужно вернуться к моменту, где инициализировался диалог и добавить реализацию в коллбек
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