© Majid Hajian 2019
Majid HajianProgressive Web Apps with Angularhttps://doi.org/10.1007/978-1-4842-4448-7_12

12. Modern Web APIs

Majid Hajian1 
(1)
Oslo, Norway
 

What if I tell you that you can build a web app, connect to a device that supports Bluetooth Low Energy, and have control over it from your web application? What if a user’s login credentials are kept in the browsers safely, and when users access the website, they are automatically signed in? What if a login to a web application needs a device connected via USB to authenticate a user? What if I can access share options on a native platform via a JavaScript API within our browser? I know what you might be thinking now; but even though these all sounded like dreams 10 years ago, today most of them are achievable or at least close to becoming realities.

For the past decade, much of the web has evolved significantly. New web APIs allow developers to connect web applications to hardware via Bluetooth and USB. Online payment has never been easier than it is today. Single sign-on and password-less solutions have brought a much better user experience with minimal effort. Developing a cross-platform via the same API across all devices and operating systems was very difficult whereas, today, it’s such a pleasant way to develop and build a web application – especially Progressive Web Apps (PWAs) since a lot of new APIs have been standardized that provide a high-level JavaScript API in our browsers to gain access to underlying low-level APIs of the platform.

In this chapter, I have chosen a few new technologies and APIs to explore and integrate with PWA note apps such as Credential Management, Payment Request, Geolocation, Media Streams, Web Bluetooth, and Web USB. I will ensure that the fundamentals of these APIs will be covered. However, you may need to develop additional ones for your applications based on your needs and requirements.

Additionally, I would suggest keeping an eye on Web Share, Web VR/AR, Background fetch, Accessibility improvement, Web Assembly, and many more new standards that are either under development or under consideration and will empower the web, especially by building a PWA.

Credential Management

The Credential management API is a Promised-based standard browser API that facilitates seamless sign-ins across devices by providing an interface between the website and the browser. This API allows the user to sign in with one tab via an account chooser and helps to store credentials in the browsers by which can be synced across devices. This helps that user who has signed in to one browser already – he or she can then stay logged in to all other devices as well if they use the same browser.

This API not only works with native-browser password management, but it can also provide information about credentials from a federated identity provider. What it means is this: any entity that a website trusts to correctly authenticate a user and provide an API for that purpose can be a provider in this API to store the credential and retrieve it if necessary. For example, Google Account, GitHub, Twitter, Facebook, or OpenID Connect are examples of a federated identity provider framework.

Keep in mind that this API will only work when the origin is secure; in other words, similar to PWA, your website must run on HTTPS.

Let’s start implementing in an Angular project and see how it works.

First, we will create a service called CredentialManagementService , and we import to my CoreModule.
declare const PasswordCredential: any;
declare const FederatedCredential: any;
declare const navigator: any;
declare const window: any;
@Injectable({
  providedIn: 'root'
})
export class CredentialManagementService {
  isCredentialManagementSupported: boolean;
  constructor(private snackBar: SnackBarService) {
    if (window.PasswordCredential || window.FederatedCredential) {
      this.isCredentialManagementSupported = true;
    } else {
      this.isCredentialManagementSupported = false;
      console.log('Credential Management API is not supported in this browser');
    }
  }
  async store({ username, password }) {
    if (this.isCredentialManagementSupported) {
      // You can either pass the passwordCredentialData as below
      // or simply pass down your HTMLFormElement. A reference to an HTMLFormElement with appropriate input fields.
      // The form should, at the very least, contain an id and password .
      // It could also require a CSRF token.
      /*
        <form id="form" method="post">
          <input type="text" name="id" autocomplete="username" />
          <input type="password" name="password" autocomplete="current-password" />
          <input type="hidden" name="csrf_token" value="*****" />
        </form>
        <script>
            const form = document.querySelector('#form');
            const credential = new PasswordCredential(form);
      // if you have a federated provider
       const cred = new FederatedCredential({
         id: id,
         name: name,
         provider: 'https://account.google.com',
         iconURL: iconUrl
             });
        <script>
      */
      // Create credential object synchronously.
      const credential = new PasswordCredential({
        id: username,
        password: password
        // name: name,
        // iconURL: iconUrl
      });
      const isStored = await navigator.credentials.store(credential);
      if (isStored) {
        this.snackBar.open('You password and username saved in your browser');
      }
    }
  }
  async get() {
    if (this.isCredentialManagementSupported) {
      return navigator.credentials.get({
        password: true,
        mediation: 'silent'
        // federated: {
        //   providers: ['https://accounts.google.com']
        // },
      });
    }
  }
  preventSilentAccess() {
    if (this.isCredentialManagementSupported) {
      navigator.credentials.preventSilentAccess();
    }
  }
}
This service has three methods that are basically wrappers around the main credential API methods to check if the API is available in the browser or not. Let’s break the service down:
  1. 1.

    A feature detection when service is initialized to ensure this API is available.

    if (window.PasswordCredential || window.FederatedCredential) {}
     
  1. 2.
    store method :
    1. A)

      Accepts username and password, and therefore we can create a password credential where it’ll be ready to store in credentials. PasswordCredential constructor accepts both HTMLFormElement and an object of essential fields. If you want to pass in the HTMLFormElement, make sure your form contains at least an ID and Password as well as CSRF token. In the method, the call constructor with ID, which is a username and password. name and iconURL, are names of the user that is signing in and the user’s avatar image, respectively and optionally. Keep in mind that we run this code if the feature is available; otherwise we let the user work with the application normally.

      Since we are building a PWA, it is always important to provide an alternative for those users whose browser of choice doesn’t support features that are being used.

       
    2. B)

      If you are going to use third-party login, you must call FederatedCredential constructor with an id as well as provider endpoint.

       
    3. C)

      Credentials API is available on navigator, the store function is Promised-based and by calling that, we can save user credentials in the browser.

       
    4. D)

      Finally, we show a message to the user in order to inform them that we store their password in the browser.

       
     
  2. 3.

    get method :

    After feature detection is checked, we call get on navigation.credentials by passing in the configuration such as password, mediation. Mediation defines how we want to tell the browser to show the account chooser to user, which has three values: optional, required, and silent. When mediation is optional, the user is explicitly shown an account chooser to sign in after a navigator.credentials.preventSilentAccess() was called. This is normally to ensure automatic sign-in doesn’t happen after the user chooses to sign out or unregister.

    Once navigator.credentials.get() resolves, it returns either an undefined or a credential object. To determine whether it is a PasswordCredential or a FederatedCredential, simply look at the type property of the object, which will be either password or federated. If the type is federated, the provider property is a string that represents the identity provider.

     
  3. 4.

    preventSilentAccess method :

    We call preventSilentAccess() on navigator.credentials. This will ensure the auto sign-in will not happen until next time the user enables auto sign-in. To resume auto sign-in, a user can choose to intentionally sign in by choosing the account they wish to sign in with, from the account chooser. Then the user is always signed back in until they explicitly sign out.

     

To continue with UserContainerComponent, we will first inject this service, then will define my autoSignIn method and will call that on ngOnInit. On both the signup and login methods, we will call the store method from the credential service to save and update the user credential.

Finally, when a user logs out, we need to call preventSilentAccess() . This is what it looks like:
  constructor(
    private credentialManagement: CredentialManagementService,
    private fb: FormBuilder,
    private auth: AuthService,
    private snackBar: SnackBarService
  ) {}
  ngOnInit() {
    this.createLoginForm();
    if (!this.auth.authenticated) {
      this.autoSignIn();
    }
  }
  private async autoSignIn() {
    const credential = await this.credentialManagement.get();
    if (credential && credential.type === 'password') {
      const { password, id, type } = credential;
      const isLogin = await this._loginFirebase({ password, email: id });
      if (isLogin) {
      // make sure to show a proper message to the user
        this.snackBar.open(`Signed in by ${id} automatically!`);
      }
    }
  }
  public signUp() {
    this.checkFormValidity(async () => {
      const signup = await this.auth.signUpFirebase(this.loginForm.value);
      const isLogin = await this.auth.authenticateUser(signup);
      if (isLogin) {
        const { email, password } = this.loginForm.value;
        this.credentialManagement.store({ username: email, password });
      }
    });
  }
  public login() {
    this.checkFormValidity(async () => {
      const { email, password } = this.loginForm.value;
      const isLogin = this._loginFirebase({ email, password });
      if (isLogin) {
        this.credentialManagement.store({ username: email, password });
      }
    });
  }
  public logOut() {
    this.auth
      .logOutFirebase()
      .then(() => {
        this.auth.authErrorMessages$.next(null);
        this.auth.isLoading$.next(false);
        this.auth.user$.next(null);
        // prevent auto signin until next time user login explicity
        // or allow us for auto sign in
        this.credentialManagement.preventSilentAccess();
      })
      .catch(e => {
        console.error(e);
        this.auth.isLoading$.next(false);
        this.auth.authErrorMessages$.next(
          'Something is wrong when signing out!'
        );
      });
  }

Note

Clone https://github.com/mhadaily/awesome-apress-pwa.git and go to Chapter 12, 01-credential-management-api folder to find all sample codes.

It is also a good practice to use the autocomplete attribute on the login form to help the browser to appropriately identify the fields (Figure 12-1).
   <input
          matInput
          placeholder="Enter your email"
          autocomplete="username"
          formControlName="email"
          required
        />
<input
          matInput
          autocomplete="current-password"
          placeholder="Enter your password"
          [type]="hide ? 'password' : 'text'"
          formControlName="password"
        />
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig1_HTML.jpg
Figure 12-1

Autocomplete attribute allows browser to show appropriate username and password for the website

We run the application in a new browser, then we will go to the login page and by entering my credential will log in to the website. You’ll see a prompt message that asks the user to save the credential in the browser (see Figure 12-2).
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig2_HTML.jpg
Figure 12-2

Credential prompt when web app wants to save credential in the browser

To test my auto sign-in, we will open a new clean browser and will go to the login page, then we will notice that we get redirected to the note list, and a snackbar message appears that shows we am automatically signed in (see Figure 12-3).
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig3_HTML.jpg
Figure 12-3

The website snackbar message and a message from browser itself after automatic sign-in occurs

Finally, the mediation optional or required will display an account chooser prompt that allows users to select their account of choice, especially if they have more than one account saved (See Figure 12-4).
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig4_HTML.jpg
Figure 12-4

The account chooser if mediation is optional or required

Browsers Support

By the time of writing this book, Chrome on Desktop and for Android, Android browser, Opera for desktop and mobile, and Samsung internet browser support this API; and it currently under consideration for Firefox. MS Edge is moving to a Chromium platform and this API should be covered soon.

Payment Request

There is a high probability that all of us reading this book have made a payment on the web – at least once. So we all know that how time consuming, and boring it is to fill out the checkout forms, especially if it has more than one step.

The payment request standard API is developed by W3C to ensure the online payment system for both consumers and merchants remains consistent and smooth with minimal effort. This is not a new way of payment; rather, it’s a way that aims to make the checkout process easier.

With this API, consumers always see a native platform UI when they want to select payment details such as shipping address, credit card, contact details, etc. Imagine that once you save all of the information in your browsers, you can simply just reuse them in every single checkout page where this API is supported. How pleasant an experience it will be: ignore the filling out of lots of fields in a checkout form, credit card information, and more. Instead we will be seeing saved information consistently with a familiar native UI. Just a few clicks or tabs to select, and it’s done!

Another advantage of this API is to accept different payment methods from a variety of handlers to the web with relatively easy integration: for example, Apple Pay, Samsung Pay, Google Pay.

Long story short, I am going to add a donation button in the PWA note app.

First, we will create a service in Angular called WebPaymentService and import it in CoreModule.
export class WebPaymentService {
  public isWebPaymentSupported: boolean;
  private requestPayment = null;
  private canMakePaymentPromise: Promise<boolean> = null;
  private supportedPaymentMethods = [
    {
      // support credit card payment
      supportedMethods: 'basic-card',
      data: {
        supportedNetworks: ['visa', 'mastercard', 'amex'],
        supportedTypes: ['credit', 'debit']
      }
    }
  // Apple pay, Google Pay, Samasung pay, Stripe and others can be added here too.
 ];
// just an example of a simple product details
  private paymentDetails: any = {
    total: {
      label: 'Total Donation',
      amount: { currency: 'USD', value: 4.99 }
    },
    displayItems: [
      {
        label: 'What I recieve',
        amount: { currency: 'USD', value: 4.49 }
      },
      {
        label: 'Tax',
        amount: { currency: 'USD', value: 0.5 }
      }
    ]
  };
  private requestPaymentOptions = {
    requestPayerName: true,
    requestPayerPhone: false,
    requestPayerEmail: true,
    requestShipping: false
    shippingType: 'shipping'
  };
  constructor() {
    if (window.PaymentRequest) {
      // Use Payment Request API which is supported
      this.isWebPaymentSupported = true;
    } else {
      this.isWebPaymentSupported = false;
    }
  }
  constructPaymentRequest() {
    if (this.isWebPaymentSupported) {
      this.requestPayment = new PaymentRequest(
        this.supportedPaymentMethods,
        this.paymentDetails,
        this.requestPaymentOptions
      );
// ensure that user have a supported payment method if not you can do other things
      if (this.requestPayment.canMakePaymentPromise) {
        this.canMakePaymentPromise = this.requestPayment.canMakePayment();
      } else {
        this.canMakePaymentPromise = Promise.resolve(true);
      }
    } else {
      // do something else for instance redirect user to normal checkout
    }
    return this;
  }
  async show(): Promise<any> {
/*  you can make sure client has a supported method already if not do somethig else. For instance, fallback to normal checkout, or let them to add one active card */
    const canMakePayment = await this.canMakePaymentPromise;
    if (canMakePayment) {
      try {
        const response = await this.requestPayment.show();
        // here where you can process response payment with your backend
        // there must be a backend implementation too.
        const status = await this.processResponseWithBackend(response);
        // after backend responsed successfully, you can do any other logic here
        // complete transaction and close the payment UI
        response.complete(status.success);
        return status.response;
      } catch (e) {
        //  API Error or user closed the UI
        console.log('API Error or user closed the UI');
        return false;
      }
    } else {
      // Fallback to traditional checkout for example
      // this.router.navigateByUrl('/donate/traditional');
    }
  }
  async abort(): Promise<boolean> {
    return this.requestPayment.abort();
  }
  // mock backend response
  async processResponseWithBackend(response): Promise<any> {
    // check with backend and respond accordingly
    return new Promise(resolve => {
      setTimeout(() => {
        resolve({ success: 'success', response });
      }, 1500);
    });
  }
}
Let’s break it down.
  1. 1.
    As always, a feature detection for progressive enhancement.
    if (window.PaymentRequest) {
          this.isWebPaymentSupported = true;
        } else {
          this.isWebPaymentSupported = false;
        }
     
  2. 2.
    For each payment, you need to construct a PaymentRequest that accepts three arguments.
    new PaymentRequest(
            this.supportedPaymentMethods,
            this.paymentDetails,
            this.requestPaymentOptions
          );
     
  3. 3.
    Define supportedPaymentMethods , which is an array of all supported payment methods. In the code example, I have just defined a basic card; however, in this chapter’s sample codes, you will find more methods such as Apple Pay, Google Pay, and Samsung Pay. You are not also limited to them; you can implement any favorite methods such as PayPal, Stripe, and more that support this API.
    private supportedPaymentMethods = [
        {
          // support credit card payment
          supportedMethods: 'basic-card',
          data: {
           // you can add more such as discover, JCB and etc.
            supportedNetworks: ['visa', 'mastercard', 'amex'],
            supportedTypes: ['credit', 'debit']
          }
        },
    ]
    Each object in this array has supportedMethods and data property that is specific for the method itself. To have a better understanding, I’ll provide an Apple Pay object as an example, too:
    {
          supportedMethods: 'https://apple.com/apple-pay',
          data: {
            version: 3,
            merchantIdentifier: 'merchant.com.example',
            merchantCapabilities: ['supports3DS', 'supportsCredit', 'supportsDebit'],
            supportedNetworks: ['amex', 'discover', 'masterCard', 'visa'],
            countryCode: 'US'
          }
        },
     
  4. 4.
    In Define paymentDetails , for instance, in my example, I have a fixed donation number; however, you may have a cart page with different products and other details that need to be added to payment details accordingly.
    private paymentDetails: any = {
        total: {
          label: 'Total Donation',
          amount: { currency: 'USD', value: 4.99 }
        },
        displayItems: [
          {
            label: 'What I recieve',
            amount: { currency: 'USD', value: 4.49 }
          },
          {
            label: 'Tax',
            amount: { currency: 'USD', value: 0.5 }
          }
        ]
      };

    There are two main properties: total indicates total amount; and displayItems, which is an array that shows cart items.

     
  5. 5.
    Define requestPaymentOptions is optional; however, you may find it very useful for different purposes – for instance, if a shipping address is required or email must be provided.
      private requestPaymentOptions = {
        requestPayerName: true,
        requestPayerPhone: false,
        requestPayerEmail: true,
        requestShipping: false,
        shippingType: 'shipping'
      };

    In this example, we ask the payer to provide an email and name only.

     
  6. 6.
    Last but not least, we show call show method on requestPayment in order to display the payment native prompt page.
    async show(): Promise<any> {
        const canMakePayment = await this.canMakePaymentPromise;
        if (canMakePayment) {
          try {
            const response = await this.requestPayment.show();
            const status = await this.processResponseWithBackend(response);
            response.complete(status.success);
            return status.response;
          } catch (e) {
            return false;
          }
        }
      }
     

There is another Promised-based method on requestPayment called canMakePayment(), which is essentially a helper to determine if the user has a supported payment method to make this payment before show() gets called. It may not be in all user agents; therefore, we need to feature detect.

Then we call show() , once the user is done, and Promise will get resolved with the user’s selection details including contact information, credit card, shipping, and more. Now it’s time to validate and process the payment with the back end.

Open header.component.html() and add the following button (see Figure 12-5):
  <button mat-menu-item (click)="donateMe()" *ngIf="isWebPaymentSupported">
    <mat-icon>attach_money</mat-icon>
    <span>Donate</span>
  </button>
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig5_HTML.jpg
Figure 12-5

Donate button where it triggers payment native UI

Lastly, inject WebPaymentService into header.component.ts. donateMe() method should be defined, and it will call requestPayment and display the appropriate message to the user once it’s resolved.
public isWebPaymentSupported: boolean;
  constructor(
    private webPayment: WebPaymentService,
  ) {
    this.isWebPaymentSupported = this.webPayment.isWebPaymentSupported;
  }
async donateMe() {
    const paymentResponse = await this.webPayment
      .constructPaymentRequest()
      .show();
    if (paymentResponse) {
      this.snackBar.open(
        `Successfully paid, Thank you for Donation ${paymentResponse.payerName}`
      );
    } else {
      // this.snackBar.open('Ops, sorry something went wrong with payment');
    }
  }
We will build application and run and test it in the browser and mobile (See Figures 12-6 and 12-7).
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig6_HTML.jpg
Figure 12-6

Payment native UI, Chrome, Mac

../images/470914_1_En_12_Chapter/470914_1_En_12_Fig7_HTML.jpg
Figure 12-7

Apple pay in Safari, Chrome, and Samsung internet browser display native payment UI

Note

Clone https://github.com/mhadaily/awesome-apress-pwa.git and go to Chapter 13, 02-request-payment-api folder to find all sample codes.

Browsers Support

By the time of writing this book, almost all major browsers support this API either in production or a nightly build for both desktop and mobile, although they may also support them partially.

Video and Audio Capturing

The Media Streams is an API related to WebRTC, which provides support for streaming audio and video data. This API has been around for a while. New Promised-based getUserMedia() is a method that ask for user permission for microphone and camera; and, thus, you will get access to the live stream.

In this section, we will add a new feature to “add note” page, where users can save an interactive video with audio to their notes.

Note, we will not send this video to a server in this example, but the implementation will be ready to communicate to the back end in order to save the video and audio if needed.

In notes-add.component.html, we will add following html snippet:
<div class="media-container" *ngIf="isMediaRecorderSupported">
      <h1>Add video with audio Note</h1>
      <div class="videos">
        <div class="video">
          <h2>LIVE STREAM</h2>
          <video #videoOutput autoplay muted></video>
        </div>
        <div class="video">
          <h2>RECORDED STREAM</h2>
          <video #recorded autoplay loop></video>
        </div>
      </div>
      <div class="buttons">
        <button mat-raised-button color="primary" (click)="record()" *ngIf="disabled.record" > Start Recording</button>
        <button mat-raised-button color="primary" (click)="stop()" *ngIf="disabled.stop"> Stop Recording </button>
        <button mat-raised-button color="secondary" (click)="play()" *ngIf="disabled.play"> Play Recording</button>
        <button mat-raised-button color="primary" (click)="download()" *ngIf="disabled.download">  Download Recording </button>
        <a #downloadLink href="">Download Link</a>
      </div>
    </div>
This code is pretty self-explanatory. We add the logic to notes-add.component.ts:
export class NotesAddComponent {
  @ViewChild('videoOutput') videoOutput: ElementRef;
  @ViewChild('recorded') recordedVideo: ElementRef;
  @ViewChild('downloadLink') downloadLink: ElementRef;
  public disabled = { record: true, stop: false, play: false, download: false };
  public userID;
  public errorMessages$ = new Subject();
  public loading$ = new Subject();
  public isMediaRecorderSupported: boolean;
  private recordedBlobs;
  private liveStream: any;
  private mediaRecorder: any;
  constructor(
    private router: Router,
    private data: DataService,
    private snackBar: SnackBarService
  ) {
    if (window.MediaRecorder) {
      this.isMediaRecorderSupported = true;
      this.getStream();
    } else {
      this.isMediaRecorderSupported = false;
    }
  }
  async getStream() {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: true,
        video: true
      });
      this.handleLiveStream(stream);
    } catch (e) {
      this.isMediaRecorderSupported = false;
      this.onSendError('No permission or something is wrong');
      return 'No permission or something is wrong';
    }
  }
  handleLiveStream(stream) {
    this.liveStream = stream;
    this.videoOutput.nativeElement.srcObject = stream;
  }
  getMediaRecorderOptions() {
    let options = {
      mimeType: 'video/webm;codecs=vp9',
      audioBitsPerSecond: 1000000, // 1 Mbps
      bitsPerSecond: 1000000, // 2 Mbps
      videoBitsPerSecond: 1000000 // 2 Mbps
    };
    if (!MediaRecorder.isTypeSupported(options.mimeType)) {
      console.log(`${options.mimeType} is not Supported`);
      options = { ...options, mimeType: 'video/webm;codecs=vp8' };
      if (!MediaRecorder.isTypeSupported(options.mimeType)) {
        console.log(`${options.mimeType} is not Supported`);
        options = { ...options, mimeType: 'video/webm' };
        if (!MediaRecorder.isTypeSupported(options.mimeType)) {
          console.log(`${options.mimeType} is not Supported`);
          options = { ...options, mimeType: " };
        }
      }
    }
    return options;
  }
  record() {
    this.recordedBlobs = [];
    this.disabled = { play: false, download: false, record: false, stop: true };
    this.mediaRecorder = new MediaRecorder(
      this.liveStream,
      this.getMediaRecorderOptions
    );
    this.mediaRecorder.ondataavailable = e => {
      {
        if (e.data) {
          this.recordedBlobs.push(e.data);
        }
      }
    };
    this.mediaRecorder.start();
    console.log('MediaRecorder started', this.mediaRecorder);
  }
  stop() {
    this.disabled = { play: true, download: true, record: true, stop: false };
    this.mediaRecorder.onstop = e => {
      this.recordedVideo.nativeElement.controls = true;
    };
    this.mediaRecorder.stop();
  }
  play() {
    this.disabled = { play: true, download: true, record: true, stop: false };
    const buffer = new Blob(this.recordedBlobs, { type: 'video/webm' });
    this.recordedVideo.nativeElement.src = window.URL.createObjectURL(buffer);
  }
  download() {
    const blob = new Blob(this.recordedBlobs, { type: 'video/webm' });
    const url = window.URL.createObjectURL(blob);
    this.downloadLink.nativeElement.url = url;
    this.downloadLink.nativeElement.download = `recording_${new Date().getTime()}.webm`;
    this.downloadLink.nativeElement.click();
    setTimeout(() => {
      window.URL.revokeObjectURL(url);
    }, 100);
  }
  onSaveNote(values) {
    this.data.addNote(values).then(
      doc => {
        this.snackBar.open(`LOCAL: ${doc.id} has been succeffully saved`);
      },
      e => {
        this.errorMessages$.next('something is wrong when adding to DB');
      }
    );
    this.router.navigate(['/notes']);
  }
  onSendError(message) {
    this.errorMessages$.next(message);
  }
}
This code is straightforward. As always, it is a feature detection for MediaRecorder, and if it is supported by a browser, we will continue and show this feature to our user and will initialize getUserMedia() ; therefore, we ask for audio and video permission as shown in Figure 12-8.
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig8_HTML.jpg
Figure 12-8

Browser asks for permission for camera and microphone

Once permission is granted, Promise gets resolved and the stream will be accessible (see Figure 12-9). When user clicks on tab “start recording” button, MediaRecorder constructor gets called with the live stream data and the options that gave already been defined.

We store each blob in an array until stop() method gets called. Once recording stops, media is ready to be played. By hitting “play” button, we will simply create a stream buffer of the stream array and by creating a Blob URL, we will assign it to an src of <video> tag.
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig9_HTML.jpg
Figure 12-9

Ask for permission for getting access to video and audio on Android mobile

Ta-da, now the video is playing directly in the browser. We are also able to work on the downloadable version of this video (see Figure 12-10).
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig10_HTML.jpg
Figure 12-10

Live stream and recorded playback

By the tab or click on “Download“ button, we will create a Blob from an array of recordedBlob and then will create a URL and assign to <a> tag thatI have defined in the template with display: none and then call click() to force browser opening download modal for user in order to ask them where this file must be saved on their system.

Note

Clone https://github.com/mhadaily/awesome-apress-pwa.git and go to Chapter 12, 03-camera-and-microphone-api folder to find all sample codes.

Browsers Support

At the time of writing this book, Opera, Chrome, and Firefox on desktop; and Chrome and Samsung internet on Android support most of the standard specs. Microsoft Edge also has this API under consideration. It also works on Safari 12 / iOS 12. I believe the API’s future is bright.

Geolocation

The Geolocation API provides the user’s location coordination and exposes it to the web application. The browser will ask for permission for privacy reasons. This Promised-based API has been around for a long time. You might even work with it already.

We will explore this API by creating a service called GeolocationService where you can find it under modules/core/geolocation.service.ts.
export interface Position {
  coords: {
    accuracy: number;
    altitude: number;
    altitudeAccuracy: number;
    heading: any;
    latitude: number;
    longitude: number;
    speed: number;
  };
  timestamp: number;
}
@Injectable()
export class GeolocationService {
  public isGeoLocationSupported: boolean;
  private geoOptions = {
    enableHighAccuracy: true, maximumAge: 30000, timeout: 27000
  };
  constructor() {
    if (navigator.geolocation) {
      this.isGeoLocationSupported = true;
    } else {
      // geolocation is not supported, fall back to other options
      this.isGeoLocationSupported = false;
    }
  }
  getCurrentPosition(): Observable<Position> {
    return Observable.create(obs => {
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
          position => {
            obs.next(position);
            obs.complete();
          },
          error => {
            obs.error(error);
          }
        );
      }
    });
  }
  watchPosition(): Observable<Position> {
    return Observable.create(obs => {
      if (navigator.geolocation) {
        navigator.geolocation.watchPosition(
          position => {
            obs.next(position);
          },
          error => {
            obs.error(error);
          },
          this.geoOptions
        );
      }
    });
  }
}
Let’s break it down.
  1. 1.

    As usual, a feature detection to ensure geolocation is available.

     
  2. 2.

    Define getCurrentPosition() , I am going to convert geolocation.getCurrentPosition() callbacks into an observable.

     
  3. 3.

    Define watchPosition() , we do the same with geolocation.watchPosition() and turn its callbacks into an observable.

     
  4. 4.

    We have already defined my Position interface by which geolocation methods provide.

     

What I’d like to do is to add user coordination to each note to keep the location as it saves. Thus, we can later show the user’s note’s coordination or exact address using a third-party map provider like Google Map. Since we are saving all coordination data, we will be able to convert this coordination to a meaningful address using third-party map providers in the back end or even in the front end based on application needs.

At the moment, to keep in simple and short, let’s just display the current latitude and longitude to the user.

First, we inject geolocation service into NotesAddComponent , then we will call getCurrentPosition() and assign it to my local location$ variable where we transform a position object into a simple string.
public isGeoLocationSupported = this.geoLocation.isGeoLocationSupported;
public location$: Observable<string> = this.geoLocation
    .getCurrentPosition()
    .pipe(map(p =>
        `Latitude:${p.coords.latitude}
        Longitude:${p.coords.longitude}`
        ));
  constructor(
    private router: Router,
    private data: DataService,
    private snackBar: SnackBarService,
    private geoLocation: GeolocationService
  ) {}
Finally, add the following html snippet where we use location$ observable with async pipe; however, we first check if geolocation is available by using *ngIf (see permission dialog in Figure 12-11).
    <h4 *ngIf="isGeoLocationSupported">You location is {{ location$ | async }}</h4>
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig11_HTML.jpg
Figure 12-11

Browser asks for location permission

Once permission is allowed by the user, the browser will provide coordination data on each method call (see Figure 12-12).
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig12_HTML.jpg
Figure 12-12

Geolocation permission dialog on Android; once it’s resolved, coordination is displayed

Note

Clone https://github.com/mhadaily/awesome-apress-pwa.git and go to Chapter 12, 04-geolocation-api folder to find all sample codes.

Browsers Support

All major browsers support this API, which globally covers over 93 percent of the market according to the caniuse.​com website.1

Web Bluetooth

This Promised-based API is a new technology that opens a new era for Internet of Things through the web. It allows a web application to get connected to Bluetooth Low Energy (BLE) devices.

Imagine developing a PWA where we are able to get access to Bluetooth and get control over devices such as smart home appliances, health accessories, ONLY with web API consistency across all browsers in different platforms.

Keep in mind that this API is still being developed and API may slightly change in the future. I recommend the following implementation status documentation on GitHub.2

Before we continue, I would suggest studying the basic knowledge of how Bluetooth Low Energy (BLE) and the Generic Attribute Profile (GATT)3 work.

In this section, we simulate a BLE device using BLE Peripheral Simulator4 app on Android and will pair my PWA note app to that device in order to receive a battery level number. What we have done is this:
  1. 1.

    Installed BLE Peripheral Simulator app

     
  2. 2.

    Select Battery Service to advertise

     
  3. 3.

    Keep the screen on and put the battery level to 73

     

Let’s get started.

First, we will create my WebBluetoothService and import it in CoreModule.
@Injectable()
export class WebBluetoothService {
  public isWebBluetoothSupported: boolean;
  private GATT_SERVICE_NAME = 'battery_service';
  private GATT_SERVICE_CHARACTERISTIC = 'battery_level';
  constructor() {
    if (navigator.bluetooth) {
      this.isWebBluetoothSupported = true;
    }
  }
  async getBatteryLevel(): Promise<any> {
    try {
       // step 1, scan for devices and pair
      const device = await navigator.bluetooth.requestDevice({
        // acceptAllDevices: true
        filters: [{ services: [this.GATT_SERVICE_NAME] }]
      });
      // step 2: connect to device
      const connectedDevice = await this.connectDevice(device);
      // step 3 : Getting Battery Service
      const service = await this.getPrimaryService(connectedDevice, this.GATT_SERVICE_NAME);
      // step 4: Read Battery level characterestic
      const characteristic = await this.getCharacteristic(service, this.GATT_SERVICE_CHARACTERISTIC);
      // step 5: ready battery level
      const value = await characteristic.readValue();
      // step 6: return value
      return `Battery Level is ${value.getUint8(0)}%`;
    } catch (e) {
      console.error(e);
      return `something is wrong: ${e}`;
    }
  }
  private connectDevice(device): Promise<any> {
    return device.gatt.connect();
  }
  private getPrimaryService(connectedDevice, serviceName): Promise<any> {
    return connectedDevice.getPrimaryService(serviceName);
  }
  private getCharacteristic(service, characterestic): Promise<any> {
    return service.getCharacteristic(characterestic);
  }
}
This service is simple. We followed these steps:
  1. 1.

    Detect if bluetooth is available.

     
  2. 2.

    Call requestDevice() with proper configuration where we ask browser to filter and show us what we are interested in. There is potentially an option to ask for checking all devices; however, it’s not recommended regarding battery health.

    To make the service simple, we have statically defined GATT service name and characteristic.

     
  3. 3.

    Try to connect to device once prompt modal appears.

     
  4. 4.

    Call getPrimaryService() to retrieve battery service.

     
  5. 5.

    By calling getCharacteristic(), we will ask for battery_level.

     
  6. 6.

    Once characteristic was resolved, we will read the value.

     

It seems a bit complex and confusing even though this a very simple device and the documentation is clear. The more you work with these types of devices and technologies, the better you will become at figuring it all out.

You can only ask a browser to discover devices by click or tab on a button; thus, we’ll add a button under the menu in header.component.html and ensure with ngIf that the button appears when it’s supported.
  <button mat-menu-item (click)="getBatteryLevel()" *ngIf="isWebBluetoothSupported">
    <mat-icon>battery_unknown</mat-icon>
    <span>Battery Level</span>
  </button>
Finally, I will define my getBatteryLevel method in header.component.ts, which only shows a message with the battery level once all promises are resolved (see Figure 12-13).
async getBatteryLevel() {
    const level = await this.bluetooth.getBatteryLevel();
    this.snackBar.open(level);
  }

Note

Clone https://github.com/mhadaily/awesome-apress-pwa.git and go to Chapter 12, 05-web-bluetooth-api folder to find all sample codes.

../images/470914_1_En_12_Chapter/470914_1_En_12_Fig13_HTML.jpg
Figure 12-13

Web Bluetooth API: pair a device and read a characteristic and display a message once all Promises are resolved

The example above unfolds read possibilities from a BLE device; however, writing5 to Bluetooth characteristics and subscribing6 to receive GATT notifications are also another case.

We have reviewed the basics of Web Bluetooth and hope that it excites you enough to get started with this awesome web technology.

There is a great Angular library for Web Bluetooth with Observable API by my community friend Wassim Chegham – and you can install by running the following command:
npm i -S @manekinekko/angular-web-bluetooth @types/web-bluetooth

Find the documentation on GitHub https://github.com/manekinekko/angular-web-bluetooth .

Browsers Support

Browsers that support this API, at the time of writing this book, are Chrome Desktop for both Windows and Mac as well as Android, Samsung internet, and Opera. I hope in the future, especially when you are reading this section, there will be more browsers supporting Web Bluetooth API.

Web USB

This Promised-based API provides a safe way to expose USB devices to the web via browsers using JavaScript high-level APIs. This is still a relatively new API and may change over time, implementation is limited, and bugs are reported.

Web USB API by default needs HTTPS, and similar to Web Bluetooth it must be called via a user gesture such as a touch or mouse click. Devices similar to a keyboard and mouse are not accessible to this API.

I believe Web USB opens a new window where it brings a lot of opportunities for academic purposes, students, manufacturers, and developers. Imagine this instead of an online developer tool that can access to a USB board directly or manufacturers who need to write native drivers; instead they will be able to develop a cross-platform JavaScript SDK. Think of a hardware support center who can access directly through their website to my device and diagnose or debug. We can count more and more case studies; however, I should mention that this technology is still growing and, if not right now, will be a mind-blowing feature for the web in the coming future. Truly, the web is amazing; isn’t it?

Enough talking, let’s get started and explorer the API. To keep it simple and give you an idea how Web USB works, we going to connect my “Transcend Pen drive,” and once it’s connected, I will just show a message where it displays hardware information including “serial number.”

First, I write a service called WebUSBService and import to CoreModule.
@Injectable()
export class WebUSBService {
  public isWebUSBSupported: boolean;
  constructor(private snackBar: SnackBarService) {
    if (navigator.usb) {
      this.isWebUSBSupported = true;
    }
  }
  async requestDevice() {
    try {
      const usbDeviceProperties = { name: 'Transcend Information, Inc.', vendorId: 0x8564 };
      const device = await navigator.usb.requestDevice({ filters: [usbDeviceProperties] });
      // await device.open();
      console.log(device);
      return `
      USB device name: ${device.productName}, Manifacture is ${device.manufacturerName}
      USB Version is: ${device.usbVersionMajor}.${device.usbVersionMinor}.${device.usbVersionSubminor}
      Product Serial Number is ${device.serialNumber}
      `;
    } catch (error) {
      return 'Error: ' + error.message;
    }
  }
  async getDevices() {
    const devices = await navigator.usb.getDevices();
    devices.map(device => {
      console.log(device.productName); // "Mass Storage Device"
      console.log(device.manufacturerName); // "JetFlash"
      this.snackBar.open(
        `this. USB device name: ${device.productName}, Manifacture is ${device.manufacturerName} is connected.`
      );
    });
  }
}
Let’s break it down:
  1. 1.

    Feature detection to ensure “usb” is available.

     
  2. 2.

    Define requestDevice method , it calls navigator.usb.requestDevice(). I needed to explicitly filter my USB device by vendorID. I didn’t magically come up with vendor hexadecimal number; what I did was to search and find my device name ‘Transcend’ in this list http://​www.​linux-usb.​org/​usb.​ids.

     
  3. 3.

    Define getDevices method , and it calls navigator.usb.getDevices(); once resolved, it will return a list of devices that are connected to the origin.

     
We add two buttons in header.component.html , which on click call getDevices() and requestDevice() methods respectively.
  <button mat-menu-item (click)="getUSBDevices()" *ngIf="isWebUSBSupported">
    <mat-icon>usb</mat-icon>
    <span>USB Devices List</span>
  </button>
  <button mat-menu-item (click)="pairUSBDevice()" *ngIf="isWebUSBSupported">
    <mat-icon>usb</mat-icon>
    <span>USB Devices Pair</span>
  </button>
Inject WebUSBService to header.component.ts . Make sure buttons are visible if isWebUSBSupported is true.
constructor(private webUsb: WebUSBService) {
        this.isWebUSBSupported = this.webUsb.isWebUSBSupported;
}
  getUSBDevices() {
    this.webUsb.getDevices();
  }
  async pairUSBDevice() {
    const message = await this.webUsb.requestDevice();
    this.snackBar.open(message);
  }
By clicking on “USB Devices Pair,” a list appears where it shows my device and I can pair it (see Figure 12-14).
../images/470914_1_En_12_Chapter/470914_1_En_12_Fig14_HTML.jpg
Figure 12-14

A device in the list based on the filter options when requestDevice() gets called. Once paired, based on logic, a message appears that shows device information such as serial number, device name, manufacturer, USB version, etc. Once device is connected, it’s ready to transfer data in and out.

Once the pair is completed successfully, the device is ready to be opened and data can be transferred in and out.

For example, here is an example for a device to communicate with:
              await device.open();
              await device.selectConfiguration(1) // Select configuration #1
              await device.claimInterface(0) // Request exclusive control over interface #0
              await device.controlTransferOut({
                      "recipient": "interface",
                      "requestType": "class",
                      "request": 9,
                     "value": 0x0300,
                    "index": 0 })
             const result = await device.transferIn(8, 64); // Ready to receive data7
          // and you need to read the result...

This information is specific to each device. However, the methods are an API in the browser.

In general, the Web USB API provides all endpoint types of USB devices:
  • Interrupt transfers:

Used for typically nonperiodic, small device “initiated” communication by calling transferIn(endpointNumber, length) and transferOut(endpointNumber, data)
  • Control transfers:

Used for command and status operations by calling controlTransferIn(setup, length) and controlTransferOut(setup, data)
  • Bulk transfers:

Used for large data such as print-job sent to a printer by calling transferIn(endpointNumber, length) and transferOut(endpointNumber, data)
  • Isochronous transfers:

Used for continuous and periodic data, such as an audio or video stream by calling isochronousTransferIn(endpointNumber, packetLengths) and isochronousTransferOut(endpointNumber, data, packetLengths)

Last but not least, it may happen that users connect or disconnect the device from their system. There are two events that can be listened to and acted on accordingly.
      navigator.usb.onconnect = event => {
        // event.device will bring the connected device
        // do something here
        console.log('this device connected again: ' + event.device);
      };
      navigator.usb.ondisconnect = event => {
        // event.device will bring the disconnected device
        // do something here
        console.log('this device disconnected: ', event.device);
      };

Debugging USB in Chrome is easier with the internal page chrome://device-log where you can see all USB device-related events in one single place.

Note

Clone https://github.com/mhadaily/awesome-apress-pwa.git and go to Chapter 12, 06-web-usb-api folder to find all sample codes.

Browsers Support

Browsers that support this API, at the time of writing this book, are Chrome For desktop and Android as well as Opera. While the API is evolving and being developed rapidly, I hope we soon see better support in the browsers.

Summary

In this chapter, we have just explored six web APIs. Although they are not an essential part of a PWA, they help to build an app that is even closer to native apps.

As I wrote in the chapter’s introduction, these are not the only new APIs that are coming to web. There are many others that are either under development or consideration to be developed soon.

I am very excited about the future of web development as I can see how it will open endless opportunities in front of us to build and ship a much better web application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.175.180