Share content with other apps

Sharing content between apps was made popular by mobile devices where manipulating files or copying content is less intuitive than on desktop operating systems. For example, on mobile, it's common to share an image with a friend by sending a text message. But sharing content isn't reserved to mobile devices; it's possible to share between apps on Windows, too.

There are two directions to sharing content, and both directions can be handled by Progressive Web Apps (PWAs):

Direction Description
Sharing content To share content, a PWA generates content (such as text, links, or files) and hands off the shared content to the operating system. The operating system lets the user decide which app they want to use to receive that content.
Receiving shared content To receive content, a PWA acts as a content target. The PWA is registered with the operating system as a content-sharing target.

PWAs that register as share targets feel natively integrated into the OS, and are more engaging to users.

Sharing content

PWAs can use the Web Share API to trigger displaying the operating system share dialog.

Note

Web sharing only works on sites served over HTTPS (which is the case for PWAs), and can only be invoked in response to a user action.

To share content such as links, text, or files, use the navigator.share function, as shown below. The navigator.share function accepts an object that should have at least one of the following properties:

  • title: a short title for the shared content.
  • text: a longer description for the shared content.
  • url: the address of a resource to be shared.
  • files: an array of files to be shared.
function shareSomeContent(title, text, url) {
  if (!navigator.share) {
    return;
  }

  navigator.share({title, text, url}).then(() => {
    console.log('The content was shared successfully');
  }).catch(error => {
    console.error('Error sharing the content', error);
  });
}

In the above code, we first check whether the browser supports Web sharing, by testing if navigator.share is defined. The navigator.share function returns a Promise object that resolves when sharing is successful, and rejects when an error occurred.

Because a Promise is used here, the above code can be rewritten as an async function, as follows:

async function shareSomeContent(title, text, url) {
  if (!navigator.share) {
    return;
  }

  try {
    await navigator.share({title, text, url});
    console.log('The content was shared successfully');
  } catch (e) {
    console.error('Error sharing the content', e);
  }
}

On Windows, the above code will trigger the share dialog, allowing the user to pick an app to receive the shared content. The share dialog is shown below:

The share dialog on Windows.

Once the user has selected an app to receive the shared content, it is up to this app to handle it any way it chooses. For example, an email app might use the title as the email subject, and use the text as the email body.

Sharing files

The navigator.share function also accepts a files array to share files with other apps.

It is important to test whether sharing files is supported by the browser before sharing them. To check whether sharing files is supported, use the navigator.canShare function:

function shareSomeFiles(files) {
  if (navigator.canShare && navigator.canShare({files})) {
    console.log('Sharing files is supported');
  } else {
    console.error('Sharing files is not supported');
  }
}

The files sharing object member must be an array of File objects. Learn more about the File interface.

One way to construct File objects is:

  1. First, use the fetch API to request a resource.
  2. Then, use the returned response to create a new File.

That approach is shown below.

async function getImageFileFromURL(imageURL, title) {
  const response = await fetch(imageURL);
  const blob = await response.blob();
  return new File([blob], title, {type: blob.type});
}

In the above code:

  1. The getImageFileFromURL function fetches an image by using a URL.
  2. The response.blob() function converts the image into a binary large object (BLOB).
  3. The code creates a File object by using the BLOB.

Demo of sharing content

DevTools Tips is a PWA that uses the navigator.share function to share text and links.

To test the feature:

  1. Go to DevTools Tips.
  2. Select a tip.
  3. Click Share tip.

The Windows Share dialog is displayed. The user picks an app in which to share the content:

The Windows Share dialog allows the user to pick which app should receive the shared content.

You can find the source code on GitHub. The app uses the Web Share API in the share.js source file.

Receiving shared content

By using the Web Share Target API, a PWA can register to be displayed as an app in the system share dialog. The PWA can then use the Web Share Target API to handle shared content coming in from other apps.

Note

Only installed PWAs can register as share targets.

Register as a target

To receive shared content, the first thing to do is register your PWA as a share target. To register, use the share_target manifest member. Upon installation of your app, the operating system uses the share_target member to include your app in the system share dialog. The operating system knows what to do when your app is picked by the user, to share the content.

The share_target member must contain the necessary information for the system to pass the shared content to your app. Consider the following manifest code:

{
    "share_target": {
        "action": "/handle-shared-content/",
        "method": "GET",
        "params": {
            "title": "title",
            "text": "text",
            "url": "url",
        }
    }
}

When your app is selected by the user as the target for shared content, the PWA is launched. A GET HTTP request is made to the URL specified by the action property. The shared data is passed as the title, text, and url query parameters. The following request is made: /handle-shared-content/?title=shared title&text=shared text&url=shared url.

If you have existing code that uses other query parameter names, you can map the default title, text, and url query parameters to your other names. In the following example, the title, text, and url query parameters are mapped to subject, body, and address:

{
    "share_target": {
        "action": "/handle-shared-content/",
        "method": "GET",
        "params": {
            "title": "subject",
            "text": "body",
            "url": "address",
        }
    }
}

Handle GET shared data

To handle the data shared over the GET request in your PWA code, use the URL constructor to extract the query parameters:

window.addEventListener('DOMContentLoaded', () => {
    console url = new URL(window.location);

    const sharedTitle = url.searchParams.get('title');
    const sharedText = url.searchParams.get('text');
    const sharedUrl = url.searchParams.get('url');
});

Handle POST shared data

If the shared data is meant to change your app in any way, such as by updating some of the content that's stored in the app, you must use the POST method and define an encoding type with enctype:

{
    "share_target": {
        "action": "/post-shared-content",
        "method": "POST",
        "enctype": "multipart/form-data",
        "params": {
            "title": "title",
            "text": "text",
            "url": "url",
        }
    }
}

The POST HTTP request contains the shared data, encoded as multipart/form-data. You can access this data on your HTTP server by using server-side code, but this won't work when the user is offline. To provide a better experience, you can access the data in the service worker, using a fetch event listener, as follows:

self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    if (event.request.method === 'POST' && url.pathname === '/post-shared-content') {
        event.respondWith((async () => {
            const data = await event.request.formData();

            const title = data.get('title');
            const text = data.get('text');
            const url = data.get('url');

            // Do something with the shared data here.

            return Response.redirect('/content-shared-success', 303);
        })());
    }
});

In the above code:

  1. The service worker intercepts the POST request.

  2. Uses the data in some way (such as to store the content locally).

  3. Redirects the user to a success page. This way, the app can work even if the network is down. The app can choose to only store the content locally, or can send the content to the server later, when connectivity is restored (such as by using Background Sync).

Handle shared files

Apps can also handle shared files. To handle files in your PWA, you must use the POST method and the multipart/form-data encoding type. Additionally, you must declare the types of files that your app can handle.

{
    "share_target": {
        "action": "/store-code-snippet",
        "method": "POST",
        "enctype": "multipart/form-data",
        "params": {
            "title": "title",
            "files": [
                {
                    "name": "textFile",
                    "accept": ["text/plain", "text/html", "text/css", 
                               "text/javascript"]
                }
            ]
        }
    }
}

The above manifest code tells the system that your app can accept text files with various MIME types. File name extensions, such as .txt, can also be passed in the accept array.

To access the shared file, use the request formData like before and use a FileReader to read the content, as follows:

self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    if (event.request.method === 'POST' && url.pathname === '/store-code-snippet') {
        event.respondWith((async () => {
            const data = await event.request.formData();

            const filename = data.get('title');
            const file = data.get('textFile');

            const reader = new FileReader();
            reader.onload = function(e) {
                const textContent = e.target.result;

                // Do something with the textContent here.

            };
            reader.readAsText(file);

            return Response.redirect('/snippet-stored-success', 303);
        })());
    }
});

See also