Skip to content

Commit

Permalink
Implement service workers and custom offline fallback page (#8594)
Browse files Browse the repository at this point in the history
Co-authored-by: quino <quino@populate.tools>
Co-authored-by: Fernando Blat <fernando@blat.es>
  • Loading branch information
3 people committed Jan 18, 2022
1 parent 49d5ab8 commit 601982c
Show file tree
Hide file tree
Showing 23 changed files with 3,049 additions and 176 deletions.
11 changes: 11 additions & 0 deletions decidim-core/app/controllers/decidim/manifests_controller.rb
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Decidim
# A controller to serve the manifest file for PWA
class ManifestsController < Decidim::ApplicationController
def show
organization_presenter = OrganizationPresenter.new(current_organization)
render layout: false, locals: { organization_params: organization_presenter }, content_type: "application/manifest+json"
end
end
end
7 changes: 7 additions & 0 deletions decidim-core/app/controllers/decidim/offline_controller.rb
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module Decidim
class OfflineController < Decidim::ApplicationController
def show; end
end
end
2 changes: 1 addition & 1 deletion decidim-core/app/packs/entrypoints/decidim_core.js
Expand Up @@ -55,6 +55,7 @@ import "src/decidim/start_conversation_dialog"
import "src/decidim/notifications"
import "src/decidim/identity_selector_dialog"
import "src/decidim/gallery"
import "src/decidim/sw/loader"

// CSS
import "entrypoints/decidim_core.scss"
Expand All @@ -67,4 +68,3 @@ require.context("../images", true)

// This needs to be loaded after confirm dialog to bind properly
Rails.start()

@@ -1,10 +1,14 @@
/* eslint-disable no-console */
/* eslint-disable no-console, no-process-env, no-undef */

$(() => {
if (!console) {
return;
}

if (process.env.NODE_ENV === "development") {
return;
}

const allMessages = window.Decidim.config.get("messages");
if (!allMessages) {
return;
Expand Down
9 changes: 9 additions & 0 deletions decidim-core/app/packs/src/decidim/sw/loader.js
@@ -0,0 +1,9 @@
// check if the browser supports serviceWorker at all
window.addEventListener("load", async () => {
if ("serviceWorker" in navigator) {
// eslint-disable-next-line no-unused-vars
const registration = await navigator.serviceWorker.register("/sw.js", { scope: "/" });
} else {
console.log("Your browser doesn't support service workers 🤷‍♀️");
}
});
55 changes: 55 additions & 0 deletions decidim-core/app/packs/src/decidim/sw/sw.js
@@ -0,0 +1,55 @@
import {
imageCache,
staticResourceCache,
offlineFallback
} from "workbox-recipes";
import { registerRoute } from "workbox-routing";
import { NetworkFirst, NetworkOnly } from "workbox-strategies";
import { CacheableResponsePlugin } from "workbox-cacheable-response";
import { ExpirationPlugin } from "workbox-expiration";


// https://developers.google.com/web/tools/workbox/guides/troubleshoot-and-debug#debugging_workbox
self.__WB_DISABLE_DEV_LOGS = true

/**
* This is a workaround to bypass a webpack compilation error
*
* The InjectManifest function requires the __WB_MANIFEST somewhere in this file,
* however, we cannot add precacheAndRoute as the issue suggests,
* as the other workbox-recipes won't work properly
*
* See more: https://github.com/GoogleChrome/workbox/issues/2519#issuecomment-634164566
*/
// eslint-disable-next-line no-unused-vars
const dummy = self.__WB_MANIFEST;

// avoid caching admin or users paths
registerRoute(
({ url }) => ["/admin/", "/users/"].some((path) => url.pathname.startsWith(path)),
new NetworkOnly()
);

// https://developers.google.com/web/tools/workbox/modules/workbox-recipes#pattern_3
registerRoute(
({ request }) => request.mode === "navigate",
new NetworkFirst({
networkTimeoutSeconds: 3,
cacheName: "pages",
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200]
}),
new ExpirationPlugin({
maxAgeSeconds: 60 * 60
})
]
}),
);

// common recipes
staticResourceCache();

imageCache();

offlineFallback({ pageFallback: "/offline" });
10 changes: 10 additions & 0 deletions decidim-core/app/presenters/decidim/organization_presenter.rb
@@ -0,0 +1,10 @@
# frozen_string_literal: true

module Decidim
# A general presenter to render organization logic to build a manifest
class OrganizationPresenter < SimpleDelegator
def translated_description
ActionView::Base.full_sanitizer.sanitize(translated_attribute(description))
end
end
end
5 changes: 5 additions & 0 deletions decidim-core/app/views/decidim/manifests/show.json.erb
@@ -0,0 +1,5 @@
{
"name": "<%= organization_params.name %>",
"lang": "<%= organization_params.default_locale %>",
"description": "<%= organization_params.translated_description %>"
}
9 changes: 9 additions & 0 deletions decidim-core/app/views/decidim/offline/show.html.erb
@@ -0,0 +1,9 @@
<div id="offline-fallback-html" style="text-align:center;padding:1rem">
<svg xmlns="http://www.w3.org/2000/svg" width="5rem" height="r5em" viewBox="0 0 25 25">
<path
d="m20.293 4-1.477 1.477A4.473 4.473 0 0 0 11.17 7.33 4.362 4.362 0 0 0 9.5 7a4.486 4.486 0 0 0-4.23 3.01 4.49 4.49 0 0 0 .042 8.971L4 20.293l.707.707L21 4.707zM19.99 8.21a3.936 3.936 0 0 0-.16-.92L8.12 19H18.5a5.497 5.497 0 0 0 1.49-10.79z"
style="fill:#1a181d" />
</svg>
<h3><%= t(".message") %></h3>
<button class="button large button--sc mt-s" onclick="location.reload()"><%= t(".retry") %></button>
</div>
Expand Up @@ -12,6 +12,7 @@
<%= render partial: "layouts/decidim/cookie_warning" %>
<%= render partial: "layouts/decidim/omnipresent_banner" %>
<%= render partial: "layouts/decidim/timeout_modal" %>
<%= render partial: "layouts/decidim/offline_banner" %>
<%= render "layouts/decidim/wrapper" do %>
<%= yield %>
Expand Down
3 changes: 3 additions & 0 deletions decidim-core/app/views/layouts/decidim/_head.html.erb
Expand Up @@ -22,6 +22,9 @@
<% end %>
<%= favicon %>

<link rel="manifest" href="/manifest.webmanifest">

<%= stylesheet_pack_tag "decidim_core", media: "all" %>
<%= invisible_captcha_styles %>
<%= organization_colors %>
Expand Down
11 changes: 11 additions & 0 deletions decidim-core/app/views/layouts/decidim/_offline_banner.html.erb
@@ -0,0 +1,11 @@
<div class="flash callout primary small js-offline-message" style="display: none">
<%= t(".cache_version_page") %>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
// show the banner if it's offline AND the offline-fallback is not displaying
if (!navigator.onLine && !document.querySelector("#offline-fallback-html")) {
document.querySelector(".js-offline-message").style.display = "block"
}
})
</script>
6 changes: 6 additions & 0 deletions decidim-core/config/locales/en.yml
Expand Up @@ -1101,6 +1101,10 @@ en:
update:
error: There was a problem updating your notifications settings.
success: Your notifications settings were successfully updated.
offline:
show:
message: It looks like you're currently offline. Please, try again later.
retry: Retry
open_data:
not_available_yet: The Open Data files are not yet available, please try again in a few minutes.
own_user_groups:
Expand Down Expand Up @@ -1642,6 +1646,8 @@ en:
notifications_dashboard:
mark_all_as_read: Mark all as read
mark_as_read: Mark as read
offline_banner:
cache_version_page: Oooops! Your network is offline. This is a previously cached version of the page you're visiting, perhaps the content is not up to date.
social_media_links:
facebook: "%{organization} at Facebook"
github: "%{organization} at GitHub"
Expand Down
4 changes: 4 additions & 0 deletions decidim-core/config/routes.rb
Expand Up @@ -3,6 +3,8 @@
Decidim::Core::Engine.routes.draw do
mount Decidim::Api::Engine => "/api"

get "/offline", to: "offline#show"

devise_for :users,
class_name: "Decidim::User",
module: :devise,
Expand All @@ -29,6 +31,8 @@
post "omniauth_registrations" => "devise/omniauth_registrations#create"
end

resource :manifest, only: [:show]

resource :locale, only: [:create]

Decidim.participatory_space_manifests.each do |manifest|
Expand Down
Expand Up @@ -103,6 +103,7 @@ def snippets
final_html = html_document
Rails.application.routes.draw do
get "test_dynamic_map", to: ->(_) { [200, {}, [final_html]] }
get "offline", to: ->(_) { [200, {}, [""]] }
end

visit "/test_dynamic_map"
Expand Down
18 changes: 15 additions & 3 deletions decidim-core/lib/decidim/webpacker/webpack/custom.js
@@ -1,7 +1,6 @@
/* eslint-disable */

const path = require("path");
const { config } = require("@rails/webpacker");
const { InjectManifest } = require("workbox-webpack-plugin");

module.exports = {
module: {
Expand Down Expand Up @@ -93,5 +92,18 @@ module.exports = {
optimization: {
runtimeChunk: false
},
entry: config.entrypoints
entry: config.entrypoints,
plugins: [
new InjectManifest({
swSrc: "src/decidim/sw/sw.js",

/**
* NOTE:
* @rails/webpacker outputs to '/packs',
* in order to make the SW run properly
* they must be put at the project's root folder '/'
*/
swDest: "../sw.js"
})
]
}
28 changes: 28 additions & 0 deletions decidim-core/spec/controllers/manifests_spec.rb
@@ -0,0 +1,28 @@
# frozen_string_literal: true

require "spec_helper"

module Decidim
describe ManifestsController, type: :controller do
routes { Decidim::Core::Engine.routes }

let(:organization) { create(:organization) }

before do
request.env["decidim.current_organization"] = organization
end

describe "GET /manifest.json" do
render_views

it "returns the manifest" do
get :show, format: :json

expect(response).to be_successful

manifest = JSON.parse(response.body)
expect(manifest["name"]).to eq(organization.name)
end
end
end
end
23 changes: 23 additions & 0 deletions decidim-core/spec/controllers/offline_controller_spec.rb
@@ -0,0 +1,23 @@
# frozen_string_literal: true

require "spec_helper"

module Decidim
describe OfflineController, type: :controller do
routes { Decidim::Core::Engine.routes }

let(:organization) { create(:organization) }

before do
request.env["decidim.current_organization"] = organization
end

describe "GET /offline" do
it "returns the offline content" do
get :show

expect(response).to be_successful
end
end
end
end

0 comments on commit 601982c

Please sign in to comment.