Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
WouterLemaire
SAP Mentor
SAP Mentor
1,067

Introduction

With the evolution of browsers on mobile devices more native functionalities are possible in browser based application on a mobile device, a good example of this it NFC scanning. In the past this required you to make a Hybrid or Native mobile app. Nowadays, this is perfectly possible on any Android device that has NFC scanning from in the browser using the web NFC api: Web NFC API - Web APIs | MDN (mozilla.org)

For iOS, this is still not possible because security constrains in iOS. In case of Fiori, you’ll have to create an MDK application with a plugin for NFC scanning as explained here:  https://community.sap.com/t5/technology-blogs-by-members/mdk-ios-nfc-uid-scanner-how-to-use-native-f...

This could help you for your new Fiori projects to have NFC scanning functionality. It can also help you to move away from Hybrid or Native Mobile applications which you might have running on BTP Mobile Services. This will save costs of Mobile Service, Mobile Device Management and simplify your application.

Problem

Using this in a UI5/Fiori application is super easy to do using the browser API for this. Unfortunately, when you want to integrate this application in BTP Work Zone, it won’t work anymore. Work Zone uses iframes for opening applications and the NFC API  can only be used on the top level frames for security reasons.

See the documentation:

Scan: NDEFReader: scan() method - Web APIs | MDN (mozilla.org)

Web NFC is only available to top-level frames and secure browsing contexts (HTTPS only). 

Interact with NFC devices on Chrome for Android  |  Capabilities  |  Chrome for Developers

Solution

As a workaround, I came up with a plugin for Work Zone to make this possible. This plugin will be loaded on the top level frame of Work Zone and trigger the scanning. To notify the application running in Work Zone, I use IFrame communication.

The plugin contains only a “Component.ts” file:

WouterLemaire_0-1734130383475.png

 

In the Component.ts file,  I create the NDEFReader object, provide a success handler to get the scanned object and error handler to know in case something goes wrong. In the end, this will activate the NFC scanner of the device and capture any NFC scanned during your session:

const ndef = new NDEFReader();
ndef.onreading = (event) => {
    console.log("Message:");
    console.log(JSON.stringify(event.message));
    const data = event.message.records[0].data;
    let decoder = new TextDecoder("utf-8");
    let scan = decoder.decode(data);
};
ndef.onreadingerror = (event) => {
    
  };
await ndef.scan();

Now, to inform the application that triggered the NFC scan with the result, I add the following code to the onRead event handler:

  • It gets all the iframe’s in Work Zone
  • Loops over them
  • Posts the result to all the iframes
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
    iframe.contentWindow?.postMessage({ action: "NFCSCANNED", tag: {id:scan} }, "*");
}

Same for the Error event handler but using another action:

const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
    iframe.contentWindow?.postMessage({ action: "NFCSCANNED", tag: {id:scan} }, "*");
}

Full code of the Component.ts file:

import MessageBox from "sap/m/MessageBox";
import BaseComponent from "sap/ui/core/UIComponent";

/**
 * @namespace be.nmbs.plugins.nfcscanning
 */
export default class Component extends BaseComponent {

    public static metadata = {
        manifest: "json"
    };

    /**
     * The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
     * @Public
     * @override
     */
    public async init() {
        // call the base component's init function
        super.init();
        if ('NDEFReader' in window) {
            const ndef = new NDEFReader();
            ndef.onreading = (event) => {
                console.log("Message:");
                console.log(JSON.stringify(event.message));
                const data = event.message.records[0].data;
                let decoder = new TextDecoder("utf-8");
                let scan = decoder.decode(data);
                const iframes = document.querySelectorAll('iframe');
                for (const iframe of iframes) {
                    iframe.contentWindow?.postMessage({ action: "NFCSCANNED", tag: {id:scan} }, "*");
                }
            };
            ndef.onreadingerror = (event) => {
                const iframes = document.querySelectorAll('iframe');
                for (const iframe of iframes) {
                    iframe.contentWindow?.postMessage({ action: "NFCSCANNED-ERROR", tag: {id:""} }, "*");
                }
              };
            await ndef.scan();
        }
    }
}

To consume this in an application, I do the following in the eventhandler of a button:

  • Register an eventhandler to catch the result
  • Trigger the message to the parent to inform that we want to start a scan

Consuming could look like this:

onclick: async function (oEvent) {
  window.addEventListener('message', (event)=>this.readTag(event));
},
readTag: function (event) {
   if(!event.data || !event.data.action || event.data.action !== "NFCSCANNED"){
       return;
   }
       MessageToast.show(`NFC Scan result: ${event.data.tag}`);
   }
}

For using typescript, it might be useful to have the NFC typescript definition file: Web-nfc.d.ts

// Type definitions for Web NFC
// Project: https://github.com/w3c/web-nfc
// Definitions by: Takefumi Yoshii <https://github.com/takefumi-yoshii>
// TypeScript Version: 3.9

// This type definitions referenced to WebIDL.
// https://w3c.github.io/web-nfc/#actual-idl-index

interface Window {
  NDEFMessage: NDEFMessage
}
declare class NDEFMessage {
  constructor(messageInit: NDEFMessageInit)
  records: ReadonlyArray<NDEFRecord>
}
declare interface NDEFMessageInit {
  records: NDEFRecordInit[]
}

declare type NDEFRecordDataSource = string | BufferSource | NDEFMessageInit

interface Window {
  NDEFRecord: NDEFRecord
}
declare class NDEFRecord {
  constructor(recordInit: NDEFRecordInit)
  readonly recordType: string
  readonly mediaType?: string
  readonly id?: string
  readonly data?: DataView
  readonly encoding?: string
  readonly lang?: string
  toRecords?: () => NDEFRecord[]
}
declare interface NDEFRecordInit {
  recordType: string
  mediaType?: string
  id?: string
  encoding?: string
  lang?: string
  data?: NDEFRecordDataSource
}

declare type NDEFMessageSource = string | BufferSource | NDEFMessageInit

interface Window {
  NDEFReader: NDEFReader
}
declare class NDEFReader extends EventTarget {
  constructor()
  onreading: (this: this, event: NDEFReadingEvent) => any
  onreadingerror: (this: this, error: Event) => any
  scan: (options?: NDEFScanOptions) => Promise<void>
  write: (
    message: NDEFMessageSource,
    options?: NDEFWriteOptions
  ) => Promise<void>
  makeReadOnly: (options?: NDEFMakeReadOnlyOptions) => Promise<void>
}

interface Window {
  NDEFReadingEvent: NDEFReadingEvent
}
declare class NDEFReadingEvent extends Event {
  constructor(type: string, readingEventInitDict: NDEFReadingEventInit)
  serialNumber: string
  message: NDEFMessage
}
interface NDEFReadingEventInit extends EventInit {
  serialNumber?: string
  message: NDEFMessageInit
}

interface NDEFWriteOptions {
  overwrite?: boolean
  signal?: AbortSignal
}
interface NDEFMakeReadOnlyOptions {
  signal?: AbortSignal
}
interface NDEFScanOptions {
  signal: AbortSignal
}
2 Comments
Maksim
Discoverer

Great blog (as always)! Extra kudos for typescript 😉

I think the second snippet is missing (trigger one):


  • Register an eventhandler to catch the result
  • Trigger the message to the parent to inform that we want to start a scan

Do we really need it? It is 

await ndef.scan();

 anyway

wridgeu
Participant

Awesome blog post @WouterLemaire and a great example of circumventing the given "architecture" limitations. 😄

One minor remark, the snippet extracted for the "Same for the Error event handler but using another action"-part is the same as the previous one. Does not matter much as they're both seen in the snippets afterwards but I thought I'd mention it.

Would be interesting to know if the NFC API would work when used within a worker context (in your case a Shared Worker). Same approach as with the launchpad plugin (which I would prefer, I think) but technically still interesting.

Regards 🙂

Labels in this area