Context
I'm using parcel-plugin-sw-precache which wraps around sw-precache to make it work with Parcel.js. Everything was working as expected, and I have been testing my offline app.
Problem
I added react-pdf.js into my project, one of the dependencies for this library doesn't get added into the service worker when it is generated by the sw-precache. I know this because the file "pdf.worker.entry.7ce4fb6a.js" gives a 304 error when I switch to offline mode.
What I have tried
I'm trying to add the file manually to the package.json parcel-plugin-sw-precache config using this code:
"sw-precache": {
"maximumFileSizeToCacheInBytes": 10485760,
"staticFileGlobs": [
"/pdf.worker.entry.7ce4fb6a.js"
]
},
I'm not sure if the file path should be relative to package.json or relative the generated service worker. In anycase, the manually specified file doesn't get added to generate services worker as I would expect. As seen below.
self.__precacheManifest = [{
"url": "index.html",
"revision": "ac5ace7a43a0fef7ae65fd3119987d1f"
}, {
"url": "castly.e31bb0bc.css",
"revision": "657409f7159cb857b9409c44a15e653f"
}, {
"url": "castly.e31bb0bc.js",
"revision": "018d4664d809ec19d167421b359823ad"
}, {
"url": "/",
"revision": "af5513bb330deae3098ab289d69a40c7"
}]
The question
If the sw-precache or parcel-plugin-sw-precache seem to be missing some files, how can I make sure they get added to the generated service worker?
In my exploration for an answer. I gave up on using parcel-plugin-sw-precache and instead I switched to using workbox. If you are interested in creating an offline app with Parcel.js. Then I recommend Workbox as it is the next generation of sw-precache.
There is how I got it working:
Learning
Learn what Workbox is and what is does with this code lab.
Implimenting
1) Install the Workbox CLI globally.
2) create a placeholding service worker in the root directory. e.g. sw_shell.js
- The shell is a holding file. The Workbox wizard will pick it up and generate a
new sw.js file automatically.
3) Add to the sw_config.js the following code:
importScripts("https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js");
if (workbox) {
workbox.skipWaiting();
workbox.clientsClaim();
workbox.precaching.suppressWarnings();
// The next line came from the code lab
workbox.precaching.precacheAndRoute([]);
workbox.routing.registerNavigationRoute("/index.html");
} else {
console.log(`Boo! Workbox didn't load š¬`);
}
4) Run this code from a command line opened in your project's root directory.
workbox wizard --injectManifest
5) Follow the steps in the wizard. For dev purposes point the "root web app" to your parcel dist folder. Workbox does it's magic and picks up those files to be hashed into a new sw.js file.
6) The wizard will ask for your existing sw.js file. In my case I use the sw_shell.js.
a:Workbox picks up the sw_shell.js.
c:Generates as new sw.js file in a location specfied when running the wizard, and injects the files to run offline.
In my case I let the new sw.js generate in my root folder because Parcel picks it up automatically as per the script in my index.js.
'use strict';
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('sw.js').then(function(reg) {
console.log('Worker registration started')
reg.onupdatefound = function() {
console.log('update found')
var installingWorker = reg.installing;
installingWorker.onstatechange = function() {
console.log('installing worker')
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
console.log('New or updated content is available.');
} else {
console.log('Content is now available offline!');
}
break;
case 'redundant':
console.error('The installing service worker became redundant.');
break;
}
};
};
}).catch(function(e) {
console.error('Error during service worker registration:', e);
});
});
}
7) Add workbox injectManifest to your package.json to make sure Workbox picks up any changes to your files:
"scripts": {
"start": "parcel index.html workbox injectManifest"
}
Let me know if you want to know more about this. There is a video here that helped me a little bit also.
Related
I'm having a lot of trouble with serving a apple-app-site-association file in ReactJS project.
I've searched a lot of GitHub issues pages (for ReactJS, Gatsby, and Angular), forums, and a lot of online communities, and it seems that I can't find a solution for this.
What I've tried is:
Adding the file into public/.well-known folder.
Adding a separate route via react-router on path "/apple-app-site-association" and returning an tag with file
Adding <link rel="apple-app-site-association" href="%PUBLIC_URL%/.well-known/apple-app-site-association"> into public/index.html
Testing through the "aasa-validator" returns:
Your file's 'content-type' header was not found or was not recognized.
Keep in mind that:
The apple-app-site-association JSON file must not have a .json file extension.
It has to be on "/apple-app-site-association" or "./well-known/apple-app-site-association" links on the website.
I can't use a redirect to another page/link.
Thanks in advance!
Ps. If it helps, I'm using a Heroku for deployment.
For NextJs
As discussed here, it is possible to server apple-app-site-association from NextJs.
Next.js uses the extension to automatically detect the content-type of the file. Since the file is extensionless, it's served as binary contentāthis should be compatible with Apple's verification.
If Apple requires specific headers, you can overwrite this type:
// next.config.js
module.exports = {
experimental: {
headers() {
return [
{
source: "/.well-known/apple-app-site-association",
headers: [{ key: "content-type", value: "application/json" }]
}
];
}
}
};
You can serve the file using React Router.
Put this in your index.js or App.js:
const reload = () => window.location.reload();
<Route path="/apple-app-site-association" onEnter={reload} />
More details here:
https://brainbank.cc/jamie/lessons/programming-react/serve-static-file-txt-html-via-react-router
several days have passed and I didn't find an answer or solution. (And I really hate to see an unanswered stackoverflow question when I'm searching for a solution)
I've talked to a lot of people and pretty much everyone said this is impossible. Reason for that is that ReactJS is unable to return JSON type of response upon client sending a GET request.
So we transfered the file onto the back-end side which is working perfectly.
tl;dr It's not possible.
For CRA:
The file apple-app-site-association should be added to the public folder or public/.well-known folder. This alone won't make it work. Because it is served with a content-type of application/octet-stream. To fix it, we should use a solution provided by the hosting provider.
For Firebase:
I am using Firebase for hosting my CRA app. So, for firebase, it is possible to specify the response headers for the path. The following gist shows how to do this. Make sure that the appAssociation should also be set to prevent firebase dynamic links from serving some other AASA file.
firebase.json config
{
"hosting": {
"public": "public",
"headers": [
{
"source": "/.well-known/apple-app-site-association",
"headers": [
{
"key": "Content-Type",
"value": "application/json"
}
]
}
],
"appAssociation": "NONE"
}
}
If you're using Cloudfront in AWS you can create a custom Response Headers Policy:
We serve our CRA app using the npm serve CLI package. This is what we did to resolve the issue of serving our extension-less apple-app-site-association file.
The fix for this is to create the apple-app-site-association as apple-app-site-association.json inside your public/.well-known directory and then create a rewrite rule inside of a file in your public directory called serve.json. The contents of that file should look like this.
{
"rewrites": [
{
"source": "!.well-known/**",
"destination": "index.html"
},
{
"source": ".well-known/apple-app-site-association",
"destination": ".well-known/apple-app-site-association.json"
}
]
}
Then when you start your static file server, you don't use the -s or --single argument. You start it using the following command.
serve -c serve.json
The first item will send all requests that don't exist inside of .well-known to index.html, and the second rewrite will send requests for that specific URL to the apple-app-site-association.json file and assign a content-type: application/json header to it. All other files with file extensions should be served as normal, ignoring any rewrite rules.
Disclaimer: This is only applicable if you have an Nginx implementation.
I got stuck on this and couldn't figure out a way to add the apple site association file to the react code in any way.
However, I was able to solve the issue by looking outside the react box and inside my server configuration box i.e. Nginx.
Here are the steps:
In the server store the AASA or apple-app-site-association.json file in a particular location - I chose to store it at (had to create two certifications and ios directories)
/var/www/certificates/ios/
Now go to the sites-available folder
cd /etc/nginx/sites-available/
Here, you will find the file with your web app's Nginx configuration information.
Let's say, the name of my file was example-web-app, so open the file using vim with sudo privileges:
sudo vim mayank-web-app
Now inside this file, you need to find
location / {
Just above this line paste this code block
location /apple-app-site-association {
alias /var/www/certificates/ios/;
index apple-app-site-association.json
autoindex on; }
Now save the file and exit, by pressing esc and typing
wq!
Now check if the changes are valid by typing
sudo nginx -t
You should get this as output
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
After this you will have to go to the sites-enabled directory
cd ../sites-enabled
9. Update the symlink here
sudo ln -s /etc/nginx/sites-available/example-web-app .
Now reload the nginx service
sudo service nginx reload
Now you can check this with two methods:
a. https://branch.io/resources/aasa-validator/#resultsbox.
b. https://example.com/apple-app-site-association
I has this problem using react/redux and Next.js
this is how I solved the issue
in my /static folder I put my apple-app-site-association
{
"webcredentials": {
"apps": ["12345431.com.app"]
},
"applinks": {
"apps": [],
"details": [
{
"appID": "12345431.com.app",
"paths": [ "*"]
}
}
]
}
}
I created a component called Apple AppleAssociation.jsx in that file I have
import React from 'react'
function AppleAssociation() {
return (
//path to apple app site association file
<>
<link rel="apple-app-site-association file" href="/static/apple-app-site-association" />
</>
)
}
export default AppleAssociation
and in my _app.jsx file I import that into my head
import React from 'react'
import Head from 'next/head'
import withRedux from 'next-redux-wrapper'
import App, { Container as NextContainer } from 'next/app'
import { Provider as ReduxProvider } from 'react-redux'
import {
AppleAssociation,
} from '../components/head'
import makeStore from '../lib/store'
class CustomApp extends App {
static async getInitialProps({ Component}) {
let pageProps = {}
return {
pageProps,
}
}
componentDidMount() {
}
render() {
const { Component, pageProps, session, store } = this.props
return (
<NextContainer>
<Head>
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimal-ui"
/>
<AppleAssociation />
</Head>
<ReduxProvider store={store}>
<NextSeo config={seo} />
<Component {...pageProps} />
</ReduxProvider>
</NextContainer>
)
}
}
export default withRedux(makeStore)(CustomApp)
you can test this by using https://branch.io/resources/aasa-validator/#resultsbox
If you have any other redirects / rewrite rules make sure the apple-app-site-association re-write is placed before others.
For example this is with AWS Amplify for React JS:
[
{
"source": "/.well-known/apple-app-site-association",
"target": "/.well-known/apple-app-site-association.json",
"status": "200",
"condition": null
},
{
"source": "</^[^.]+$|\\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|ttf|map|json)$)([^.]+$)/>",
"target": "/index.html",
"status": "200",
"condition": null
}
]
with the apple-app-site-association file placed as a JSON file here:
/public/.well-known/apple-app-site-association.json
I have read all I can on service workers and PWA's and I can't figure out what I am doing wrong, I think it must be my virtual directory on IIS or something but I have tried changing the scope of the service worker registration, changed the start_url in the manifest, etc., and nothing seems to work. The error I get is in the dev tools on the Application->Manifest tab:
"Site cannot be installed: no matching service worker detected. You may need to reload the page, or check that the service worker for the current page also controls the start URL from the manifest"
I have created a virgin application on IIS called TestWeb and can browse to a 'hello world' page at
https://localhost/TestWeb/
All the js files and the manifest are in the root of the site. In the header of the page, I have:
<link rel="manifest" href="Manifest.json">
and that points to a json file in the root of the site like so:
{
"dir": "ltr",
"lang": "en",
"name": "xxxx",
"scope": "/",
"display": "standalone",
"start_url": ".",
"short_name": "xxxx",
"theme_color": "#ffffff",
"description": "",
"orientation": "any",
"background_color": "transparent",
"related_applications": [],
"prefer_related_applications": false,
"icons": [
{
"src": "Images/AppIcons/windows10/Square71x71Logo.scale-400.png",
"sizes": "284x284"
},
etc...
For the scope of the manifest, I have tried "./", "/", "/TestWeb/", "/TestWeb", etc.. When the document is ready, I call this function:
if ("serviceWorker" in navigator) {
if (navigator.serviceWorker.controller) {
console.log("Active service worker found, no need to register");
} else {
// Register the service worker
navigator.serviceWorker
.register("ServiceWorkerRegister.js", {
scope: "./"
})
.then(function (reg) {
console.log("Service worker has been registered for scope: " + reg.scope);
});
//.catch(err => console.log(err));
}
}
}
For the scope here, I have tried "./", "/", "/TestWeb/", "/TestWeb", etc..
The service worker successfully activates and is running but there is no 'add' button on the browser, and going to the Application->Manfiest in dev tools, I get the error saying no matching serivce worker. I have tried refreshing multiple times.
I am stumped, does anyone know of something else to try?
To get installation prompt your source code must have this (ServiceWorkerRegister.js) file with at least an event listener for fetch, and other installable requirements too.
I had already connected the free custom page.link subdomain and this works great and as expected. It opens my app and redirects users to the specific pages. However, I recently got a custom domain from google domains and wanted to connect it. the domain is https://gifte.app
I connected it to my projects associated domains : applinks:gifte.app and another link : applinks:gifte.app/applinks
In the firebase console I also added https://gifte.app and added https://gifte.app/applinks
when adding the first one, it told me to add some lines to the DNS settings which I did. And when adding this one https://gifte.app/applinks firebase told me to add
"appAssociation": "AUTO",
"rewrites": [ { "source": "/applink/**", "dynamicLinks": true } ]
to firebase.json which I added and is in my project directory.
when creating a dynamic link. I used
DynamicLinkComponents.init(link: linkParameter, domainURIPrefix: "https://gifte.app/applinks")
and created the link the way shown in the firebase tutorial videos. Running the project and trying to create the dynamic link. I get this error
Error Domain=com.firebase.durabledeeplink Code=0 "Your project does not own Dynamic Links domain: https://gifte.app
however when using https://gifte.page.link I do not get the error and it works fine.
When using https://gifte.app instead as the domainURIPrefix, the dynamic link is generated. however when it is clicked in the notes app. It doesnt redirect to the app. It instead redirects me to a 404 ERROR
This file does not exist and there was no index.html found in the
current directory or 404.html in the root directory.
Why am I seeing this? You may have deployed the wrong directory for
your application. Check your firebase.json and make sure the public
directory is pointing to a directory that contains an index.html file.
You can also add a 404.html in the root of your site to replace this
page with a custom error page
In summary of creating the links
https://gifte.page.link as domainURIPrefix works
https://gifte.app as domainURIPrefix creates url but doesn't redirect to app when clicked in notes. Instead to firebase error screen
https://gifte.app/applinks as domainURIPrefix does not work and gives error in app
So my question is, how can I get the custom dynamic links working the same as the free page.link subdomain? I don't get why adding the custom domain has caused so many problems and why using https://gifte.app/applinks gives an error saying that I do not own the domain.
my firebase.json file
{
"functions": {
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint"
],
"source": "functions"
},
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"appAssociation": "AUTO",
"rewrites": [ { "source": "/applink/**", "dynamicLinks": true } ]
}
}
Does anyone have a solution? Thank you.
We have an electron crypto app that signs transactions (among other things).
We want other websites to have the ability to have a button that opens that electron app, pre-filled with some params (the transaction information).
flow is:
user clicks "make transaction" on some-crypto-site.com
electron app opens up with pre-filled params
user clicks "sign transaction" in electron app
electron app does stuff behind the scenes
electron app closes and sends a message to some-crypto-site.com
This could be done at runtime, or install time.
What I tried (linux, chrome)
calling app.setAsDefaultProtocolClient with the code of this gist, which is basically:
app.setAsDefaultProtocolClient("my-app")
But after I put my-app://foo?bar=baz in chrome browser, I get the following popup, and pressing open-xdg does nothing (other than dismissing the popup)
I looked into
Electron protocol api which seems to handle in-app protocols only
webtorrent .desktop file This might be the way to go, I'm just not sure how to go about it.
Maybe there's a way to do so at install time through electron builder?
Thanks in advance for the help, I have no idea how to proceed here!
Resources that might be useful
github repo with mac+window example
github comment for linux
github comment for linux 2
SO answer for all 3 OSs
SO windows answer
npm package for windows registery
SO mac answer
SO linux answer
microsoft docs for windows
windows article
github comment for windows
github comment for mac
info.plst for mac
old repo for mac and win
Since this may be relevant to what Iām doing at work, I decided to give it a go.
Iāve only tested this on OSX though!
I looked at the documentation for app.setAsDefaultProtocolClient and it says this:
Note: On macOS, you can only register protocols that have been added to your app's info.plist, which can not be modified at runtime. You can however change the file with a simple text editor or script during build time. Please refer to Apple's documentation for details.
These protocols can be defined when packaging your app with electron-builder. See build:
{
"name": "foobar",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"dist": "electron-builder"
},
"devDependencies": {
"electron": "^3.0.7",
"electron-builder": "^20.38.2"
},
"dependencies": {},
"build": {
"appId": "foobar.id",
"mac": {
"category": "foo.bar.category"
},
"protocols": {
"name": "foobar-protocol",
"schemes": [
"foobar"
]
}
}
}
In your main thread:
const {app, BrowserWindow} = require('electron');
let mainWindow;
function createWindow () {
mainWindow = new BrowserWindow({width: 800, height: 600})
mainWindow.loadFile('index.html');
}
app.on('ready', createWindow);
var link;
// This will catch clicks on links such as open in foobar
app.on('open-url', function (event, data) {
event.preventDefault();
link = data;
});
app.setAsDefaultProtocolClient('foobar');
// Export so you can access it from the renderer thread
module.exports.getLink = () => link;
In your renderer thread:
Notice the use of the remote API to access the getLink function exported in the main thread
<!DOCTYPE html>
<html>
<body>
<p>Received this data <input id="data"/></p>
<script>
const {getLink} = require('electron').remote.require('./main.js');
document.querySelector('#data').value = getLink();
</script>
</body>
</html>
Example
open in foobar
This also allows you to launch from the command line:
open "foobar://xyz=1"
How do you get back to the original caller?
I suppose that when you launch the app you could include the caller url:
<a href="foobar://abc=1&caller=example.comā>open in foobar</a>
When your electron app finishes processing data, it would simply ping back that url
Credits
Most of my findings are based on:
From this GitHub issue
And the excellent work from #oikonomopo
All little bit different from above.
open-url fires before the ready event so you can store it in a variable and use within the widow did-finish-load.
let link;
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 720,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
mainWindow.openDevTools();
mainWindow.setContentProtection(true);
mainWindow.loadFile('index.html');
mainWindow.webContents.on("did-finish-load", function() {
mainWindow.webContents.send('link', link);
});
}
app.on('ready', createWindow);
// This will catch clicks on links such as open in foobar
app.on('open-url', function(event, url) {
link = url;
if (mainWindow?.webContents) {
mainWindow.webContents.send('link', link);
}
});
app.setAsDefaultProtocolClient('protocols');
You can then use the value in your render html like this.
<!DOCTYPE html>
<html>
<head></head>
<body>
<script>
const ipc = require("electron").ipcRenderer;
ipc.on("link", function (event, url) {
console.log(url);
console.log(parseQuery(decodeURI(url)));
});
function parseQuery(queryString) {
queryString = queryString.substring(queryString.indexOf("://") + 3);
var query = {};
var pairs = (queryString[0] === "?" ? queryString.substr(1) : queryString).split("&");
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split("=");
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
}
return query;
}
</script>
</body>
</html>
With the new firefox webextensions: Is there a way to save the current page (or a part of it) as html (or text) to disk? If not, how are the chances, such an API will be implemented in the future?
I didn't find any suitable API and appreciate any help.
Regards
There are probably several ways to do this. The following will get you started. It saves the webpage in the currently focused tab in the active window to the browser's default downloads path. The file name is set to 'samplePage.html' (you can change that by modifying the filename value in the downloads.download() options; or you can remove that field entirely and leave it to the default naming).
You will need to store icon images in your webextension package for the user to be able to click on. Also, be sure to navigate to a webpage you want to save before you try to use the webextension; webextensions are not active on the Firefox about:debugging page.
manifest:
{
"name": "SavePage",
"version": "1.0",
"description": "Clicking browser icon saves page html",
"manifest_version": 2,
"icons": {
"48": "icons/clickme-48.png"
},
"permissions": [
"tabs",
"activeTab",
"downloads"
],
"browser_action": {
"default_icon": "icons/clickme-32.png"
},
"background": {
"scripts": ["background.js"]
}
}
background script:
/* BACKGROUND SCRIPT
Clicking on browser toolbar button saves the webpage in the
current tab to the browser's default downloads path with a
filename of "samplePage.html". The "tabs" and "downloads"
permissions are required.
*/
browser.browserAction.onClicked.addListener((tab) => {
var currentUrl = tab.url;
function onStartedDownload(id) {
console.log(`Started to download: ${id}`);
}
function onFailed(error) {
console.log(`Something stinks: ${error}`);
}
var startDownload = browser.downloads.download({
url : currentUrl,
filename: 'samplePage.html'
});
startDownload.then(onStartedDownload, onFailed);
});
An alternative approach might be to try to save the webpage to local storage rather than to disk. I have not explored that option.
These pages may be helpful:
downloads.download()
browserAction.onClicked
There may be security risks in giving a webextension these permissions. You will have to weigh the risks for your own usage pattern.