Встраиваемые UI модули (далее просто модули) - это отдельные элементы интерфейса, страницы или даже разделы, которые CASHOFF предоставляет в виде готового решения для встраивания в интерфейс приложения.
Модули взаимодействуют с сервером CASHOFF напрямую, визуализируя полученные от него данные (графики, отчеты) и предоставляя возможность пользователю с ними работать (добавлять профили, сканировать чеки и т.д.).
Модули являются готовой альтернативой самостоятельной реализации UI в приложении. В рамках интеграции CASHOFF так же выполняет брендирование интерфейса под дизайн приложения.
Технически модули построены на web-технологиях и поэтому для работы требуют браузер либо браузерный компонент webview (интеграция в приложение, в т.ч. и мобильное). Каждый модуль загружается в изолированный html-элемент <iframe> (далее фрейм), внутри которого он работает в своеобразной "песочнице", без возможности повлиять на приложение. Модули грузятся напрямую с серверов CASHOFF, поэтому не могут работать без доступа в интернет. С другой стороны это гарантирует, что клиент работает с самой последней версией модуля, а процесс обновления модуля не требует никаких изменений в приложении - всё происходит автоматически.
Общий алгоритм работы с модулями
Для показа модуля необходимы две ключевые вещи: сессия пользователя и загруженная в браузер библиотека libco.js.
Сессия необходима для авторизации пользователя внутри фрейма. Она определяет по данным какого пользователя будет открыт модуль. Способы выпуска сессии будут описаны в разделе ниже.
Библиотека libco.js - это программный модуль на языке JavaScript, который организует в браузере весь процесс работы с модулями CASHOFF. Данная библиотека расположена по постоянному адресу https://cashoff.ru/static/build/libco.js и должна быть подключена напрямую с сервера CASHOFF. Это обеспечит её актуальность и поддержку всех имеющихся модулей.
Данная ссылка для боевой версии, в случае работы с тестовыми стендами домен меняется на домен тестового стенда. Модуль libco.js привязан к стенду, с которого загружен - по этому, к примеру, нельзя использовать libco.js с боя на тесте.
Алгоритм работы с модулем состоит из следующих шагов:
- на сервере выпустить пользователю сессию
- передать сессию html-странице внутри браузера, в которую будет добавляться модуль
- загрузить на страницу libco.js
- с помощью API libco.js создать объект сессии
- с помощью объекта сессии создать <iframe> с модулем и добавить его на страницу
- по окончанию работы с модулем удалить его со страницы
Шаг 1 выполняется при начале работы клиента с модулями CASHOFF и повторяется по завершению времени жизни сессии.
Шаги 2, 3, 4 выполняются один раз, но их необходимо повторять при перезагрузке html-страницы.
Шаги 5, 6 выполняются для каждого модуля отдельно.
Выпуск сессии
Предусмотрено 2 механизма выпуска сессии:
- выпуск через серверное API. В API есть методы по добавлению/удалению/просмотру пользовательских сессий. Подробнее о таких сессиях и их выпуске написано в разделе Пользователи.
- через технологию JWT (JSON Web Token). Такие сессии выпускаются самостоятельно сервером приложения без обращений к CASHOFF. Как их выпускать описано разделом ниже.
Преимуществом первого варианта будет возможность удалить сессию до завершения её срока действия. Так же с помощью этих сессий можно с фронта делать запросы к API от имени пользователя.
В свою очередь с помощью JWT можно выпускать сессии в любое время без обращения к серверу CASHOFF, что позволяет сделать управление сессиями на стороне сервера приложения и ускорить время загрузки модуля. Однако JWT нельзя "отозвать", а так же его нельзя на данный момент использовать для выполнения запросов по API от имени пользователя. Дополнительно в JWT доступна авторегистрация - создание пользователя в cashoff без предварительного API запроса POST /users/add.
Выпуск сессии через JWT
CASHOFF использует стандартную реализацию JWT (RFC 7519) с рядом дополнительных требований. Алгоритм подписи должен использоваться HS256, в качестве ключа подписи выступает PrivateKey (тот же, что и для серверного взаимодействия).
В полезной нагрузке (payload) могут быть следующие поля:
Название поля | Обязателен | Тип | Описание |
---|---|---|---|
iss | Да | string | Заполняется ServiceID |
exp | Да | number | Cрок, до которого сессия будет активна. Интерпретируется как время в часовом поясе UTC |
user_id | Нет | number | Идентификатор пользователя в CASHOFF |
ext_id | Нет | string | Идентификатор пользователя во внешней системе (тот же ext_id, что и в серверном API) |
Нет | string | Одноименное поле запроса POST /users/add | |
enable_cashback | Нет | boolean | Одноименное поле запроса POST /users/add |
JWT должен быть сериализован в одну строку в виде Compact Serialization. Пример итогового значения:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjYXNob2ZmX2FwaSIsImV4cCI6MTY5NzU1NDQ3NSwiZXh0X2lkIjoiZGVtb191c2VyIn0.gj7-7fXkq4b4JDWWRTorWZ6KcwGq0bo2XDZ2F6uCqAM
Далее данная строка используется в качестве sessionID так же, как и идентификатор сессии из серверного API.
При формировании JWT необходимо заполнить либо поле user_id, либо ext_id. Если указано оба поля, то приоритет даётся user_id.
Для использования user_id к моменту генерации токена пользователь уже должен быть создан через серверное API (собственно оно и возвращает id пользователя в CASHOFF). А вот при использовании ext_id возможна авторегистрация: если у нас нет пользователя с таким ext_id, то CASHOFF создаст его. Процесс создания будет аналогичен выполнению запроса POST /users/add с указанным ext_id. В токене могут быть заполнены часть полей из этого запроса (email и enable_cashback), остальные останутся со значениями по умолчанию. Если данных полей не достаточно для настройки регистрации, то необходимо заранее вызвать POST /users/add, а не использовать авторегистрацию.
Работа с libco.js
После успешной загрузки libco.js добавит в глобальное окружение только одну функцию: loadCO. Эта функция создает объект сессии, через который и будет происходить работа с модулями:
var session = loadCO(sessionID, platformID);
Где в качестве sessionID передается идентификатор сессии, а в качестве platformID идентификатор площадки. Идентификатор площадки по умолчанию равен ServiceID и отличается от него только в тех случаях, когда для приложения предоставляется несколько вариантов брендирования. На странице можно использовать только один объект сессии и следовательно loadCO должен быть вызван только один раз на страницу.
Создание модуля
Модуль создается с помощью объекта сессии следующим образом:
var module = session.loadModule(moduleName, moduleParams, integrationParams);
Где:
- moduleName – название модуля. Для каждого модуля оно свое и указано в списке модулей.
- moduleParams – параметры модуля (объект). Настраивают то, что именно будет показывать модуль. Например, модулю с отчетом по расходам можно указать по какому счету или карте показывать отчет.
- integrationParams – параметры того, как будет происходить интеграция модуля со страницей сайта (объект).
Обязательным является только первый аргумент – название модуля.
После создания объекта модуля нужно средствами браузера добавить его на страницу:
containerElement.appendChild(module.node);
В module.node хранится элемент iframe, который и нужно добавить на страницу. Загрузка модуля начнется только после добавления этого элемента на страницу.
При перемещении модуля по DOM его состояние теряется и, после каждого такого действия, он будет перезагружаться. Поэтому таких действий, по возможности, стоит избегать.
Библиотека libco.js автоматически всем элементам iframe добавляет атрибут class вида `co_${moduleName}`, например class="co_mobile-main". На него можно ориентироваться при навешивании css-стилей на iframe.
Изменение текущего модуля
Ранее созданный объект модуля можно переключить на отображение другого модуля или изменить его параметры с помощью метода changeTo:
module.changeTo(newModuleName, newModuleParams, newIntegrationParams);
Данный вызов изменит только содержимое модуля, но не изменит его настройки и подписки на сообщения (module.receiveMsg()).
Модули применяют изменения на лету, поэтому такой способ будет существенно быстрее удаления текущего и добавления нового модуля.
Удаление модуля со страницы
После того, как работа с модулем завершена и он больше не понадобится, его нужно удалить из системы:
module.destroy()
После вызова этого метода ресурсы, связанные с модулем будут освобождены, а элемент iframe удален со страницы.
Получение сообщений от модуля
Модуль может информировать страницу о различных событиях внутри себя с помощью сообщений. Страница, для того чтобы получать такие сообщения, должна на них подписаться:
module.receiveMsg(msgType, function(msgData){…})
Подписка на сообщения происходит для каждого модуля отдельно. Функция будет вызвана при каждом получении сообщения и в качестве первого аргумента она получит данные (тело) этого сообщения. Есть ряд глобальных сообщений, а так же каждый модуль может иметь свои собственные. Глобальные описаны ниже,а специфичные для модуля перечислены в описании модуля.
Вызов методов модулей
Ряд модулей экспортирует "методы": это некоторые функции, которые приложение может вызвать внутри модуля. Вызов их выполняется через следующую функцию:
module.runMethod(methodName, methodParams, callback, errorCallback): Promise
Где:
- methodName – название метода. Каждый модуль может предоставлять свои методы.
- methodParams – параметры метода. Если параметров несколько, то они указываются в виде массива или объекта
- callback – функция, которая будет вызвана при успешном выполнении метода и получит результат вызова
- errorCallback – функция, которая будет вызвана при ошибке вызова
- Promise – экземпляр window.Promise, если последний существует
Обязательным аргументом является только название метода (methodName).
Результат выполнения метода (успешно/нет) и возвращаемое значение можно получить двумя способами: либо указать callback и errorCallback, либо извлечь из промиса.
Для создания экземпляра Promise libco.js будет использовать следующую сигнатуру:
var promise = new window.Promise(function(resolve, reject){…})
Экспорт методов для модулей
Можно не только вызывать методы модуля, но и предоставлять свои методы, которые модули CASHOFF смогут вызывать. Например, таким образом некоторые модули запрашивают определенные действия у родительской страницы, которые не возможно выполнить внутри модуля.
Методы экспортируются через объект сессии и доступны всем модулям сразу:
session.exportMethod(methodName, function(arg, module){…})
Где:
- methodName – название метода
- arg – параметры вызова
- module – объект модуля, который осуществил вызов
Такие методы могут иметь возвращаемое значение - оно будет передано в вызывающий модуль. Так же возможно вернуть промис - в таком случае результатом функции станет результат промиса.
В качестве промиса принимается любой объект, который имеет метод then(). Этот метод будет использоваться для получения значения промиса.
Однако если объект имеет так же метод done(), то done() будет иметь больший приоритет, чем then() и значение промиса будет получено через done().
Навигация между модулями
Многие модули CASHOFF логически связаны друг с другом переходами (навигацией). Например, в модуле с компактным представлением отчета может быть ссылка на этот же отчет в полном размере. Однако самостоятельно модули не могут производить изменения на странице, поэтому навигация между модулями должна быть организована интегрирующей системой.
Реализация навигации между модулями состоит из следующих этапов:
- Указать при создании модуля функцию, которая будет вызвана, когда пользователь совершит в этом модуле действие, предполагающее навигацию.
- Функция (callback), в качестве аргументов получит:
- название навигации - уникальный, в рамках модуля, идентификатор, который говорит о том, какое действие совершил пользователь
- название модуля – этот модуль нужно показать пользователю в рамках навигации
- параметры модуля – эти параметры должны быть переданы показываемому в рамках навигации модулю при его создании
Используя все эти данные, функция должна показать пользователю запрошенный модуль с указанными параметрами.
Таким образом, запрос навигации представляет собой запрос на открытие какого-либо модуля CASHOFF.
При этом этот новый модуль может быть расположен на логически другой странице сайта. Например, если компактный модуль отчета расположен на главной странице сайта, а полный модуль отчета расположен в разделе личных финансов, то при запросе навигации имеет смысл не просто показать желаемый модуль, но и показать его в своем разделе – т.е. сменить страницу сайта.
Если какая-либо навигация предполагает перезагрузку страницы сайта, то нужно каким-либо образом передать между страницами название модуля и его параметры.
Поддерживаемые навигации указываются при создании модуля и весь процесс навигации выглядит примерно так:
var module = co.loadModule('mini-report', null, { 'navigation': { '*': function(to, module, params) { console.log('Не известная навигация ' + to); }, 'big_report': function(to, module, params) { siteCore.toPFMPage(); co.loadModule(module, params); … } } })
'navigation' – это поле третьего аргумента loadModule, в котором в виде словаря указываются поддерживаемые навигации и функции, их осуществляющие. В качестве ключа 'navigation' можно указать:
- название навигации: функция будет вызвана только для данной навигации
- *: функция будет вызвана для всех навигаций, для которых не задано собственных функций
Можно использовать для всех навигаций одну функцию, которая будет осуществлять маршрутизацию по названию модуля, а не по названию навигации.
Сообщения от модулей
В данном разделе описаны различные события, о которых модуль сигнализирует сообщением, а так же то, как их обрабатывать.
Статус загрузки модуля
Т.к. модуль при добавлении на страницу начинает с нуля грузить все данные с сервера, то это может занять какое-то время. Для индикации процесса загрузки модуля в приложении предусмотрено два сообщения: ready и notReady. Событие ready наступает когда модуль полностью загрузился, а notReady когда модуль в процессе перехода к другой страницы (перезагружается).
Однако текущие модули реализованы по технологии SPA (single page application) и в процессе переходов не перезагружаются. По этому реагировать стоит прежде всего на ready. Типовое использование данного события - показ индикатора загрузки с момента добавления модуля на страницу до момента получения ready. Например так:
var module = session.loadModule(moduleName, moduleParams, integrationParams); containerElement.appendChild(module.node); showSpinnerForCashoffModule(module); module.receiveMsg('ready', function() { hideSpinnerForCashoffModule(module); });
Доступность модуля
Web-сервера CASHOFF в какой-то момент могут быть не доступны, либо клиент имеет проблемы подключения к ним/с интернетом в целом. На этот случай предусмотрено сообщение notAvailable, которое сигнализирует что модуль не может загрузиться. Подписаться на него можно следующим образом:
module.receiveMsg('notAvailable', (reason) => {...});
В качестве reason приходит код причины недоступности:
- maintenance: на сервисе ведутся плановые работы
- lost_connection: соединение с сетью интернет не доступно
Важно учитывать, что это событие происходит на этапе открытия модуля либо в процессе его работы. Если клиент имеет изначальные проблемы с интернетом, то проблемы возникнут уже на этапе загрузке libco.js - их нужно обрабатывать отдельно.
Особенности встройки модулей
Размеры модулей
Различные модули по-разному определяют свой размер. У каждой из размерности (ширины и высоты) может быть один из следующих режимов адаптивности:
- размер фиксирован: размерность имеет заданное значение и не меняется. libco.js фиксирует данное значение на элементе <iframe> через css-свойства width/height
- диапазон размеров: размерность задана в определенном диапазоне, в рамках него модуль "тянется" и адаптирует дизайн под размер. libco.js фиксирует диапазон на элементе <iframe> через css-свойства min-*/max-*. Зачастую диапазон открытый - указано только минимальное значение, максимальное может быть произвольным. Определение конкретного размера из заданного диапазона через css-свойства оставлено на приложение: оно должно задать элементу <iframe> необходимый ему размер. К примеру сделать его адаптивным к другим элементам страницы.
- размер по содержимому: размерность не фиксирована, а определяется содержимым модуля (т.е. управляется не снаружи, а изнутри). Такая адаптивность в некоторых модуля применяется к высоте: это позволяет модулю растянутся на полный необходимый размер, а вертикальный скрол происходит уже внутри приложения. Такой режим применяется для тех виджетов, у которых зафиксировать высоту не возможно, но необходимо избежать двойного скрола (внутри приложения и внутри модуля).
В описании модуля указано в каком режиме работают его размерности.
Темная и светлая темы
Ряд модулей поддерживают выбор между темной и светлой темами: в темной подложка (фон) темная, а текст поверх светлый; в светлой наоборот - светлая (белая) подложка с темным текстом. По умолчанию всегда используется светлая, но в ряде модулей можно включить тёмную:
- mobile-main
- cashback
- stories-list
- stories-favorites-list
- story
Тема указывается в момент создания модуля (session.loadModule()) в integrationParams. За это отвечает строковый ключ theme, он может принимать следующие значения:
- "dark" - тёмная тема
- "light" - светлая тема
- "system" - использовать тему из настройки системы (браузера)
- undefined - не указано, светлая
Пример создания модуля mobile-main с включённой тёмной темой:
var module = session.loadModule("mobile-main", undefined, {"theme": "dark"});
Смена темы на лету на данный момент не поддерживается, по этому для смены необходимо пересоздать модуль.
Поддерживаемые браузеры и webview
CASHOFF осуществляет поддержку для следующих настольных браузеров:
- Edge
- Chrome
- Firefox
- Safari
Для всех браузеров поддержка распространяется на текущую и предыдущую версии. Так же, во вторую очередь, происходит поддержка браузеров, основанных на движках WebKit/Blink, таких как Яндекс.Браузер, Opera, Vivaldi и т.д.
Если браузер совершенно точно не поддерживается (не просто не актуальная версия, а в целом отсутствует ряд необходимых фич), то co.loadModule вернет null и использовать встраиваемые модули будет не возможно.
Проверить, поддерживается ли текущий браузер или нет, можно с помощью объекта сессии:
var isSupported = co.isBrowserSupported()
В случае мобильных браузеров поддержка следующая:
- Safari из текущей мажорной версии iOS/iPadOS и вплоть до 2 мажорных версий младше.
- Chrome, Firefox текущей и предыдущей версии (android/ios).
В случае webview поддержка следующая:
- iOS: WKWebView, версии аналогичны версиям Safari из предыдущего пункта
- android: встроенный Android System WebView текущей и предыдущей версии
- остальные webview: актуальные версии WebKit/Blink, либо основанные на браузерах поддерживаемых версий
Готовый пример html страницы с модулем
Ниже будет пример готовой html страницы, которая при открытии загрузит модуль. Перед использованием нужно заполнить переменные sessionID, platformID и сохранить файл с расширением .html. Если нужен тестовый стенд не https://developer.cashoff.ru/, то поменять домен в первом теге script. Данный пример загрузит модуль main во всё окно браузера.
<html> <head> <meta name="viewport" content="viewport-fit=cover initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"> <script src="https://developer.cashoff.ru/static/build/libco.js"></script> <style> html, body { background: white; margin: 0px; padding: 0px; } iframe { width: 100vw; height: 100vh; } </style> </head> <body> <div id="module-target"></div> </body> <script type="text/javascript"> window.addEventListener('DOMContentLoaded', () => { var sessionID = "идентификатор сессии" var platformID = "идентификатор площадки" var moduleTarget = document.getElementById("module-target"); var session = loadCO(sessionID, platformID); var module = session.loadModule("main"); moduleTarget.appendChild(module.node); }); </script> </html>