S Subster

SDK

Вы поставите нашу библиотеку для iOS или Android, опознаете покупателя, покажете экран с ценами прямо из приложения и узнаете, открыт ли доступ.

Перед началом

Шаг 1. Поставьте библиотеку

iOS. Xcode → File → Add Package Dependencies, адрес https://github.com/web2web-dev/web2app-ios-sdk.git, версия 0.8.1, продукт Web2AppSDK.

Android. Библиотека лежит в JitPack; разрешение на интернет она объявляет сама.

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}

// build.gradle.kts of the app module
dependencies {
    implementation("com.github.web2web-dev:web2app-android-sdk:0.7.2")
}

Шаг 2. Вызовите configure при запуске

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

import Web2AppSDK

// once at launch; baseUrl is a URL, not a String
Web2App.configure(projectId: "<PROJECT_ID>", baseUrl: URL(string: "https://api.subster.ai")!)
import app.web2app.sdk.Web2AppSdk

// once at launch, e.g. in Application.onCreate(); Android needs a context
// since 0.7.2 configure warms up WebView (hundreds of ms on the main thread);
// opt out with warmUpWebView = false; call WebView.setDataDirectorySuffix before configure
Web2AppSdk.configure(applicationContext, "<PROJECT_ID>", "https://api.subster.ai")

Пример в кабинете не соберётся: там import Web2App, адрес строкой на iOS и нет контекста на Android.

Шаг 3. Опознайте покупателя

Библиотека меняет код перехода на идентификатор покупателя (guid) и сохраняет его на устройстве.

Что у васiOSAndroid
первый запуск, кода нетidentify(deepLinkValue: nil)identify(onResult, onNeedEmail)
код из ссылки возврата, из письма или от трекераidentify(deepLinkValue: code)identifyWithDeepLinkValue(code)
опознать не удалось.failure(.needsEmailFallback) → requestEmailRecovery(email)onNeedEmail → requestEmailRecovery(email)
покупатель уже опознанcurrentGuid()currentGuid()
порядок проверок в identifyсохранённый → код, а без кода отпечаток устройства → почтасохранённый → Install Referrer → отпечаток устройства → почта

Как достать код из ссылки — «Возврат в приложение», шаг 3. Что стоит за каждым путём — «Как мы узнаём покупателя».

Web2App.identify(deepLinkValue: code) { result in
    switch result {
    case .success(let guid): print("identified:", guid)
    case .failure(.needsEmailFallback): showEmailField() // then Web2App.requestEmailRecovery(email) { _ in }
    case .failure(let error): print("identify failed:", error)
    }
}
Web2AppSdk.identify(
    onResult = { result -> result.onSuccess { guid -> /* identified */ } },
    onNeedEmail = { showEmailField() }, // then Web2AppSdk.requestEmailRecovery(email) { }
)

Где ошибаются чаще всего:

Шаг 4. Спросите право доступа

entitlement спрашивает право по сохранённому идентификатору — тем же открытым адресом, что в «Проверить право доступа».

Web2App.entitlement { grant in
    // iOS calls this closure on a background thread
    DispatchQueue.main.async {
        if grant?.isActive == true { unlockPremium() }
    }
}
Web2AppSdk.entitlement { grant ->
    if (grant?.isActive == true) unlockPremium()
}
Поле записиЧто в нём
isActivetrue, когда статус active
levelуровень, который проект назначил цене; без такой пары — идентификатор цены
statusactive, expired или revoked
expiresAtконец срока в ISO 8601; пусто у бессрочной записи
priceIdидентификатор цены
testModetrue у записи режима проверки (раздел «Пока включён режим проверки» ниже); поле есть с iOS 0.8.0 и Android 0.7.2, в ранних версиях его нет

Где короткий ответ подводит:

Шаг 5. Покажите экран с ценами из приложения

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

Как открытьiOSAndroidЧто вернётся
встроенное окно, по IDopenWebPaywallEmbedded(paywallId:)openWebPaywallEmbeddedById(context, paywallId)PaywallResult
встроенное окно, по адресуopenWebPaywallEmbedded(paywallURL:)openWebPaywallEmbedded(context, paywallUrl)PaywallResult
окно браузера, по IDopenWebPaywall(paywallId:)openWebPaywallById(context, paywallId)запись о доступе или nil
окно браузера, по адресуopenWebPaywall(paywallURL:)openWebPaywall(context, paywallUrl)запись о доступе или nil

Окно браузера — это SFSafariViewController и Chrome Custom Tabs. Необязательные параметры у всех четырёх: email подставится в поле почты, adaptyProfileId и revenuecatProfileId — для подписочной платформы.

Web2App.openWebPaywallEmbedded(paywallId: "<PAYWALL_ID>", email: userEmail) { result in
    switch result {
    case .paid(let grant): unlockPremium(grant)
    case .pending: recheckEntitlementLater() // paid, the record has not arrived yet
    case .notPaid: break
    case .unavailable: showError()           // the screen was not shown
    }
}
Web2AppSdk.openWebPaywallEmbeddedById(context, "<PAYWALL_ID>", email = userEmail) { result ->
    when (result) {
        is PaywallResult.Paid -> unlockPremium(result.grant)
        PaywallResult.Pending -> recheckEntitlementLater()
        PaywallResult.NotPaid -> Unit
        PaywallResult.Unavailable -> showError()
    }
}
РезультатЧто случилось
paid · Paidоплата подтверждена, запись о доступе в руках
pending · Pendingстраница сообщила об оплате, но запись за десять секунд не появилась — спросите право ещё раз через несколько секунд
notPaid · NotPaidэкран закрыли, записи нет
unavailable · Unavailableэкран не показан: нет configure или экран не нашёлся по ID. На Android с 0.7.2 — ещё и сорванный показ экрана с ценами или квиза: страница упала два раза подряд. На iOS упавшая страница загружается заново, такого исхода после показа нет. У экрана с ценами перед этим исходом библиотека до 10 секунд проверяет оплату и, если она прошла, отдаёт Paid

ID экрана копируется в кабинете: «Воронки» → вкладка «Пейволлы» → ⋮ в строке экрана → «Скопировать ID». По ID находится только экран, к которому домен привязан напрямую; экран внутри привязанной воронки не найдётся.

Оплата ложится на идентификатор библиотеки, тот, что отдаёт currentGuid(); если сохранённого нет, библиотека заводит новый. Его и передавайте своему серверу.

Как показан экранКогда библиотека начинает спрашивать правоКак долго
встроенное окнопосле закрытия окнараз в секунду, до 10 раз
окно браузера на iOSпосле закрытия окнараз в две секунды, до 30 раз
окно браузера на Androidсразу при открытиираз в две секунды, до 30 раз

На Android окно браузера не сообщает о закрытии, поэтому через минуту после открытия колбэк получит nil, даже если покупатель ещё платит. Спросите право заново, когда приложение вернулось на передний план.

Новый показ закрывает прежний: два встроенных окна одновременно не живут.

Показ без ожидания на iOS

Экран, загруженный заранее, открывается по ID сразу.

Web2App.preloadPaywalls(paywallIds: ["<PAYWALL_ID>"], email: userEmail)
// later, with the same email and profile ids
Web2App.openWebPaywallEmbedded(paywallId: "<PAYWALL_ID>", email: userEmail) { result in /* as above */ }
ВопросОтвет
что видно без предзагрузкииндикатор загрузки, пока страница грузится: на iOS с 0.8.0, на Android с 0.7.2
где работаетiOS с 0.8.0; на Android предзагрузки нет
когда зватьпосле identify, задолго до показа
когда готовая страница подойдётпоказ через openWebPaywallEmbedded(paywallId:), email и оба profile-id те же, что при предзагрузке, странице меньше часа; иначе показ идёт обычным путём, с загрузкой
покупатель ещё не опознанс 0.8.1 предзагрузка работает и так; в 0.8.0 вызов ничего не делал
после показас любым исходом библиотека загружает этот экран заново
экран больше не понадобитсяinvalidatePreloadedPaywalls(paywallIds:); все сразу, например при выходе из аккаунта, — clearPreloadedPaywalls()
памятькаждая заранее загруженная страница — отдельный процесс на десятки мегабайт; держите только те, что покажете
статистикапока экран не показан, просмотр, пиксели и замер скорости не засчитываются

События встроенного окна

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

Web2App.setFunnelEventListener { name, data in
    analytics.log(name, ["screen": data.screenIndex as Any])
}
Web2App.setFunnelEventListener(nil) // unsubscribe when the screen is gone
Web2AppSdk.setFunnelEventListener { name, data ->
    analytics.log(name, mapOf("screen" to data.screenIndex))
}
Web2AppSdk.setFunnelEventListener(null) // unsubscribe when the screen is gone
СобытиеКогдаПоля в dataЗакрывает окно
quiz_startначало квиза—нет
quiz_screen_viewпоказан экранscreenId, screenIndex, screenTotalнет
quiz_answerответ; на каждый ответ своё событиете же плюс blockId, blockTypeнет
quiz_email_submitпочта отправленаscreenIdнет
quiz_completeквиз пройден—нет
paywall_resultисход экрана с ценами—да, если оплачено
closeстраница просит закрыть окно—да

Квиз во встроенном окне

openQuizEmbedded(quizURL:) на iOS и openQuizEmbedded(context, quizUrl) на Android открывают квиз в том же окне, с теми же событиями. Результат — причина закрытия: page — страница, user — пользователь, paid — внутри окна прошла оплата; unavailable — нет configure или сорванный показ. Квиз, в отличие от экрана с ценами, доступ не проверяет: после оплаты спросите право (шаг 4).

Открытия по ID у квиза нет. Адрес по ID отдаёт GET /public/quiz-url/<quizId> — ответ { "success": true, "data": { "url": "…" } }, либо 404, если квиз не опубликован или домен привязан не к нему самому.

Пока включён режим проверки

Режим проверки включают в кабинете — как. Пока он включён, библиотека получает действующую запись с уровнем test на любой идентификатор, который знает проект.

Что библиотека делает сама, а что остаётся вам

ЗадачаКто
хранить идентификатор покупателя, обменивать на него код перехода, Install Referrer и отпечаток устройствабиблиотека
просить письмо с кодом и слать отметку первого открытиябиблиотека
дописывать к адресу экрана с ценами идентификатор, почту и профили подписочных платформбиблиотека
дожидаться права доступа после оплаты и писать «Логи SDK»библиотека
принимать Universal Link и App Link и доставать из них кодвы — «Возврат в приложение», шаг 3
регистрировать схему возврата и передавать её адреса в handleReturnURL / handleReturnUrlвы — там же, шаг 4
показывать поле почты, когда библиотека о нём проситвы
проверять доступ со своего сервера, когда за решением стоят деньгивы — «Проверить право доступа»
узнавать о продлениях, отменах и возвратахвы — «Вебхуки»
держать ключ sk_live_… только на сервере, не в приложениивы

Отладка

Дальше