-
Notifications
You must be signed in to change notification settings - Fork 58
/
mobile-pwa.Rmd
663 lines (542 loc) · 26.4 KB
/
mobile-pwa.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
# `{shinyMobile}` and PWA {#mobile-pwa}
Transforming a classic Shiny app into a __PWA__ is a game changer for end users. By the end of this chapter, you'll be able to provide top-notch features for your Shiny apps like:
- Add a __fullscreen__ support.
- Make them __installable__.
- Support __offline__ capabilities.
::: {.importantblock data-latex=""}
Some of the PWA features won't work with iOS (https://medium.com/@firt/progressive-web-apps-on-ios-are-here-d00430dee3a7), like the install prompt.
:::
::: {.warningblock data-latex=""}
As a reminder, the code examples shown throughout this chapter are gathered in the `{OSUICode}` package accessible here: https://github.com/DivadNojnarg/OSUICode/tree/1d42aa7531705954d3c21921c8bbb10623b20d12, specifically PWA apps are available here: https://github.com/DivadNojnarg/OSUICode/blob/1d42aa7531705954d3c21921c8bbb10623b20d12/inst/shinyMobile/pwa/app.R.
:::
## Introduction
Below, we review one by one the necessary steps to convert a Shiny app to a __PWA__. To get a good idea of what our mission exactly is, we leverage the `Application` tab of the developer tools as shown on Figure \@ref(fig:mobile-pwa-1). Alternatively, you
may use the [Google Lighthouse](https://developers.google.com/web/tools/lighthouse/) utility to provide a general diagnosis to the app, as illustrated on Figure \@ref(fig:mobile-pwa-2). There are many categories like __performance__, __accessibility__. In our case, let's just select the PWA category, check the mobile device radio and click on generate a report.
According to the diagnostic result displayed in Figure \@ref(fig:mobile-pwa-lighthouse-result), we don't meet all requirements; most importantly there is:
- No __manifest__.
- No __service worker__.
- No icons.
- No offline fallback.
```{r mobile-pwa-1, echo=FALSE, fig.cap='Application tab of the developers tools.', out.width='100%', fig.align = "center"}
knitr::include_graphics("images/mobile/mobile-pwa-application.png")
```
```{r mobile-pwa-2, echo=FALSE, fig.cap='Google Lightouse utility.', out.width='100%', fig.align = "center"}
knitr::include_graphics("images/mobile/mobile-pwa-lighthouse.png")
```
```{r mobile-pwa-lighthouse-result, echo=FALSE, fig.cap='Lighthouse audit result.', out.width='100%'}
knitr::include_graphics("images/mobile/mobile-pwa-lighthouse-result.png")
```
## `{charpente}` and PWA tools
`{charpente}` has tools to help design a PWA, particularly the `set_pwa()` function, which does all the previously mentioned steps in only one line of code. There are, however, a few prerequisites:
- The app must __belong to a package__. However, if you followed the previous chapter, this is already the case.
- The function must target the app directory.
Let's create a `inst/examples/pwa-app` sub-folder and the `app.R` file:
```{r, eval=FALSE}
library(shiny)
library(shinyMobile)
ui <- f7_page(
navbar = f7_navbar("PWA App"),
toolbar = f7_toolbar(),
title = "shinyMobile"
)
server <- function(input, output, session) {}
shinyApp(ui, server)
```
Then we set the PWA configuration with `set_pwa()`. Overall, this function generates a `manifest.webmanifest` file, downloads the Google PWA compatibility script, adds a custom dependency pointing to the `manifest.webmanifest` file and a `144x144` icon file, copies a boilerplate `service-worker.js` with its `offline.html` page and optionally registers the service worker (whose code is borrowed from [web.dev](https://web.dev/offline-fallback-page/#registering-the-service-worker)):
```{r, echo=FALSE, results='asis'}
js_code <- "window.addEventListener('load', () => {
if ('serviceWorker' in navigator) {
var pathname = window.location.pathname;
navigator.serviceWorker
.register(
pathname +
'service-worker.js',
{ scope: pathname}
)
.then(function() {
console.log('Service Worker Registered');
});
};
});"
code_chunk_custom(js_code, "js")
```
In the `{shinyMobile}` case, as Framework7 already registers any provided __service
worker__, we don't need that initialization script. Therefore, to skip the creation
of `sw-register.js` and importing it in `main.js`, we should actually call:
```{r, eval=FALSE}
set_pwa("inst/examples/pwa-app", register_service_worker = FALSE)
```
Importantly, this function does not handle icon creation. There are tools such as
[appsco](https://appsco.pe/developer/splash-screens) and
[app-manifest](https://app-manifest.firebaseapp.com), to create
those custom icons and splash screens, if you need to.
In the following, we provide more detail about the mentioned steps.
### Create the manifest
We would like to create a __JSON__ configuration file like this:
```{r, echo=FALSE, results='asis'}
js_code <- '{
"short_name": "My App",
"name": "Super amazing app",
"description": "This app is just mind blowing",
"icons": [
{
"src": "icons/icon.png",
"type": "image/png",
"sizes": "192x192"
}
// ...
],
"start_url": "<APP_URL>",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [
{
"name": "Open toast",
"short_name": "Shortcut",
"description": "Do something",
"url": "<APP_URL>/...",
"icons": [{ "src": "icons/.png", "sizes": "192x192" }]
}
]
}'
code_chunk_custom(js_code, "js")
```
All fields are following the official recommendation provided by Google regarding the PWA, I do not recommend removing any entry, except the `shortcuts`, described later. Since the state of the art may slightly change in the future, you are encouraged to regularly check this [website](https://web.dev/add-manifest/) to get the latest features.
This file has to be accessible by the app, hence best practice is to put it in the `/www` folder,
icon images being hosted in the `/www/icons` sub-directory. The `{charpente}` `create_manifest()` function writes a JSON file at the provided location.
Interestingly the `shortcuts` fields gives the ability to start the app in a specific state, so that end users save time. This feature is only supported by latest Android devices as well as up-to-date Windows 10 computers (no Apple support). In practice, the shortcut url can be processed by `shiny::parseQueryString` on the server side. For instance, if the url contains a query string like `https://domain/path/?foo=1`, we could show a notification:
```{r, eval=FALSE}
observeEvent(session$clientData$url_search, {
query <- parseQueryString(session$clientData$url_search)
req(length(query) > 0)
# Ways of accessing the values
if (as.numeric(query$foo) == 1) {
f7_notif(text = "Plop")
}
})
```
The web manifest and icons have to be included in the `head` before the Google PWA compatibility script:
```{r, echo=FALSE, results='asis'}
html_code <- '<link rel="manifest" href="manifest.webmanifest" />
<!-- include icon also from manifest -->
<link rel="icon" type="image/png"
href="icons/icon-144.png" sizes="144x144" />'
code_chunk_custom(html_code, "html")
```
`set_pwa()` internally calls `create_pwa_dependency()`, which creates an HTML __dependency__ containing all necessary resources:
```{r, eval=FALSE}
#' PWA dependencies utils
#'
#' @description This function attaches PWA manifest and
#' icons to the given tag
#'
#' @param tag Element to attach the dependencies.
#'
#' @importFrom utils packageVersion
#' @importFrom htmltools tagList htmlDependency
#' @export
add_pwa_deps <- function(tag) {
pwa_deps <- htmlDependency(
name = "pwa-utils",
version = packageVersion("shinyMobile"),
src = c(file = "shinyMobile-0.0.0.9000"),
head = "<link rel=\"manifest\"
href=\"manifest.webmanifest\"/>
<link rel=\"icon\" type=\"image/png\"
href=\"icons/icon-144.png\" sizes=\"144x144\" />",
package = "mypkg2",
)
tagList(tag, pwa_deps)
}
```
In practice, since the package already relies on other dependencies like Framework7, we will leverage the `add_dependencies()` function to add all dependencies at once.
::: {.noteblock data-latex=""}
All provided icons must follow the convention `icon-<size_in_px>.png` like
`icon-144.png`, which is the default.
:::
### Google PWA compatibility
As we use the Google PWA compatibility script, we have to include at least one icon
like `<link rel="icon" type="image/png" href="res/icon-128.png" sizes="128x128" />`.
However, we found some discrepancies between the developer tools recommendations and the
PWA compatibility script. Therefore, we recommend following the developer tools prescriptions, that
is, to include at least one icon of size 144x144. All other elements are generated by the script itself,
which is convenient. Indeed, having to handle all possible screen sizes and different OS is particularly
tricky, repetitive, and not interesting.
The HTML dependency is downloaded with `create_dependency("pwacompat", options = charpente_options(bundle = FALSE))`.
Don't forget to update the `add_dependencies()` call in `f7_page()` by including the two new dependencies, that is `pwa` and `pwacompat`:
```{r, eval=FALSE}
f7_page <- function(..., navbar, toolbar, title = NULL,
options = NULL) {
# Config tag (unchanged)
# Body tag (unchanged)
tagList(
tags$head(
# Head content (unchanged)
),
add_dependencies(
body_tag,
deps = c("framework7", "shinyMobile", "pwa", "pwacompat")
)
)
}
```
Calling `devtools::load_all()` and running the app again, you should see the new dependencies
in the `head` (Figure \@ref(fig:mobile-pwa-deps)).
```{r mobile-pwa-deps, echo=FALSE, fig.cap='New PWA dependencies in the head tag.', out.width='100%'}
knitr::include_graphics("images/mobile/mobile-pwa-deps.png")
```
Yet, according to Figure \@ref(fig:mobile-pwa-missing-sw), we still miss the __service worker__,
as shown in the manifest diagnostic. This demonstrates how powerful the developer tools are as the end user is always guided step by step.
```{r mobile-pwa-missing-sw, echo=FALSE, fig.cap='Missing service worker registration.', out.width='100%'}
knitr::include_graphics("images/mobile/mobile-pwa-missing-sw.png")
```
### Service worker and offline page
The second mandatory step to make our app installable is the __service worker__.
We borrowed and modified the code from [web.dev](https://web.dev/offline-fallback-page/). `set_pwa()` copies this code in the the provided app `/www` folder:
```{r, echo=FALSE, results='asis'}
js_code <- "// Incrementing OFFLINE_VERSION will kick off the install
// event and force previously cached resources to be
// updated from the network.
const OFFLINE_VERSION = 1;
const CACHE_NAME = 'offline';
// Customize this with a different URL if needed.
const OFFLINE_URL = 'offline.html';
self.addEventListener('install', (event) => {
// Install logic
});
self.addEventListener('activate', (event) => {
// Activate logic
});
self.addEventListener('fetch', (event) => {
// Fetch logic
});"
code_chunk_custom(js_code, "js")
```
This service worker is composed of three steps, which we succinctly describe below.
#### Installation
During the __installation__ step, the cache is initialized and assets like HTML page (`offline.html`), CSS, JS and images are asynchronously cached. Assets's respective path is taken from the server location, for instance, Framework7 assets are located in `framework7-5.7.14/...` and jQuery assets in `shared/`. Best practice is to look at the developer tools `Source` tab, which provides the right location.
```{r, echo=FALSE, results='asis'}
js_code <- "self.addEventListener('install', (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
await cache.add(
new Request(OFFLINE_URL, { cache: 'reload' })
);
// Cache other assets ...
})()
);
// Force the waiting service worker to become
// the active service worker.
self.skipWaiting();
});"
code_chunk_custom(js_code, "js")
```
#### Activation
This step ensures that the service worker boots. As the service worker boot-up time may be delayed (until 0.5 s), the navigation preload [feature](https://developers.google.com/web/updates/2017/02/navigation-preload) guaranties to have reasonable performances by making network requests in parallel of the booting process. In sum, don't touch this code.
```{r, echo=FALSE, results='asis'}
js_code <- "self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
// Enable navigation preload if it's supported.
// Speeds up
if ('navigationPreload' in self.registration) {
await self.registration.navigationPreload.enable();
}
})()
);
// Tell the active service worker to take control of
// the page immediately.
self.clients.claim();
});"
code_chunk_custom(js_code, "js")
```
#### Fetch
Once active, the service worker intercepts all network requests sent by the client and returns answers according to a predefined strategy. Here we set the "network first" strategy, meaning we always try to return an answer from the network and fall back to the cache if the request failed (for instance, in case of missing internet connection). In the above code, there are two kind of requests: __navigation__, which is related to an HTML page, and other requests corresponding to static assets like CSS or JS. Therefore, we have an `if` and `else` statement to consider those two cases. If you would like to know more about caching strategies please refer to the Google documentation: https://developers.google.com/web/tools/workbox/modules/workbox-strategies.
```{r, echo=FALSE, results='asis'}
js_code <- "// Fix service-worker bug
if (event.request.cache === 'only-if-cached') return;
// We only want to call event.respondWith() if this
// is a navigation request for an HTML page ...
if (event.request.mode === 'navigate') {
// Navigation request
} else {
// Other requests
}"
code_chunk_custom(js_code, "js")
```
Below is the navigation request logic, which is what will be triggered each time an end-user points to your app. As stated above, if the navigation preload is available, we return the preload response. If not, the request is fetched. In case of failure, we fall back to the offline HTML page, cached during the installation step.
```{r, echo=FALSE, results='asis'}
js_code <- "// Navigation request logic
event.respondWith(
(async () => {
try {
// First, try to use the navigation preload response
// if it's supported.
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
// Always try the network first.
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
console.log('Returning offline page instead.', error);
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})()
);"
code_chunk_custom(js_code, "js")
```
All other requests are handled in the `else` statement. The logic remains the same. We first try to get assets from the network and fallback to the cache upon error, that is for instance in offline mode.
```{r, echo=FALSE, results='asis'}
js_code <- "// Other requests
event.respondWith(
(async () => {
try {
// Always try the network first.
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(event.request);
if (cachedResponse) return cachedResponse;
}
})()
);"
code_chunk_custom(js_code, "js")
```
To sum up, this service worker redirects the end user to the offline cached page (`offline.html`) whenever the app is offline, thereby offering a better user experience. The full code is located [here](https://github.com/DivadNojnarg/outstanding-shiny-ui-code/blob/5bc49eab9496696a06da3f62d6aaf8ef468cdad4/inst/shinyMobile/pwa/www/service-worker.js).
::: {.importantblock data-latex=""}
We strongly advise keeping the same file names.
:::
#### Registration
The next step involves the service worker __registration__. Framework7 has a dedicated module in the app configuration. We modify the `config` in `helpers_config.js` before initializing the `app` and run `build_js()` to update the minified file:
```{r, echo=FALSE, results='asis'}
js_code <- "config.serviceWorker = {
path: window.location.pathname + 'service-worker.js',
scope: window.location.pathname
};"
code_chunk_custom(js_code, "js")
```
If the process is successful, you get the result shown in Figure \@ref(fig:mobile-pwa-registered-sw).
```{r mobile-pwa-registered-sw, echo=FALSE, fig.cap='Registered service worker.', out.width='100%'}
knitr::include_graphics("images/mobile/mobile-pwa-registered-sw.png")
```
At this point, you should also check whether the service worker was able to cache files by inspecting the cache storage section, as depicted by Figure \@ref(fig:mobile-pwa-cache).
```{r mobile-pwa-cache, echo=FALSE, fig.cap='Service worker caches static assets to make them available offline.', out.width='100%'}
knitr::include_graphics("images/mobile/mobile-pwa-cache.png")
```
#### Offline fallback
The new PWA standard imposes returning a valid response when the app is __offline__. The offline page is also copied from `{charpente}` and below is a summarized version:
```{r, echo=FALSE, results='asis'}
html_code <- '<!DOCTYPE html>
<html>
<head>
<!-- Required meta tags ... -->
<link
rel="stylesheet"
href="framework7-5.7.14/css/framework7.bundle.min.css">
</head>
<body>
<div id="app">
<!-- App content (navbar, toolbar, page, ...) -->
</div>
<script type="text/javascript" src="shared/jquery.min.js">
</script>
<script
type="text/javascript"
src="framework7-5.7.14/js/framework7.bundle.min.js">
</script>
<!-- Path to your app js -->
<script>
var app = new Framework7({
// ...
});
// ...
</script>
</body>
</html>'
code_chunk_custom(html_code, "html")
```
Notice that jQuery, required for easier DOM interactions, as well as Framework7 CSS and JS assets are cached in the above service worker script, thereby making them
available to `offline.html`. This offline fallback relies on Framework7 for consistency reasons but could be replaced by any other HTML page. The whole code is stored [here](https://github.com/DivadNojnarg/outstanding-shiny-ui-code/blob/5bc49eab9496696a06da3f62d6aaf8ef468cdad4/inst/shinyMobile/pwa/www/offline.html).
Now, let's audit our app again: congrats! It is __installable__ and reliable, although further PWA optimization may be provided.
```{r mobile-pwa-audit-final, echo=FALSE, fig.cap='Installable shinyMobile app.', out.width='100%'}
knitr::include_graphics("images/mobile/mobile-pwa-audit-final.png")
```
::: {.importantblock data-latex=""}
A common source of error is the __browser cache__. It is best practice to regularly empty it. Alternatively, you may run in incognito mode, which does not cache files.
:::
### Disable PWA for the end user
With the above approach, `{shinyMobile}` will always look for a service worker to register.
Particularly, this would raise an error in case no service worker is found on the server.
What if the user doesn't want to create a PWA, let's say for less important applications?
We may add a parameter to `f7_page()`, for instance `allowPWA`, that is either `TRUE` or `FALSE` and store its value in the `body` `data-pwa` attribute.
```{r, eval=FALSE}
f7_page <- function(..., navbar, toolbar, title = NULL,
options = shinyMobile_options,
allowPWA = TRUE) {
# ... unchanged
# create body_tag
body_tag <- tags$body(
`data-pwa` = tolower(allowPWA),
tags$div(
id = "app",
# ... unchanged
)
)
# ... unchanged
}
```
We recover it on the JS side within `helpers_config.js`:
```{r, echo=FALSE, results='asis'}
js_code <- "// check if the app is intended to be a PWA
let isPWA = $('body').attr('data-pwa') === 'true';
if (isPWA) {
config.serviceWorker = {
path: window.location.pathname + 'service-worker.js',
scope: window.location.pathname
};
}
"
code_chunk_custom(js_code, "js")
```
It __only__ creates `config.serviceWorker` if the user specifies `allowPWA = TRUE`.
## Handle the installation
It is a great opportunity to propose a [custom](https://developers.google.com/web/fundamentals/app-install-banners/native) installation experience.
::: {.warningblock data-latex=""}
To be able to install the app, make sure to replace `start_url` by the url
where the app is deployed like `https://dgranjon.shinyapps.io/installable-pwa-app/` for instance.
Missing that step would cause an issue during the service worker registration.
:::
We create a new script with `create_js("helpers_pwa")` and export the `setPWA` function:
```{r, echo=FALSE, results='asis'}
js_code <- "export const setPWA = (app) => {
// Install logic
};"
code_chunk_custom(js_code, "js")
```
Once the installation criteria are met, the web browser raises the __beforeinstallprompt__ event (except
on the iOS platform, which is not compatible yet). We add an event listener inside `setPWA`:
```{r, echo=FALSE, results='asis'}
js_code <- "let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent the mini-infobar from appearing on mobile
e.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = e;
});"
code_chunk_custom(js_code, "js")
```
This code adds an event listener to the window, prevents it from showing at start with `e.preventDefault` and
captures it in an external variable called `deferredPrompt`.
The next step comprises the design of our custom piece of UI, which will trigger the `prompt` install. We can benefit from the rich Framework7 interface and display
a [toast](https://framework7.io/docs/toast.html) containing an install button. The initialization
is fairly simple, following the pattern `app.<COMPONENT>.create(parameters)`:
```{r, echo=FALSE, results='asis'}
js_code <- "// Create custom install UI
let installToast = app.toast.create({
position: 'center',
text: `<button
id=\"install-button\"
class=\"toast-button button color-green\">
Install
</button>`
});"
code_chunk_custom(js_code, "js")
```
We give it an id so as to call it later and edit the __beforeinstallprompt__ event listener to show the toast:
```{r, echo=FALSE, results='asis'}
js_code <- "let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent the mini-infobar from appearing on mobile
e.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = e;
// Show install trigger
installToast.open();
});"
code_chunk_custom(js_code, "js")
```
::: {.noteblock data-latex=""}
With jQuery like `$(window).on('beforeinstallprompt', ...)`, we would capture the event with `e.originalEvent`.
:::
We register a second event listener, which fires on the toast button click. We first close the
toast, call the `prompt` method on the deferred event and log the result:
```{r, echo=FALSE, results='asis'}
js_code <- "app.utils.nextTick(function() {
$('#install-button').on('click', function() {
// close install toast
installToast.close();
if (!deferredPrompt) {
// The deferred prompt isn't available.
return;
}
// Show the install prompt.
deferredPrompt.prompt();
// Log the result
deferredPrompt.userChoice.then((result) => {
console.log('OK', 'userChoice', result);
// Reset the deferred prompt variable, since
// prompt() can only be called once.
deferredPrompt = null;
});
});
}, 500);"
code_chunk_custom(js_code, "js")
```
Inside `init.js`, we add our brand new module:
```{r, echo=FALSE, results='asis'}
js_code <- "import { setConfig } from './helpers_config.js';
import { initTheme } from './helpers_theme.js'
import { setPWA } from './helpers_pwa.js'
// other imports ...
$( document ).ready(function() {
let config = setConfig();
// create app instance
app = new Framework7(config);
// Set theme: dark mode, touch, filled, color, taphold css
initTheme(config, app);
// PWA setup
setPWA(app);
});"
code_chunk_custom(js_code, "js")
```
We run `build_js()` and deploy the app to shinyapps.io (remember, we must serve the app under HTTPS). Figure \@ref(fig:mobile-pwa-installable)
illustrates the install prompt window that appears to install the app. Once installed, the __beforeinstallprompt__
event does not fire anymore and the app may be launched as a standalone app, for instance on macOSX (Figure \@ref(fig:mobile-pwa-installed)).
```{r mobile-pwa-installable, echo=FALSE, fig.cap='Install prompt window.', out.width='50%', fig.align = "center"}
knitr::include_graphics("images/mobile/mobile-pwa-installable.png")
```
```{r mobile-pwa-installed, echo=FALSE, fig.cap='Installed PWA on macOSX.', out.width='50%', fig.align = "center"}
knitr::include_graphics("images/mobile/mobile-pwa-installed.png")
```
In Figure \@ref(fig:mobile-pwa-installed), the blue window color corresponds to the `tags$meta(name = "theme-color", content = "#2196f3")`, passed in the `f7_page()` layout element. To simulate a network issue and validate the offline mode, we selected the developer tools __Network__ tab and changed the dropdown value to offline. As shown in Figure \@ref(fig:mobile-pwa-offline), the offline template shows and pulls static assets from the service worker (the failed network requests are shown in red).
```{r mobile-pwa-offline, echo=FALSE, fig.cap='Offline HTML template.', out.width='100%'}
knitr::include_graphics("images/mobile/mobile-pwa-offline.png")
```
The final product may be run with:
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("shinyMobile/pwa"), "r")
```
This chapter was part of a workshop available [here](https://github.com/RinteRface/rencontresR2021).
<!--
## Workbox
[Workbox](https://developers.google.com/web/tools/workbox) is a more robust alternative
to the approach described above with boilerplate code to enable:
- pre-caching of dependencies like CSS and JS but also images and Google fonts
to improve performances.
- Improve offline experience.
- ...
::: {.importantblock data-latex=""}
This part does not work yet at the time of review submission.
I'll probably remove it ...
:::
-->
## Other resources
The process described above works perfectly for any Shiny template. The reader may also consider other packages like [{shiny.pwa}](https://github.com/pedrocoutinhosilva/shiny.pwa), that creates a PWA-compatible structure at run time, within the app `/www` folder.