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

Miniapps

MiniappEndpoint.getCatalog()

Возвращает отфильтрованный и отсортированный каталог мини-приложений с etag-кэшем.

Этапы обработки:

  1. Сервер-кеш (localPrio, TTL 5 мин): сырой список active=true AND block != 'HIDDEN' кешируется на инстансе. DB-скан выполняется только при промахе.
  2. In-memory фильтр: expiresAt == null || expiresAt > nowtargetApps матч по appIdtagsInclude/tagsExclude матч по тегам пользователя
  3. Сортировка: block ASC, затем sortOrder ASC
  4. Etag: sha256(miniappId:updatedAt.ms,...|tag1,tag2,...) → первые 16 символов hex
Параметр Тип Описание
locale String ISO 639-1 ('th', 'en', 'ru'); fallback 'en' → первый доступный
etag String? Etag из предыдущего вызова; совпадение → modified: false
appId String Идентификатор клиента: 'wallet', 'closeloop', 'merchant'

Etag инвалидируется при изменении любого поля записи (updatedAt) или при изменении тегов пользователя (смена nfcEnabled, user_profile.tags).

Ответ MiniappCatalogResult: - modified: false — каталог не изменился - modified: true, etag, items[], nextExpiresAt? — обновлённый список

MiniappEndpoint.launch()

sequenceDiagram
  App->>MiniappEndpoint: launch(miniappId, appId)
  MiniappEndpoint->>DB: найти miniapp WHERE miniappId=? AND active=true
  MiniappEndpoint->>MiniappEndpoint: проверить targetApps.contains(appId)
  MiniappEndpoint->>DB: UserProfile (name, phoneMasked, accountTier)
  MiniappEndpoint->>LaunchJwtService: sign(userId, authId, status, name,\n phoneMasked, accountTier, launchUrl)
  LaunchJwtService->>MiniappEndpoint: RS256 JWT (TTL 5 мин)
  MiniappEndpoint->>App: MiniappLaunchResult{token, launchUrl}
  App->>WebView: открыть launchUrl с Bearer token

targetApps: null или пустой JSON-массив → видно во всех клиентах. Передача appId не из targetAppsNotAuthorizedException.

Launch JWT payload

Поле Значение Описание
iss auth.onewallet Издатель
aud miniapp:{miniappId} Аудитория — конкретный мини-апп
sub userId (строка) Субъект
jti UUIDv4 Уникальный ID токена (предотвращает replay)
exp now + 5 мин Время истечения
id int Внутренний userId Auth Center
auth_id String UUID из Serverpod auth (authUserId)
status String Статус пользователя (active, kyc_pending, …)
profile.name String user_profile.firstName
profile.phone_masked String Маскированный телефон
profile.account_tier int BASIC=0, STANDARD=1, PREMIUM=2, VIP=3
launch_url String URL мини-приложения из БД
kid (header) jwtLaunchKeyId из passwords.yaml ID ключа для JWKS lookup

Алгоритм подписи: RS256. Ключ: jwtLaunchRsaPrivateKeyPem из passwords.yaml.

Типы action

actionType Поведение Ключевые поля
BANNER Показывает imageUrl в карточке каталога; без перехода imageUrl, iconUrl
LAUNCH_JWT Открывает url в InAppWebView с Bearer launch-JWT url
EXTERNAL_LINK Открывает url в браузере без токена url

Таргетинг

По приложению (targetApps)

Поле targetApps в модели Miniapp — JSON-массив строк.

Поле Тип Пример
targetApps String? (JSON array) '["wallet","closeloop"]'
Пустой / null Мини-апп виден во всех клиентах
Конкретные значения wallet / closeloop / merchant Ограничивает видимость

Дополнительный каталог-фильтр: block != 'HIDDEN' — скрывает записи без удаления из БД.

По тегам пользователя (tagsInclude / tagsExclude)

Персонализация каталога без новых DB-полей. Семантика идентична pm.fee_rule / pm.limit_rule — оба поля хранятся как List<String> в модели Miniapp.

Поле модели Тип Поведение
tagsInclude List<String>? null/пусто → все проходят. Непусто → все теги должны присутствовать (include.every(userTags.contains))
tagsExclude List<String>? null/пусто → все проходят. Непусто → ни один тег не должен присутствовать (!exclude.any(userTags.contains))

Оба условия применяются одновременно (AND).

Построение тегов пользователя (_buildUserTags)

Set<String> _buildUserTags(UserProfile? profile) {
  if (profile == null) return {};
  return {
    if (profile.nfcEnabled) 'nfc:enabled',  // единственный авто-тег
    ...?profile.tags,                         // ручные метки из Admin Panel
  };
}

Автоматические теги — вычисляются при каждом запросе, дублирования данных в profile.tags не требуется:

Тег Источник Пример
acct:{accountType} users.accountType acct:consumer, acct:merchant, acct:agent
status:{status} users.status status:active, status:suspended
lang:{languageCode} user_profile.languageCode lang:th, lang:en, lang:ru
tier:{accountTier} user_profile.accountTier tier:basic, tier:premium, tier:vip
biz:{categoryCode} user_profile.businessCategoryCode biz:retail, biz:food
has:business user_profile.businessName присутствует если businessName непустой
has:referral-agent user_profile.referralAgentId присутствует если referralAgentId задан
nfc:enabled user_profile.nfcEnabled присутствует если == true
vat:payer user_profile.isVatPayer присутствует если == true

Ручные теги (user_profile.tags) — только для меток, которые нельзя вывести из схемы:

Примеры Назначение
beta-tester, early-access Feature flags, программы раннего доступа
partner:grab, partner:lazada Партнёрские программы
promo:summer2026 Временные акции
staff Сотрудники компании

Примеры правил

// Только с NFC
tagsInclude: ["nfc:enabled"],  tagsExclude: []

// Только без NFC (баннер «включи NFC»)
tagsInclude: [],  tagsExclude: ["nfc:enabled"]

// PREMIUM-мерчанты с VAT
tagsInclude: ["acct:merchant", "tier:premium", "vat:payer"]

// VIP с NFC, тайский интерфейс
tagsInclude: ["nfc:enabled", "lang:th", "tier:vip"]

// Партнёры Grab, кроме бета-тестеров
tagsInclude: ["partner:grab"],  tagsExclude: ["beta-tester"]

// Видно всем
tagsInclude: [],  tagsExclude: []

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

ETag и теги

ETag включает отсортированный список тегов пользователя:

sha256("id1:ts1,id2:ts2,...|lang:th,nfc:enabled,tier:premium")[:16]
При изменении nfcEnabled или user_profile.tags ETag меняется → клиент получит обновлённый каталог на следующем запросе.

JwksRoute — /jwks.json

Публичный endpoint для верификации launch-JWT мини-приложениями.

Параметр Значение
URL GET /jwks.json
Кэш cache-control: public, max-age=86400 (24 часа)
Содержимое JSON Web Key Set из jwtLaunchJwksJson в passwords.yaml
kid Мини-апп выбирает ключ по kid из заголовка JWT для поддержки ротации

Сценарий верификации в мини-приложении: 1. Получить JWT из Authorization: Bearer <token> 2. Прочитать kid из заголовка токена 3. Запросить GET /jwks.json (кэш 24 ч) 4. Найти ключ с совпадающим kid, верифицировать подпись RS256 5. Проверить aud == miniapp:{miniappId}, iss == auth.onewallet, exp

Если jwtLaunchJwksJson не задан в passwords.yaml — endpoint вернёт {"keys": []}.

Hive cache на клиенте (etag-паттерн)

Клиентское приложение (one_loop_app) кэширует каталог через etag:

  1. Первый запрос: getCatalog(locale, etag: null, appId) → получить etag + items
  2. Сохранить etag в Hive
  3. Последующие запросы: getCatalog(locale, etag: savedEtag, appId)
  4. Если modified == false — отобразить кэшированный список
  5. Если modified == true — обновить Hive + UI

nextExpiresAt в ответе позволяет клиенту запланировать принудительную инвалидацию кэша к моменту истечения срока самого «скорого» мини-аппа.