Miniapps
MiniappEndpoint.getCatalog()¶
Возвращает отфильтрованный и отсортированный каталог мини-приложений с etag-кэшем.
Этапы обработки:
- Сервер-кеш (
localPrio, TTL 5 мин): сырой списокactive=true AND block != 'HIDDEN'кешируется на инстансе. DB-скан выполняется только при промахе. - In-memory фильтр:
expiresAt == null || expiresAt > now→targetAppsматч поappId→tagsInclude/tagsExcludeматч по тегам пользователя - Сортировка:
blockASC, затемsortOrderASC - 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 не из targetApps → NotAuthorizedException.
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 включает отсортированный список тегов пользователя:
При изменении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:
- Первый запрос:
getCatalog(locale, etag: null, appId)→ получитьetag+items - Сохранить etag в Hive
- Последующие запросы:
getCatalog(locale, etag: savedEtag, appId) - Если
modified == false— отобразить кэшированный список - Если
modified == true— обновить Hive + UI
nextExpiresAt в ответе позволяет клиенту запланировать принудительную инвалидацию кэша к моменту истечения срока самого «скорого» мини-аппа.