08 miniapps and banners
Каталог мини-приложений и рекламных баннеров в one_loop_app.
Типы мини-приложений¶
| Тип (actionType) | Что показывает | Когда использовать |
|---|---|---|
BANNER |
Рекламный баннер с изображением (imageUrl) |
Акции, промо-кампании |
LAUNCH_JWT |
WebView с RS256 JWT-авторизацией | Встроенные сервисы платформы |
EXTERNAL_LINK |
Внешняя ссылка — открывается в браузере | Партнёрские сервисы |
Поля мини-приложения¶
Модель: onewallet_base_server/lib/src/models/miniapp.spy.yaml, таблица miniapp.
| Поле | Тип | Обязательный | Описание |
|---|---|---|---|
miniappId |
String | да | Уникальный идентификатор (slug) |
nameJson |
String | да | Локализованное название — JSON {"en":"..","th":".."} |
descriptionJson |
String? | нет | Локализованное описание — JSON |
iconUrl |
String? | нет | URL квадратной иконки |
iconBlurhash |
String? | нет | Blurhash-заглушка до загрузки иконки |
imageUrl |
String? | нет | URL полноформатного изображения (для BANNER) |
url |
String | да | Целевой URL (WebView или внешний) |
actionType |
String | да | BANNER / LAUNCH_JWT / EXTERNAL_LINK |
block |
String | да | Секция каталога; используется для сортировки |
sortOrder |
int | да | Порядок внутри секции (меньше — выше) |
active |
bool | да | false — полностью скрыт из выдачи |
targetApps |
String? | нет | JSON-массив appId ["wallet","closeloop"]; null = все |
tagsInclude |
List\<String>? | нет | Показать только если у пользователя есть все перечисленные теги |
tagsExclude |
List\<String>? | нет | Скрыть если у пользователя есть хотя бы один из тегов |
expiresAt |
DateTime? | нет | После этого момента фильтруется in-memory |
createdAt |
DateTime | да | Дата создания записи |
updatedAt |
DateTime | да | Дата последнего изменения (влияет на etag) |
Индексы¶
| Индекс | Поля | Уникальный |
|---|---|---|
miniapp_id_unique |
miniappId |
да |
miniapp_block_sort_idx |
block, sortOrder |
нет |
miniapp_expires_idx |
expiresAt |
нет |
Таргетинг¶
| Параметр | Тип | Пример | Поведение |
|---|---|---|---|
targetApps |
JSON array of String | ["wallet"] |
Виден только в указанных приложениях |
targetApps = null или [] |
— | — | Виден во всех клиентах |
tagsInclude |
List\<String> | ["nfc:enabled"] |
Виден только пользователям с всеми указанными тегами |
tagsExclude |
List\<String> | ["nfc:enabled"] |
Скрыт от пользователей с любым из указанных тегов |
expiresAt |
DateTime (UTC) | 2026-12-31T23:59:59Z |
Фильтруется in-memory после истечения |
active |
bool | false |
Исключён из любой выдачи на уровне SQL |
block = 'HIDDEN' |
String | — | Исключён из каталога (block.notEquals('HIDDEN')) |
Фильтрация выполняется в три прохода: SQL (active + block), сервер-кеш (TTL 5 мин), in-memory (expiresAt + targetApps + tagsInclude/tagsExclude).
Теги пользователя — полный справочник¶
Теги делятся на два вида: автоматические (вычисляются из полей профиля при каждом запросе) и ручные (задаются через Admin Panel в поле user_profile.tags).
Автоматические теги¶
Вычисляются сервером при каждом запросе — не требуют ручного проставления, всегда актуальны.
| Тег | Источник | Примеры |
|---|---|---|
acct:{тип} |
users.accountType |
acct:consumer, acct:merchant, acct:agent |
status:{статус} |
users.status |
status:active, status:suspended |
lang:{код} |
user_profile.languageCode |
lang:th, lang:en, lang:ru |
tier:{уровень} |
user_profile.accountTier |
tier:basic, tier:standard, tier:premium, tier:vip |
biz:{категория} |
user_profile.businessCategoryCode |
biz:retail, biz:food, biz:transport |
has:business |
user_profile.businessName |
присутствует если businessName непустой |
has:referral-agent |
user_profile.referralAgentId |
присутствует если referralAgentId задан |
nfc:enabled |
user_profile.nfcEnabled |
присутствует если NFC включён |
vat:payer |
user_profile.isVatPayer |
присутствует если плательщик НДС |
Ручные теги (user_profile.tags)¶
Задаются администратором через Admin Panel. Используются только для меток, которые нельзя вывести автоматически из существующих полей профиля.
| Тег | Сценарий |
|---|---|
beta-tester |
Ранний доступ к новым мини-приложениям |
early-access |
Участники программы раннего доступа |
partner:grab |
Партнёрские пользователи Grab |
partner:lazada |
Партнёрские пользователи Lazada |
promo:summer2026 |
Участники конкретной акции |
staff |
Сотрудники компании |
Примеры правил таргетинга¶
Только для пользователей с NFC¶
Применение: мини-приложение оплаты NFC-чипом.Только для пользователей без NFC¶
Применение: баннер с предложением включить NFC.Только для PREMIUM и VIP (через ручные теги)¶
Важно:
tagsInclude— логическое И (все теги должны присутствовать). Для условия «PREMIUM или VIP» создайте два отдельных мини-приложения с разнымиtagsInclude.
Для «только PREMIUM»:
Мини-приложение для мерчантов с налоговой отчётностью¶
Показывается только мерчантам, являющимся плательщиками НДС.Партнёрское приложение — скрыть от бета-тестеров¶
Бета-функция только для тайских PREMIUM-пользователей с NFC¶
Логика применения¶
Мини-приложение показывается, если:
(tagsInclude пуст ИЛИ все теги из tagsInclude есть у пользователя)
И
(tagsExclude пуст ИЛИ ни один тег из tagsExclude нет у пользователя)
tagsInclude |
tagsExclude |
Результат |
|---|---|---|
[] |
[] |
Видно всем |
["nfc:enabled"] |
[] |
Только с NFC |
[] |
["nfc:enabled"] |
Только без NFC |
["tier:premium"] |
["beta-tester"] |
PREMIUM-пользователи, кроме бета-тестеров |
["nfc:enabled", "tier:vip"] |
[] |
VIP с NFC |
Как работает LAUNCH_JWT¶
sequenceDiagram
participant U as Пользователь
participant App as one_loop_app
participant AC as Auth Center
participant W as WebView
U->>App: нажать на мини-приложение
App->>AC: MiniappEndpoint.launch(miniappId, appId)
AC->>AC: проверить active + targetApps
AC->>AC: подписать RS256 JWT (exp 5 мин)
AC->>App: MiniappLaunchResult {token, launchUrl}
App->>W: открыть launchUrl + Bearer token
W->>W: верифицировать JWT через /api/jwks
JWT payload¶
Токен подписывается приватным ключом RSA (jwtLaunchRsaPrivateKeyPem из passwords.yaml).
| Поле | Где в токене | Пример значения |
|---|---|---|
iss |
standard claim | auth.onewallet |
aud |
standard claim | miniapp:<miniappId> |
sub |
standard claim | "42" (userId) |
jti |
standard claim | UUID v4 |
exp |
standard claim | now + 5 минут |
id |
payload | 42 (userId, int) |
auth_id |
payload | UUID authId из Serverpod |
status |
payload | active |
profile.name |
payload | "Somchai" |
profile.phone_masked |
payload | "+66 8** *** **34" |
profile.account_tier |
payload | 0=BASIC 1=STANDARD 2=PREMIUM 3=VIP |
launch_url |
payload | URL мини-приложения |
kid |
header | значение jwtLaunchKeyId из passwords.yaml |
Публичный ключ для верификации отдаётся через GET /api/jwks (формат JWKS из jwtLaunchJwksJson).
Кэш каталога¶
Каталог кэшируется на клиенте (Hive) по значению etag.
| Механизм | Описание |
|---|---|
etag |
SHA-256 (16 символов) от miniappId:updatedAt всех записей + теги пользователя |
| Инвалидация | Любое изменение поля модели (updatedAt) или смена nfcEnabled/user_profile.tags |
modified: false |
Сервер вернёт флаг без items, если etag совпадает |
nextExpiresAt |
Ближайшее expiresAt из выборки — клиент может запланировать следующий refresh |
| Клиентская сторона | Hive box хранит список items + etag между сессиями |
Локализация¶
Поля nameJson и descriptionJson хранятся как JSON-объект с ISO 639-1 ключами:
Логика выбора локали при getCatalog(locale):
1. Значение для запрошенной локали (locale)
2. Fallback на "en"
3. Fallback на первое доступное значение в объекте