Перейти к содержанию

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

tagsInclude: ["nfc:enabled"]
tagsExclude: []
Применение: мини-приложение оплаты NFC-чипом.

Только для пользователей без NFC

tagsInclude: []
tagsExclude: ["nfc:enabled"]
Применение: баннер с предложением включить NFC.

Только для PREMIUM и VIP (через ручные теги)

tagsInclude: []   ← нельзя выразить «или» через include

Важно: tagsInclude — логическое И (все теги должны присутствовать). Для условия «PREMIUM или VIP» создайте два отдельных мини-приложения с разными tagsInclude.

Для «только PREMIUM»:

tagsInclude: ["tier:premium"]

Мини-приложение для мерчантов с налоговой отчётностью

tagsInclude: ["acct:merchant", "vat:payer"]
Показывается только мерчантам, являющимся плательщиками НДС.

Партнёрское приложение — скрыть от бета-тестеров

tagsInclude: []
tagsExclude: ["beta-tester"]

Бета-функция только для тайских PREMIUM-пользователей с NFC

tagsInclude: ["nfc:enabled", "lang:th", "tier:premium"]
tagsExclude: []

Логика применения

Мини-приложение показывается, если:
  (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 ключами:

{
  "en": "Travel Insurance",
  "th": "ประกันการเดินทาง",
  "ru": "Страхование путешествий"
}

Логика выбора локали при getCatalog(locale): 1. Значение для запрошенной локали (locale) 2. Fallback на "en" 3. Fallback на первое доступное значение в объекте