Native Smartface & WebView Interaction

Using a Hybrid Approach in a Fully Native Environment

Smartface is a fully native development environment with 100% native API access, however, it has a JavaScript based architecture and Smartface applications developed with JavaScript exhibit similarities with Web apps developed via JavaScript. The main difference is that Smartface itself has its own implementation of the UI objects (sf-core) without any use of the DOM which is the basis of the visual portions of the web pages.

Without the DOM support in Smartface due to its native architecture, you cannot use other Web frameworks and libraries such as jQuery or Angular or reactJS. In those cases, Smartface WebView comes in handy, enabling the display of the web content (like a browser) within the App as a component.

Smartface JavaScript engine has its own scope and each WebView has it own scope. They are not interacting to each other directly but there are ways to establish a connection between each other.

In the rest of this guide, WebView component will be used intensively; assuming the reader is knowledgeable about WebView Guide and WebView API Reference.

WebView Code Injection

Smartface WebView instance has a evaluateJS method to run a code block within the displayed Web Page content.

This method takes the script as a string. You cannot pass objects between native app and the WebView directly, as the Smartface app and the Web Page have their own scopes. If you want to pass objects, you can serialize them as JSON and deserialize them on the other side.

Add Smartface Logo to Web Page
Add Smartface Logo to Web Page
const img = document.createElement("img");
img.src = "http://account.smartface.io/Images/logo.png";
document.body.insertBefore(img, document.body.firstChild);

Given code above adds the Smartface logo to the web page, which should be running inside the web page scope.

To run the code in the web page contained in the WebView, following things should be done in order:

  1. Convert your script-to-run to string a. The injected code needs to run on document load!

  2. Have your WebView instance set up

  3. After page is loaded, evaluate the script string

Script as String
Script as String
const script = `
function insertLogo() {
const img = document.createElement("img");
img.src = "http://account.smartface.io/Images/logo.png";
document.body.insertBefore(img, document.body.firstChild);
}
if (document.readyState === "complete")
insertLogo();
else
document.onload = insertLogo();
`;

The sample code given above is for modifying the document.onLoad event. If you are using jQuery you can use document.ready().

WebView Setup and run Script onLoad
WebView Setup and run Script onLoad
import WebView from "sf-core/ui/webview";
import FlexLayout from "sf-core/ui/flexlayout";
const url = "http://example.com/";
const myWebView = new WebView({
left: 0,
top: 0,
right: 0,
bottom: 0,
positionType: FlexLayout.PositionType.ABSOLUTE,
onShow: (event) => {
console.log(event.url);
if (url === event.url) { //make sure script is running only on specific url(s)
console.log("about to eval");
myWebView.evaluateJS(script);
}
}
});
this.layout.addChild(myWebView);
myWebView.loadURL(url);
Page Code
Page Code
import Page1Design from 'generated/pages/page1';
import WebView from "sf-core/ui/webview";
import FlexLayout from "sf-core/ui/flexlayout";
const script = `
function insertLogo() {
const img = document.createElement("img");
img.src = "http://account.smartface.io/Images/logo.png";
document.body.insertBefore(img, document.body.firstChild);
}
if (document.readyState === "complete")
insertLogo();
else
document.onload = insertLogo();
`;
export default class Page1 extends Page1Design {
myWebView: WebView;
constructor() {
super();
this.onShow = onShow.bind(this, this.onShow.bind(this));
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
}
initWebView() {
const myWebView = new WebView({
left: 0,
top: 0,
right: 0,
bottom: 0,
positionType: FlexLayout.PositionType.ABSOLUTE,
onShow: (event) => {
console.log(event.url);
if (url === event.url) { //make sure script is running only on specific url(s)
console.log("about to eval");
myWebView.evaluateJS(script);
}
}
});
this.myWebView = myWebView;
this.layout.addChild(myWebView);
}
}
function onShow(superOnShow: () => void) {
superOnShow();
const url = "http://example.com/";
this.myWebView.loadURL(url);
}
function onLoad(superOnLoad: () => void) {
superOnLoad();
this.initWebView();
}

Using WebView Bridge (util)

WebView Bridge (a util library) allows bi-directional communication with Web Page. The usages mentioned above, they all are invoked from Native side to Web Page: A JS code is executed and the synchronous response is parsed on the app. For a press of a button, a timer event, a web socket event on the Web Page can cause an action on the native side. This util library allows event and event data flow to the native side. In order to trigger an action on the native side, using this WebView Bridge, following code should be executed:

WebPage
WebPage
//using window variable is recommended
window.boubleEvent("eventName", eventDataObject);

It is possible to add this call to the Web Page from the source of the Web Page (if the developer can access its source code & change it), or it is possible to inject that code from Native using evaluateJS

Web Page HTML
Web Page HTML
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<button id="btn">Press me</button>
</body>
</html>

If the developer can change the code, the HTMLwill be like this:

Modified Web Page HTML
Modified Web Page HTML
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<button id="btn">Press me</button>
<script type="text/javascript">
const counter = 0;
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
window.boubleEvent("eventName", {
pressCount: counter
});
counter++;
});
</script>
</body>
</html>

Another approach is to inject that code from native side. There is no need to use them both; use only one of them.

Event Injection
Event Injection
const wvb = page.wvb = new WebViewBridge({
webView: page.webView,
source: "http://www.google.com"
});
page.wvb = wvb;
wvb.on("myEvent", () => {
alert("myEvent", "Event fired");
});
wvb.ready().then(()=>{
wvb.evaluateJS(script);
});
const script = `
setTimeout(()=> {
window.boubleEvent("myEvent");
}, 1000);
`;

Example Use Cases

  • Display charts or graphics (one way interactions work best for using WebViews in a native app)

  • Logins provided via web (e.g. Oauth Implicit flow)

  • Remove or hide unwanted elements, such as logos from the web page

  • Share authentication

  • Display other components that are on the web that require some data from the app

Hide Elements in a Web Page

In this example, a web page inside WebView is shown and 2 native buttons are added to bottom of the native Smartface page. Those buttons are performing show/hide operations within the web page.

Web Page HTML
Web Page HTML
<!DOCTYPE html>
<html>
<head>
<title>Smartface - WebView Hide & Show Example </title>
<style type="text/css">
#item {
background-color: RoyalBlue;
color: white;
width: calc(100% - 40px);
height: calc(100% - 40px);
margin: 20px;
display: flex;
align-items: center;
font-size: 72pt;
}
#item div {
width: 100%;
text-align: center;
display: block;
}
html,
body {
height: 100%;
margin: 0;
background-color: DarkSeaGreen;
}
</style>
</head>
<body>
<div id="item">
<div>Item</div>
</div>
</body>
</html>

Link to the web page is also available: https://az793023.vo.msecnd.net/examples/sf-core/webview/hide-show.html Abstract Smartface code is given below:

Smartface Code
Smartface Code
const btnHide = new Button({
onPress: () => {
this.webview1.evaluateJS('document.getElementById("item").style.display="none";');
}
});
const btnShow = new Button({
onPress: function() {
page.webview1.evaluateJS('document.getElementById("item").style.display="flex";');
}
});
function onShow(superOnShow: () => void) {
this.webview1.loadURL("https://az793023.vo.msecnd.net/examples/sf-core/webview/hide-show.html");
};
Page Hide Show Code
Page Hide Show UI
Page Hide Show Code
scripts/pages/pageHideShow.ts
import PageHideShowDesign from 'generated/pages/pageHideShow';
export default class PageHideShow extends PageHideShowDesign {
constructor() {
super();
this.onShow = onShow.bind(this, this.onShow.bind(this));
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
}
initComponents() {
this.btnHide.onPress = () => {
this.webView1.evaluateJS('document.getElementById("item").style.display="none";', () => { });
};
this.btnShow.onPress = () => {
this.webView1.evaluateJS('document.getElementById("item").style.display="flex";', () => { });
}
}
}
function onShow(superOnShow: () => void) {
superOnShow();
this.webView1.loadURL("https://az793023.vo.msecnd.net/examples/sf-core/webview/hide-show.html");
}
function onLoad(superOnLoad: () => void) {
superOnLoad();
this.headerBar.title = "Hide Show";
this.initComponents();
}
Page Hide Show UI
.ui/pageHideShow.pgx
{
"components": [
{
"className": ".sf-page",
"id": "73dd-6eef-0570-de44",
"initialized": true,
"props": {
"children": [
"7d83-93d7-e744-5512",
"1587-0041-10da-aeb1",
"2ce7-f3d6-d755-c118",
"e8d7-ed5b-1706-d591"
],
"name": "pageHideShow",
"orientation": "PORTRAIT",
"parent": null
},
"type": "Page",
"userProps": {}
},
{
"className": ".sf-statusBar",
"id": "7d83-93d7-e744-5512",
"props": {
"children": [],
"isRemovable": false,
"name": "statusBar",
"parent": "73dd-6eef-0570-de44"
},
"type": "StatusBar",
"userProps": {}
},
{
"className": ".sf-headerBar",
"id": "1587-0041-10da-aeb1",
"props": {
"children": [],
"isRemovable": false,
"name": "headerBar",
"parent": "73dd-6eef-0570-de44",
"title": "pageHideShow"
},
"type": "HeaderBar",
"userProps": {
"title": "pageHideShow"
}
},
{
"className": ".sf-webView",
"id": "2ce7-f3d6-d755-c118",
"props": {
"children": [],
"name": "webView1",
"parent": "73dd-6eef-0570-de44",
"usePageVariable": true
},
"type": "WebView",
"userProps": {
"backgroundColor": "rgba( 255, 255, 255, 1 )",
"bottom": 0,
"flexProps": {
"flexGrow": 1
},
"testId": "seCIhrddt",
"usePageVariable": true
}
},
{
"className": ".sf-flexLayout",
"id": "e8d7-ed5b-1706-d591",
"props": {
"children": [
"32a0-2435-e9f4-9f5d",
"1e5b-0adf-7131-ac69"
],
"name": "flWrapper",
"parent": "73dd-6eef-0570-de44"
},
"type": "FlexLayout",
"userProps": {
"flexProps": {
"flexDirection": "ROW"
},
"height": 70,
"testId": "ntg288NQf"
}
},
{
"className": ".sf-button",
"id": "32a0-2435-e9f4-9f5d",
"props": {
"children": [],
"name": "btnHide",
"parent": "e8d7-ed5b-1706-d591",
"text": "Hide",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"height": 70,
"testId": "nOZpeoW_O",
"text": "Hide",
"usePageVariable": true
}
},
{
"className": ".sf-button",
"id": "1e5b-0adf-7131-ac69",
"props": {
"children": [],
"name": "btnShow",
"parent": "e8d7-ed5b-1706-d591",
"text": "Show",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"height": 70,
"testId": "Da1wDDoPp",
"text": "Show",
"usePageVariable": true
}
}
]
}

Display a chart

In this example, an HTML Chart is shown within the WebView and its data is being modified (set) by the native Smartface code.

Chart example has been taken from Chart.js and slightly modified for demo purposes. Used sample page is on https://az793023.vo.msecnd.net/examples/sf-core/webview/chart.html

Chart Page Code
Chart Page UI
Chart Page Code
scripts/pages/pgChart.ts
import PgChartDesign from 'generated/pages/pgChart';
export default class PgChart extends PgChartDesign {
constructor() {
super();
this.onShow = onShow.bind(this, this.onShow.bind(this));
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
}
initComponents() {
this.btnRandomizeData.onPress = () => {
const script = `
config.data.datasets.forEach(function(dataset) {
dataset.data = dataset.data.map(function() {
return randomScalingFactor();
});
});
window.myLine.update();`;
this.webView1.evaluateJS(script, () => { });
}
this.btnAddDataSet.onPress = () => {
const script = `
const colorName = colorNames[config.data.datasets.length % colorNames.length];
const newColor = window.chartColors[colorName];
const newDataset = {
label: 'Dataset ' + config.data.datasets.length,
backgroundColor: newColor,
borderColor: newColor,
data: [],
fill: false
};
for (let index = 0; index < config.data.labels.length; ++index) {
newDataset.data.push(randomScalingFactor());
}
config.data.datasets.push(newDataset);
window.myLine.update();`;
this.webView1.evaluateJS(script, () => { });
}
this.btnRemoveDataset.onPress = () => {
const script = `
config.data.datasets.splice(0, 1);
window.myLine.update();`;
this.webView1.evaluateJS(script, () => { });
}
this.btnAddData.onPress = () => {
const script = `
if (config.data.datasets.length > 0) {
const month = MONTHS[config.data.labels.length % MONTHS.length];
config.data.labels.push(month);
config.data.datasets.forEach(function(dataset) {
dataset.data.push(randomScalingFactor());
});
window.myLine.update();
}`;
this.webView1.evaluateJS(script, () => { });
}
this.btnRemoveData.onPress = () => {
const script = `
config.data.labels.splice(-1, 1); // remove the label first
config.data.datasets.forEach(function(dataset, datasetIndex) {
dataset.data.pop();
});
window.myLine.update();`;
this.webView1.evaluateJS(script, () => { });
}
}
}
function onShow(superOnShow: () => void) {
superOnShow();
this.webView1.loadURL("https://az793023.vo.msecnd.net/examples/sf-core/webview/chart.html");
}
function onLoad(superOnLoad: () => void) {
superOnLoad();
this.initComponents();
}
Chart Page UI
.ui/pgChart.pgx
{
"components": [
{
"className": ".sf-page",
"id": "dd76-5bb1-4adc-c2f0",
"initialized": true,
"props": {
"children": [
"7411-c478-ff2f-1930",
"03a3-01bf-8f91-421b",
"1555-2314-51c9-9161",
"b200-db5a-6e9e-d332"
],
"name": "pgChart",
"orientation": "PORTRAIT",
"parent": null
},
"type": "Page",
"userProps": {}
},
{
"className": ".sf-statusBar",
"id": "7411-c478-ff2f-1930",
"props": {
"children": [],
"isRemovable": false,
"name": "statusBar",
"parent": "dd76-5bb1-4adc-c2f0"
},
"type": "StatusBar",
"userProps": {}
},
{
"className": ".sf-headerBar",
"id": "03a3-01bf-8f91-421b",
"props": {
"children": [],
"isRemovable": false,
"name": "headerBar",
"parent": "dd76-5bb1-4adc-c2f0",
"title": "Chart"
},
"type": "HeaderBar",
"userProps": {
"title": "Chart"
}
},
{
"className": ".sf-webView",
"id": "1555-2314-51c9-9161",
"props": {
"children": [],
"name": "webView1",
"parent": "dd76-5bb1-4adc-c2f0",
"usePageVariable": true
},
"type": "WebView",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"testId": "_QyBNF7be",
"usePageVariable": true
}
},
{
"className": ".sf-flexLayout",
"id": "b200-db5a-6e9e-d332",
"props": {
"children": [
"2808-3b6f-1c76-bd54",
"abe0-a084-69b9-1686",
"9556-e787-9a36-eaba",
"082d-6c0f-166a-2b40",
"d328-f939-9e82-568d"
],
"name": "flWrapper",
"parent": "dd76-5bb1-4adc-c2f0"
},
"type": "FlexLayout",
"userProps": {
"flexProps": {
"flexDirection": "ROW",
"flexWrap": "WRAP"
},
"height": 210,
"testId": "x6w7n_iZz"
}
},
{
"className": ".sf-button",
"id": "2808-3b6f-1c76-bd54",
"props": {
"children": [],
"name": "btnRandomizeData",
"parent": "b200-db5a-6e9e-d332",
"text": "Randomize Data",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"height": 70,
"testId": "UNDG84t8h",
"text": "Randomize Data",
"usePageVariable": true,
"width": 180
}
},
{
"className": ".sf-button",
"id": "abe0-a084-69b9-1686",
"props": {
"children": [],
"name": "btnAddDataSet",
"parent": "b200-db5a-6e9e-d332",
"text": "Add Dataset",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"height": 70,
"testId": "yJ0BYv-VL",
"text": "Add Dataset",
"usePageVariable": true,
"width": 120
}
},
{
"className": ".sf-button",
"id": "9556-e787-9a36-eaba",
"props": {
"children": [],
"name": "btnRemoveDataset",
"parent": "b200-db5a-6e9e-d332",
"text": "Remove Dataset",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"height": 70,
"testId": "aDSMf1DHG",
"text": "Remove Dataset",
"usePageVariable": true,
"width": 150
}
},
{
"className": ".sf-button",
"id": "082d-6c0f-166a-2b40",
"props": {
"children": [],
"name": "btnAddData",
"parent": "b200-db5a-6e9e-d332",
"text": "Add Data",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"height": 70,
"testId": "owsuQpaMm",
"text": "Add Data",
"usePageVariable": true,
"width": 120
}
},
{
"className": ".sf-button",
"id": "d328-f939-9e82-568d",
"props": {
"children": [],
"name": "btnRemoveData",
"parent": "b200-db5a-6e9e-d332",
"text": "Remove Data",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"height": 70,
"testId": "e6j6Wfems",
"text": "Remove Data",
"usePageVariable": true,
"width": 120
}
}
]
}

Getting button press from WebView

In this example, let's assume the app is displaying some web content, such as an advertisement campaign. The content of the page is displayed, at the end of the document there is a button, such as "I am interested". If user press to the button, the app should capture the press and do something on the native side.

Button Press Code
Button Press UI
Button Press Code
scripts/pages/pgButtonPress.ts
import PgButtonPressDesign from 'generated/pages/pgButtonPress';
import WebViewBridge from 'sf-extension-utils/lib/webviewbridge';
export default class PgButtonPress extends PgButtonPressDesign {
constructor() {
super();
this.onShow = onShow.bind(this, this.onShow.bind(this));
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
}
initWebViewBridge() {
const url = "https://az793023.vo.msecnd.net/examples/webviewbridge/buttonevent.html";
const script = `
const btn = document.getElementById("btn");
btn.onclick = function() {
window.boubleEvent("buttonPress");
};
`;
//Within constructor
const wvb = new WebViewBridge({
webView: this.webView1, //WebView. Should be assigned from UI editor.
source: url
});
wvb.on("buttonPress", (data) => {
const text = "Button pressed";
console.info(text);
alert(text);
//Do your own logic
});
wvb.ready().then(() => {
wvb.evaluateJS(script);
});
}
}
function onShow(superOnShow: () => void) {
superOnShow();
}
function onLoad(superOnLoad: () => void) {
superOnLoad();
this.initWebViewBridge();
}
Button Press UI
.ui/pgButtonPress.pgx
{
"components": [
{
"className": ".sf-page",
"id": "fb98-d27f-d144-77f6",
"initialized": true,
"props": {
"children": [
"c75a-bf12-3dd1-8036",
"29b7-dcda-bc59-8b09",
"6b25-f8da-fcf9-6af0"
],
"name": "pgButtonPress",
"orientation": "PORTRAIT",
"parent": null
},
"type": "Page",
"userProps": {}
},
{
"className": ".sf-statusBar",
"id": "c75a-bf12-3dd1-8036",
"props": {
"children": [],
"isRemovable": false,
"name": "statusBar",
"parent": "fb98-d27f-d144-77f6"
},
"type": "StatusBar",
"userProps": {}
},
{
"className": ".sf-headerBar",
"id": "29b7-dcda-bc59-8b09",
"props": {
"children": [],
"isRemovable": false,
"name": "headerBar",
"parent": "fb98-d27f-d144-77f6",
"title": "pgButtonPress"
},
"type": "HeaderBar",
"userProps": {
"title": "pgButtonPress"
}
},
{
"className": ".sf-webView",
"id": "6b25-f8da-fcf9-6af0",
"props": {
"children": [],
"name": "webView1",
"parent": "fb98-d27f-d144-77f6",
"usePageVariable": true
},
"type": "WebView",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"testId": "UgtVwmh27",
"usePageVariable": true
}
}
]
}

WebView Bridge - Loading Dynamic Libraries

WebView bridge also allows loading other libraries into the WebView. In the documentation of WebView Bridge loadScripts method will be used to perform this action. This method is adding script tags with src value set to the given URL. This URL should be relative to the page. If it is not relative, absolute URL should be used. Loading of the scripts are performed after WebView Bridge is ready. This method also returns a Promise object. This Promise should be used to evaluate scripts which are dependent on the libraries which are loaded with the loadScripts method.

In this example, a cookie string will be injected into the WebView. This cannot be done with JavaScript injection, because the very first initial request should be already authenticated.

Adding Cookie with Native API Access
Adding Cookie with Native API Access
import System from 'sf-core/device/system';
const cookie = "Your full cookie";
const targetUrl = "http://example.com"
if (System.OS === System.OSType.IOS) {
//@ts-ignore
const MutableRequest = SF.requireClass("NSMutableURLRequest");
//@ts-ignore
const NSURL = SF.requireClass("NSURL");
const request = MutableRequest.requestWithURL(NSURL.URLWithString(targetUrl));
request.addValueForHTTPHeaderField(`${cookie}`, "Cookie");
webview.nativeObject.load(request); //this is equvalent of webview.loadURL
}
else {
//@ts-ignore
const CookieManager = requireClass("android.webkit.CookieManager");
const cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.setCookie(targetUrl, `${cookie}`);
webview.loadURL(targetUrl);
}

evaluateJS Performance

WebView.evaluateJS is used for two things:

  1. To execute a JavaScript on the WebView

  2. Parsing and getting the result of the executed JavaScript statement

Regardless of what both of the steps will be executed. In some cases, the result of the executes JavaScript statement might be too long to parse. This will greatly reduce the performance of the evaluateJS execution. Simply add a single null statement to the end of the JavaScript statement, this will reduce the parsing of the result operation. Util WebView Bridge evaluateJS is designed with that performance trick. Here is the code block from WebView Bridge:

WebView Bridge - evalutateJS
WebView Bridge - evalutateJS
function evaluateJS(javascript, onReceive) {
if (!this.parseResponses) //a flag used for wrapping the code or not
javascript = `(function(){\n${javascript}\n})();null;`; //Wraps the Code in an immedaite function
return this.webView.evaluateJS(javascript, onReceive);
}