NativeAppIntegration
The plugin connects the pages of a Festi project to a native mobile application that shows them in a WebView.
Overview
The plugin does three things:
- It adds a JavaScript bridge and safe-area styles to each themed page of a mobile request.
- It sends the session data of the current user to the native application.
- It receives the Firebase token of the device and dispatches a server event with the token.
The plugin has no tables, no tblDefs and no DataAccessObject.
The plugin does not store the Firebase token. The project stores the token in a listener of the event.
The server does not detect the native application.
Core::isMobileRequest() examines the User-Agent header, thus the plugin also adds its files for a mobile browser.
The bridge detects the native application in the browser: it looks for window.flutter_inappwebview.
Requirements
- Festi Core with
Core::isMobileRequest(). - The Festi Theme package. The plugin extends
Festi\Theme\Plugin\DisplayPluginand uses the events ofFesti\Theme\ITheme. - A theme that dispatches
ITheme::EVENT_THEME_BEFORE_FETCH_MAINandITheme::EVENT_THEME_HEAD_BOTTOM. - jQuery on the page. The script of the plugin sends the token with
jQuery.ajax. - A native application that uses the
flutter_inappwebviewlibrary.
static/css/styles.css uses these selectors: #header, #left-panel, #content, .b-mobile-nav,
.header-right-panel, #divSmallBoxes and .modal.
A theme with other selectors needs its own styles.
Installation
Install the plugin with the Festi CLI. Run the command from the root of the project repository:
./vendor/bin/festi-plugin --mode install --path ./src/dashboard/ --dump ./dumps/ \
--plugin NativeAppIntegration \
--repository [email protected]:FestiPlugins/php_festi_plugin_nativeappintegration.git
The CLI example was not run for this page.
The command does these steps:
- It adds the repository to the plugins directory as a git submodule.
- It writes a migration into the dumps directory. The migration adds the plugin to the
festi_pluginstable. - It applies the migrations that the database does not have.
The plugin has no install directory, thus the migration contains only the festi_plugins row.
Commit the submodule and the new migration together.
To get a newer version of the plugin, update the submodule:
git submodule update --remote src/dashboard/plugins/NativeAppIntegration
Configuration
The plugin has no settings. init.php adds the listeners of the plugin on Core::EVENT_ON_AFTER_INIT.
URL rule for the Firebase token
The script of the plugin sends the token to /api/firebase/token/.
The method NativeAppIntegrationPlugin::onJsonStoreFirebaseToken() receives the request.
The plugin has no install dump, thus the project adds the URL rule in a migration.
Create an empty migration file:
./vendor/bin/festi-make-migration --dump ./dumps/
Put this SQL into the new file. The example is for PostgreSQL and was not run for this page:
INSERT INTO festi_url_rules (plugin, pattern, method)
SELECT 'NativeAppIntegration', '~^/api/firebase/token/$~', 'onJsonStoreFirebaseToken'
WHERE NOT EXISTS (
SELECT 1 FROM festi_url_rules
WHERE plugin = 'NativeAppIntegration'
AND method = 'onJsonStoreFirebaseToken'
);
INSERT INTO festi_url_rules2areas (id_url_rule, area)
SELECT festi_url_rules.id, 'backend'
FROM festi_url_rules
WHERE festi_url_rules.plugin = 'NativeAppIntegration'
AND festi_url_rules.method = 'onJsonStoreFirebaseToken'
AND NOT EXISTS (
SELECT 1 FROM festi_url_rules2areas
WHERE festi_url_rules2areas.id_url_rule = festi_url_rules.id
AND festi_url_rules2areas.area = 'backend'
);
Apply the migration:
./vendor/bin/festi-migrate --path ./src/dashboard/ --dump ./dumps/
Do not use festi-plugin --mode url for this rule.
That mode adds a new method to the plugin class, and the class already has onJsonStoreFirebaseToken().
Usage
Store the Firebase token
onJsonStoreFirebaseToken() reads the field token from the request.
The request body can be form data or JSON (Content-Type: application/json).
When the token is empty, the method throws a SystemException with the message Token is required.
When the token is valid, the method dispatches StoreUserDeviceFirebaseTokenEvent.
Add a listener in your project and store the token there:
use plugin\NativeAppIntegration\Domain\Event\StoreUserDeviceFirebaseTokenEvent;
$core = Core::getInstance();
$core->addEventListener(
StoreUserDeviceFirebaseTokenEvent::EVENT_NAME,
function (StoreUserDeviceFirebaseTokenEvent $event) use ($core) {
$userId = $core->user->getID();
$token = $event->getToken();
// Store the token for the user ID.
// For example:
// UserDeviceTokens::storeToken($userId, $token);
}
);
UserDeviceTokens in the example is a class of your project. The plugin does not contain it.
Use your own URL for the token
You can send the token to a method of your own plugin.
- Add the method and its URL rule with the Festi CLI:
./vendor/bin/festi-plugin --mode url --plugin YourPlugin --pattern "~^/your-url/$~" \
--name StoreFirebaseToken --area backend --type json
The command adds the method onJsonStoreFirebaseToken to YourPluginPlugin.php.
It also adds the entries in the festi_url_rules and festi_url_rules2areas tables.
This CLI example was not run for this page.
- Write the body of the method:
/**
* @urlRule ~^/your-url/$~
* @area backend
*/
public function onJsonStoreFirebaseToken(Response &$response): bool
{
// Read the token from the POST request and store it for the current user.
return true;
}
- Give the URL to the script of the plugin. Listen to
LoadUrlStoreUserDeviceFirebaseTokenEvent:
use plugin\NativeAppIntegration\Domain\Event\LoadUrlStoreUserDeviceFirebaseTokenEvent;
$core = Core::getInstance();
$core->addEventListener(
LoadUrlStoreUserDeviceFirebaseTokenEvent::EVENT_NAME,
function (LoadUrlStoreUserDeviceFirebaseTokenEvent $event) use ($core) {
$event->setUrl($core->getUrl('/your-url/'));
}
);
Without step 3 the script sends the token to /api/firebase/token/.
Use the bridge in your JavaScript
static/js/NativeAppBridge.js creates the global object NativeAppBridgeInstance.
| Member | Description |
|---|---|
notifySessionUpdate(data) |
Sends web_session_update with { userId, email }. |
notifyLoggedIn(data) |
Sends web_user_logged_in with { userId, email }. |
notifyLoggedOut(reason) |
Sends web_user_logged_out with { reason }. The default reason is user_action. |
on(event, callback) |
Adds a listener for an event of the native application. Returns a function that removes the listener. |
isNative |
true when the page is in the native WebView. |
isReady |
true when the bridge can send messages. |
deviceInfo |
The data of the last native_device_info event. |
insets |
The data of the last native_insets event. |
firebaseToken |
The token of the last native_firebase_token event. |
const removeListener = NativeAppBridgeInstance.on('native_insets', (data) => {
console.debug('Insets', data);
});
NativeAppBridgeInstance.notifyLoggedOut('session_expired');
The bridge keeps the messages in a queue until it is ready. Then it sends them.
How it works
Listeners
| Method | Event | Function |
|---|---|---|
onBeforeFetchMain() |
ITheme::EVENT_THEME_BEFORE_FETCH_MAIN |
Includes static/css/styles.css and static/js/NativeAppBridge.js. |
onThemeHeadAddScripts() |
ITheme::EVENT_THEME_HEAD_BOTTOM |
Returns the content of templates/scripts.phtml. |
The two methods do nothing when Core::isMobileRequest() returns false.
onBeforeFetchMain() uses ITheme::EVENT_THEME_BEFORE_FETCH_MAIN, not Core::EVENT_ON_RESPONSE.
A page that the theme shows outside the usual response flow does not dispatch Core::EVENT_ON_RESPONSE.
The exception page is an example.
The theme CSS is already in the list at this point, thus the safe-area rules come after the theme rules.
Script of the page
templates/scripts.phtml does these steps:
- It calls
NativeAppBridgeInstance.notifySessionUpdate(sessionData). - It adds listeners for the events of the native application.
Session data
| Key | Source |
|---|---|
userId |
$core->user->getID() as a string, or null when there is no user object |
email |
$core->user->getValue('email'), or null when the value is empty |
Messages
A page sends a message with window.flutter_inappwebview.callHandler('onWebMessage', { event, data }).
The native application sends a message as a CustomEvent with the name native-message.
The detail of that event contains { event, data }.
templates/scripts.phtml handles these events of the native application:
| Event | Function |
|---|---|
native_device_info |
Writes the data to the console (console.debug). |
native_firebase_token |
Sends a POST request with the field token to the token URL. |
native_insets |
Writes the data to the console (console.debug). |
File layout
NativeAppIntegration/
├── NativeAppIntegrationPlugin.php # listeners and the token method
├── init.php # adds the listeners on EVENT_ON_AFTER_INIT
├── Domain/
│ └── Event/
│ ├── LoadUrlStoreUserDeviceFirebaseTokenEvent.php # changes the token URL
│ └── StoreUserDeviceFirebaseTokenEvent.php # carries the token
├── templates/
│ └── scripts.phtml # session data and listeners of the bridge
└── static/
├── css/styles.css # safe-area styles
└── js/NativeAppBridge.js # creates NativeAppBridgeInstance