Angular & Ionic 4 – PayPal mobilno plaćanje

Cilj ovog blog posta je pokazati kako implementirati PayPal plaćanje unutar Ionic 4, PWA, aplikacije koristeći Smart Payment Button (Checkout).

Osim Smart Payment Buttons, PayPal omogućava sljedeće vrste plaćanja:

PayPal Smart Payment
PayPal Developer: https://developer.paypal.com/

Primjeri u ovom blog postu temelje se na dokumentaciji dostupnoj na poveznici https://developer.paypal.com/.

Iako u testnom okruženju, plaćanje kreirano na ovaj način biti će uspješno provedeno što će se moći vidjeti unutar PayPal testnog sučelja i prikazano u nastavku ovog blog posta.

Ipak, kako je vidljivo u službenoj dokumentaciji potrebno je napraviti i serverski tj. backend dio koji će služiti kao potvrda uspješnog plaćanja. Serverski dio u ovom blog postu neće biti detaljnije obrađen.

Smart Payment Buttons funkcionira na sljedeći način:

  • Gumb za plaćanje prikazan je na web/mobilnoj aplikaciji
  • Kupac klikne na gumb
  • Poziva se PayPal Orders API koji priprema transakciju
  • Prikazuje se PayPal Checkout forma za prijavu/plaćanje
  • Kupac odobrava plaćanje
  • Poziva se PayPal Orders API koji izvršava transakciju
  • Kupcu se prikazuje poruka o uspješnoj transakciji

 

Smart Payment Buttons

PayPal Sandbox & App Name

Kako bi mogao testirati PayPal naplatu potrebni su mi: testni PayPal korisnički račun na koji će sredstva biti uplaćena, testni PayPal korisnički račun koji će izvršavati plaćanje kao i profil aplikacije putem koje će se plaćanje izvršavati.

PayPal Sandbox

S obzirom da već imam postojeći PayPal račun ne moram ga sada kreirati nego je dovoljno otići na adresu https://developer.paypal.com/developer/accounts/ kako bi kreirao Sandbox račun.

PayPal Sandbox

Moguće je kreirati testni račun fizičke ili pravne osobe tj. tvrtke. U ovom ću primjeru kreirati poslovni PayPal račun.

Valuta tog računa biti će u američkim dolarima (USD), a plaćanje ću kasnije izvršavati u dolarima i eurima (EUR). PayPal automatski radi konverziju valuta.

PayPal Sandbox

Klikom na „Create Account“ kreiram Sandbox PayPal račun fortuno@example.com.

PayPal Sandbox

My Apps & Credentials

Nakon toga na adresi https://developer.paypal.com/developer/applications kreiram profil aplikacije putem koje ću izvršavati plaćanje.

PayPal Apps & Credentials

Klikom na „Create App“ dolazim do ekrana gdje je potrebno unijeti naziv aplikacije i odabrati Sandbox račun s kojim će ta aplikacija biti povezana.

PayPal Apps & Credentials

Nakon toga dobijem Client ID koji će mi biti potreban kasnije i bez kojega ne mogu izvršiti plaćanje.

PayPal Apps & Credentials

Smart Payment Buttons & Ionic 4

Sada ću kreirati novi Ionic 4 projekt. To činim naredbom:

$ ionic start Ionic4PayPal blank

Fokus će biti na početnom ekranu koji će se sastojati samo od gumba za plaćanje tako da koristim blank temu.

Detaljnije o kreiranju Ionic 4 aplikacije moguće je pronaći u blog postu pod naslovom „Ionic 4 CRUD aplikacija“.

PayPal Checkout tj. Smart Payment Buttons omogućava brzu i jednostavnu implementaciju plaćanja u bilo koju aplikaciju na siguran način.

Implementacija počinje dodavanjem sljedeće skripte unutar index.html datoteke:

<script src="https://www.paypal.com/sdk/js?client-id=CLIENTID"></script>

Kao što se može vidjeti, ovdje mi je potreban ranije kreiran Client ID. Ako drugačije ne navedem zadana valuta biti će USD. U jednom od primjera kao valutu ću dodati EUR, parametar currency.

Popis svih dostupnih parametara moguće je vidjeti na poveznici https://developer.paypal.com/docs/checkout/reference/customize-sdk/.

index.html datoteka sada izgleda ovako:

<!DOCTYPE html>
<html lang="en">
 
<head>
  <meta charset="utf-8" />
  <title>Ionic 4 - PayPal</title>
 
  <base href="/" />
 
  <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
  <meta name="format-detection" content="telephone=no" />
  <meta name="msapplication-tap-highlight" content="no" />
 
  <link rel="icon" type="image/png" href="assets/icon/favicon.png" />
 
  <!-- add to homescreen for ios -->
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="apple-mobile-web-app-status-bar-style" content="black" />
 
  <script src="https://www.paypal.com/sdk/js?client-id=CLIENTID"></script>
  <!-- <script src="https://www.paypal.com/sdk/js?client-id=CLIENTID&currency=EUR"></script> -->
</head>
 
<body>
  <app-root></app-root>
</body>
 
</html>

Prikaz gumba na ekranu vrši se putem ID-a.

<div id="paypal-button-container"></div>

Sve zajedno to sada izgleda ovako:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic 4 - PayPal
    </ion-title>
  </ion-toolbar>
</ion-header>
 
<ion-content>
  <ion-row *ngIf="!placanjeUspjesno" text-center>
      <ion-col size-lg="4" offset-lg="4">
          <ion-card padding>
         <div id="paypal-button-container"></div>
          </ion-card>
      </ion-col>
  </ion-row>
  <ion-row *ngIf="placanjeUspjesno">
     <ion-col size-lg="4" offset-lg="5" text-center>
        <ion-card padding>
        <p>Thank you for your payment. Your transaction has been completed, and a receipt for your purchase has been emailed to you. Log into your PayPal account to view transaction details.</p>
      </ion-card>
      </ion-col>
  </ion-row>
</ion-content>

Koristim ngIf direktivu kako bi nakon uspješnog plaćanja umjesto gumba za plaćanje prikazao odgovarajuću poruku.

S obzirom da je u pitanju demo aplikacija na ekranu se neće nalaziti ništa drugo osim gumba za plaćanje na vrhu ekrana. U suprotnom bi se više moralo razmisliti o optimalnom mjestu za postavljanje gumba. Više o tome na poveznici https://developer.paypal.com/docs/checkout/best-practices/feature-paypal/#

Angular & Ionic 4 – PayPal Smart Payment

Inicijalizacija transakcije vrši se klikom na gumb “PayPal” tj. funkcijom createOrder unutar koje ću proslijediti samo iznos koji želim na/platiti.

createOrder: function (data, actions) {
   return actions.order.create({
          purchase_units: [{
            amount: {
              value: _this.paymentAmount
            }
          }]
   });
},

Kada kupac odobri transakciju poziva se funkcija onApprove.

onApprove: function (data, actions) {
           return actions.order.capture()
             .then(function (details) {
               console.log(details);
               if(details.status == "COMPLETED"){
                // Show a success message to the buyer
               //alert('Transaction completed by ' + details.payer.name.given_name + '!');
                  _this.placanjeUspjesno = true;
               } else {
                 alert("Neuspješno plaćanje!");
               }
             })
             .catch(err => {
               console.log(err);
             })
}

Kompletna funkcionalnost izgleda ovako:

import { Component } from '@angular/core';
 
declare var window: any;
 
@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
 
  paymentAmount: string = "10";
  placanjeUspjesno: boolean = false;
 
  constructor() {
     this.paymentOptions();
  }
 
  paymentOptions(){
    let _this = this;
    setTimeout(() => {
      // Render the PayPal button into #paypal-button-container
      window.paypal.Buttons({
 
        // Set up the transaction
        createOrder: function (data, actions) {
          return actions.order.create({
            purchase_units: [{
              amount: {
                value: _this.paymentAmount
              }
            }]
          });
        },
 
        // Finalize the transaction
         onApprove: function (data, actions) {
           return actions.order.capture()
             .then(function (details) {
               console.log(details);
               if(details.status == "COMPLETED"){
                // Show a success message to the buyer
               //alert('Transaction completed by ' + details.payer.name.given_name + '!');
                  _this.placanjeUspjesno = true;
               } else {
                 alert("Neuspješno plaćanje!");
               }
             })
             .catch(err => {
               console.log(err);
             })
         }
 
      }).render('#paypal-button-container');
    }, 500)
}
 
}

Demo prikaz

Sada ću proći kroz čitav proces naplate i prikazati ga slikama.

Klikom na žuti gumb „PayPal“ pokrećem postupak plaćanja. Prijavljujem se pomoću PayPal Sandbox računa.

PayPal Smart Payment

Biram na koji način želim platiti navedeni iznos. To mogu učiniti sa svojeg ukupnog balansa, kreditnom ili debitnom karticom koje su povezane s PayPal računom.

PayPal Smart Payment

Klikom na “Pay Now” potvrđujem plaćanje. Plaćanje je uspješno izvršeno ako dobijem (Order) ID. U ovom slučaju to je “id”: “99D86747XH516732E”.

PayPal Smart Payment

PayPal će mi vratiti sljedeći odgovor, u kojemu imam sve potrebno kako bi kasnije mogao raditi provjeri plaćanja, nakon što je plaćanje uspješno prošlo:

{
  "create_time": "2019-06-03T09:43:05Z",
  "update_time": "2019-06-03T09:43:05Z",
  "id": "99D86747XH516732E",
  "intent": "CAPTURE",
  "status": "COMPLETED",
  "payer": {
    "email_address": "tomislavstankovic1-buyer@gmail.com",
    "payer_id": "FW4BEH8QAPTEG",
    "address": {
      "country_code": "US"
    },
    "name": {
      "given_name": "test",
      "surname": "buyer"
    },
    "phone": {
      "phone_number": {
        "national_number": "4083320878"
      }
    }
  },
  "purchase_units": [
    {
      "reference_id": "default",
      "amount": {
        "value": "10.00",
        "currency_code": "USD"
      },
      "payee": {
        "email_address": "fortuno@example.com",
        "merchant_id": "RMF4F2P7GMT84"
      },
      "shipping": {
        "name": {
          "full_name": "test buyer"
        },
        "address": {
          "address_line_1": "1 Main St",
          "admin_area_2": "San Jose",
          "admin_area_1": "CA",
          "postal_code": "95131",
          "country_code": "US"
        }
      },
      "payments": {
        "captures": [
          {
            "status": "COMPLETED",
            "id": "9V075368A1755811N",
            "final_capture": true,
            "create_time": "2019-06-03T09:43:05Z",
            "update_time": "2019-06-03T09:43:05Z",
            "amount": {
              "value": "10.00",
              "currency_code": "USD"
            },
            "seller_protection": {
              "status": "ELIGIBLE",
              "dispute_categories": [
                "ITEM_NOT_RECEIVED",
                "UNAUTHORIZED_TRANSACTION"
              ]
            },
            "links": [
              {
                "href": "https://api.sandbox.paypal.com/v2/payments/captures/9V075368A1755811N",
                "rel": "self",
                "method": "GET",
                "title": "GET"
              },
              {
                "href": "https://api.sandbox.paypal.com/v2/payments/captures/9V075368A1755811N/refund",
                "rel": "refund",
                "method": "POST",
                "title": "POST"
              },
              {
                "href": "https://api.sandbox.paypal.com/v2/checkout/orders/99D86747XH516732E",
                "rel": "up",
                "method": "GET",
                "title": "GET"
              }
            ]
          }
        ]
      }
    }
  ],
  "links": [
    {
      "href": "https://api.sandbox.paypal.com/v2/checkout/orders/99D86747XH516732E",
      "rel": "self",
      "method": "GET",
      "title": "GET"
    }
  ]
}

Osim toga, potvrdu o uspješnom plaćanju mogu vidjeti ako se prijavim unutar PayPal Sandbox računa na adresi https://www.sandbox.paypal.com/ sa korisničkim imenom tomislavstankovic1-buyer@gmail.com i pripadajućom lozinkom.

PayPal Account Details

Ovdje mogu vidjeti koliki mi je trenutni tj. ukupni dostupan iznos na PayPal Sandbox računu kao i popis svih transakcija.

PayPal Account Details

Klikom na „Informatika Fortuno’s Test Store“ mogu vidjeti detalje navedene transakcije.

PayPal Transaction Summary

Zaključak

Što se klijentskog dijela aplikacije tiče to bi bilo sve.

PayPal Smart Payment Demo

Ostaje još napraviti serverski, backend, dio gdje će se vršiti provjera plaćanja. Na taj API šaljem ranije spomenuti ID “id”: “99D86747XH516732E”.

// 1. Set up your server to make calls to PayPal
 
// 1a. Import the SDK package
const checkoutNodeJssdk = require('@paypal/checkout-server-sdk');
 
// 1b. Import the PayPal SDK client that was created in `Set up Server-Side SDK`.
/**
 *
 * PayPal HTTP client dependency
 */
const payPalClient = require('../Common/payPalClient');
 
// 2. Set up your server to receive a call from the client
module.exports = async function handleRequest(req, res) {
 
  // 2a. Get the order ID from the request body
  const orderID = req.body.orderID;
 
  // 3. Call PayPal to get the transaction details
  let request = new checkoutNodeJssdk.orders.OrdersGetRequest(orderID);
 
  let order;
  try {
    order = await payPalClient.client().execute(request);
  } catch (err) {
 
    // 4. Handle any errors from the call
    console.error(err);
    return res.send(500);
  }
 
  // 5. Validate the transaction details are as expected
  if (order.result.purchase_units[0].amount.value !== '220.00') {
    return res.send(400);
  }
 
  // 6. Save the transaction in your database
  // await database.saveTransaction(orderID);
 
  // 7. Return a successful response to the client
  return res.send(200);
}

Struktura projekta prema package.json:

{
  "name": "Ionic4PayPal",
  "version": "0.0.1",
  "author": "Tomislav Stanković",
  "homepage": "https://www.tomislavstankovic.com/",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/common": "^7.2.2",
    "@angular/core": "^7.2.2",
    "@angular/forms": "^7.2.2",
    "@angular/http": "^7.2.2",
    "@angular/platform-browser": "^7.2.2",
    "@angular/platform-browser-dynamic": "^7.2.2",
    "@angular/router": "^7.2.2",
    "@ionic-native/core": "^5.0.0",
    "@ionic-native/splash-screen": "^5.0.0",
    "@ionic-native/status-bar": "^5.0.0",
    "@ionic/angular": "^4.1.0",
    "core-js": "^2.5.4",
    "rxjs": "~6.5.1",
    "tslib": "^1.9.0",
    "zone.js": "~0.8.29"
  },
  "devDependencies": {
    "@angular-devkit/architect": "~0.13.8",
    "@angular-devkit/build-angular": "~0.13.8",
    "@angular-devkit/core": "~7.3.8",
    "@angular-devkit/schematics": "~7.3.8",
    "@angular/cli": "~7.3.8",
    "@angular/compiler": "~7.2.2",
    "@angular/compiler-cli": "~7.2.2",
    "@angular/language-service": "~7.2.2",
    "@ionic/angular-toolkit": "~1.5.1",
    "@types/node": "~12.0.0",
    "@types/jasmine": "~2.8.8",
    "@types/jasminewd2": "~2.0.3",
    "codelyzer": "~4.5.0",
    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~4.1.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "~1.1.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.4.0",
    "ts-node": "~8.1.0",
    "tslint": "~5.17.0",
    "typescript": "~3.1.6"
  },
  "description": "Ionic 4 PayPal project"
}

P.S. Ovaj je blog post originalno objavljen na adresi https://www.fortuno.hr/ionic-4-paypal-smart-payment-buttons/.

Ionic 4 & Angular Router – prosljeđivanje parametara

Sličan blog post, pod naslovom “Ionic 2 – prosljeđivanje parametara između stranica“, sam već objavio, ali vrijeme je za novi jer Ionic 4 više ne radi isključivo na dosadašnji pop/push način, nego koristi Angular Router, pod uvjetom da se koristi i Angular Framework u pozadini.

Kreiranje projekta

Novi Ionic 4 projekt kreiram već dobro poznatom naredbom:

$ ionic start Ionic4AngularRouter blank
$ cd Ionic4AngularRouter

U ovom koraku trebam postaviti temelj tj. na HomePage komponenti gdje ću kreirati dva gumba kako bi mogao prikazati dva načina za prosljeđivanje parametara.

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic 4 - Angular Router
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <ion-button expand="full" color="dark" (click)="queryParamsFunction()">
    Query Params
  </ion-button>
 
  <ion-button expand="full" color="dark" (click)="navigationExtrasFunction()">
    Navigation Extras
  </ion-button>
</ion-content>

Na ekranu će to izgledati ovako:

Ionic 4 & Angular Router

Osim toga, moram pripremiti i neke podatke, about objekt, koje ću prosljeđivati na sljedeću stranicu.

import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  about = {
    name: 'Tomislav Stanković',
    website: 'www.tomislavstankovic.com',
    interests: [
      'Ionic', 'Angular', 'NodeJS'
    ]
  };

  constructor() {}

  queryParamsFunction(){}

  navigationExtrasFunction(){}

}

Sada mogu kreirati stranicu na koju ću proslijediti podatke i prikazati ih. To činim pomoću naredbe:

$ ionic generate page details

Ionic 4 & Angular Router

Prosljeđeni podaci će na ovom ekranu biti prikazani na sljedeći način:


  
    
      
    
    Angular Router / Details
  

 


  
    
      {{ data.name }}
    
    
        {{ data.website }}
    
  
  
    
      {{ i }}
    
  

Ionic 4 & Angular Router

Sada napokon mogu pokazati kako proslijediti podatke tj. parametre sa HomePage na DetailsPage na dva načina.

Prosljeđenim ću parametrima pristupiti kroz ActivatedRoute.

Query Params

Ovaj način je najčešći s kojim sam se susretao i ok je ako se radi mobilna aplikacija kojoj će se pristupati kroz npr. Google Play Store jer se u tom slučaju neće vidjeti URL svake od stranica.

http://localhost:8101/details?aboutData=%7B%22name%22:%22Tomislav%20Stankovi%C4%87%22,%22website%22:%22www.tomislavstankovic.com%22,%22interests%22:%5B%22Ionic%22,%22Angular%22,%22NodeJS%22%5D%7D

Podatke prosljeđujem sa HomePage

import { Component } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  about = {
    name: 'Tomislav Stanković',
    website: 'www.tomislavstankovic.com',
    interests: [
      'Ionic', 'Angular', 'NodeJS'
    ]
  };

  constructor(private _router: Router) {}

  queryParamsFunction(){
    this._router.navigate(['/details'], 
       { queryParams: 
           { aboutData: JSON.stringify(this.about) }
       }
    );
  }

  navigationExtrasFunction(){}

}

Na DetailsPage podatke dohvaćam na sljedeći način:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-details',
  templateUrl: './details.page.html',
  styleUrls: ['./details.page.scss'],
})
export class DetailsPage implements OnInit {

  data: any;

  constructor(private _activatedRoute: ActivatedRoute) { 

   this._activatedRoute.queryParams.subscribe(params => {
        if (params && params.aboutData) {
            this.data = JSON.parse(params.aboutData);
            console.log(this.data);
        }
    });  

  }

   ngOnInit() {}

}

U praksi to izgleda ovako:

Ionic 4 & Angular Router

Navigation Extras State

Kako bi ovo funkcioniralo potrebno je koristiti Angular 7.2 ili noviju verziju.

State passed to any navigation. This value will be accessible through the extras object returned from router.getCurrentNavigation() while a navigation is executing. Once a navigation completes, this value will be written to history.state when the location.go or location.replaceState method is called before activating of this route. Note that history.state will not pass an object equality test because the navigationId will be added to the state before being written.

While history.state can accept any type of value, because the router adds the navigationId on each navigation, the state must always be an object. – NavigationExtras

Ovo rješenje je slično onome iznad s tom razlikom što izgleda ljepše kada je vidljiv URL jer se parametri ne vide. To je posebno korisno kada se radi Ionic Progressive Web App.

http://localhost:8101/details

Podatke prosljeđujem sa HomePage

import { Component } from '@angular/core';
import { Router, NavigationExtras } from '@angular/router';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  about = {
    name: 'Tomislav Stanković',
    website: 'www.tomislavstankovic.com',
    interests: [
      'Ionic', 'Angular', 'NodeJS'
    ]
  };

  constructor(private _router: Router) {}

  queryParamsFunction(){}

  navigationExtrasFunction(){
    let navigationExtras: NavigationExtras = {
      state: {
        aboutData: this.about
      }
    };
    this._router.navigate(['/details'], navigationExtras);
  }

}

Na DetailsPage podatke dohvaćam na sljedeći način:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
  selector: 'app-details',
  templateUrl: './details.page.html',
  styleUrls: ['./details.page.scss'],
})
export class DetailsPage implements OnInit {

  data: any;

  constructor(private _activatedRoute: ActivatedRoute,
              private _router: Router) { 

      this._activatedRoute.queryParams.subscribe(params => {
          if (this._router.getCurrentNavigation().extras.state) {
              this.data = this._router.getCurrentNavigation().extras.state.aboutData;
              console.log(this.data);
          }
      });

  }

   ngOnInit() {
  }

}

U praksi to izgleda ovako:

Ionic 4 & Angular Router

Zaključak

Struktura projekta prema package.json

{
  "name": "Ionic4AngularRouter",
  "version": "0.0.1",
  "author": "Tomislav Stanković",
  "homepage": "https://www.tomislavstankovic.com/",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/common": "^7.2.2",
    "@angular/core": "^7.2.2",
    "@angular/forms": "^7.2.2",
    "@angular/http": "^7.2.2",
    "@angular/platform-browser": "^7.2.2",
    "@angular/platform-browser-dynamic": "^7.2.2",
    "@angular/router": "^7.2.2",
    "@ionic-native/core": "^5.0.0",
    "@ionic-native/splash-screen": "^5.0.0",
    "@ionic-native/status-bar": "^5.0.0",
    "@ionic/angular": "^4.1.0",
    "core-js": "^2.5.4",
    "rxjs": "~6.5.1",
    "tslib": "^1.9.0",
    "zone.js": "~0.8.29"
  },
  "devDependencies": {
    "@angular-devkit/architect": "~0.13.8",
    "@angular-devkit/build-angular": "~0.13.8",
    "@angular-devkit/core": "~7.3.8",
    "@angular-devkit/schematics": "~7.3.8",
    "@angular/cli": "~7.3.8",
    "@angular/compiler": "~7.2.2",
    "@angular/compiler-cli": "~7.2.2",
    "@angular/language-service": "~7.2.2",
    "@ionic/angular-toolkit": "~1.5.1",
    "@types/node": "~12.0.0",
    "@types/jasmine": "~2.8.8",
    "@types/jasminewd2": "~2.0.3",
    "codelyzer": "~4.5.0",
    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~4.1.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "~1.1.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.4.0",
    "ts-node": "~8.1.0",
    "tslint": "~5.16.0",
    "typescript": "~3.1.6"
  },
  "description": "An Ionic project"
}

Ionic 4 PWA – kreiranje aplikacije

Do sada sam u blog postovima pisao o Ionic aplikacijama koje svoju budućnost temelje na objavi na nekoj od trgovina aplikacijama u .apk ili .ipa obliku.

Ovaj će blog post ići u sasvim jednom drugom smjeru jer u potpunosti mogu zaobići trgovine aplikacijama i svoju PWA aplikaciju objaviti jednako kao što bi objavio bilo koju web stranicu.

Što je, Ionic, PWA?

Ionic 4 PWA je zapravo obična Ionic 4 aplikacija prilagođena prikazu na svim platformama bez potrebe za objavom putem Google Play Storea, Apple App Storea i sl.

Kreiranje projekta

Projekt kreiram već poznatom naredbom:

$ ionic start Ionic4PWA blank
cd Ionic4PWA

Ionic 4

U ovom koraku ovo je tek obična Ionic 4 aplikacija. Za sada neću instalirati Ionic Native pluginove jer mi za ovaj primjer neće biti potrebni.

 create mode 100644 .gitignore
 create mode 100644 angular.json
 create mode 100644 e2e/protractor.conf.js
 create mode 100644 e2e/src/app.e2e-spec.ts
 create mode 100644 e2e/src/app.po.ts
 create mode 100644 e2e/tsconfig.e2e.json
 create mode 100644 ionic.config.json
 create mode 100644 package-lock.json
 create mode 100644 package.json
 create mode 100644 src/app/app-routing.module.ts
 create mode 100644 src/app/app.component.html
 create mode 100644 src/app/app.component.spec.ts
 create mode 100644 src/app/app.component.ts
 create mode 100644 src/app/app.module.ts
 create mode 100644 src/app/home/home.module.ts
 create mode 100644 src/app/home/home.page.html
 create mode 100644 src/app/home/home.page.scss
 create mode 100644 src/app/home/home.page.spec.ts
 create mode 100644 src/app/home/home.page.ts
 create mode 100644 src/assets/icon/favicon.png
 create mode 100644 src/assets/shapes.svg
 create mode 100644 src/environments/environment.prod.ts
 create mode 100644 src/environments/environment.ts
 create mode 100644 src/global.scss
 create mode 100644 src/index.html
 create mode 100644 src/karma.conf.js
 create mode 100644 src/main.ts
 create mode 100644 src/polyfills.ts
 create mode 100644 src/test.ts
 create mode 100644 src/theme/variables.scss
 create mode 100644 src/tsconfig.app.json
 create mode 100644 src/tsconfig.spec.json
 create mode 100644 src/tslint.json
 create mode 100644 src/zone-flags.ts
 create mode 100644 tsconfig.json
 create mode 100644 tslint.json

Dodavanje PWA funkcionalnosti

Za razliku od Ionica 3 gdje je Service Worker bio dodan od trenutka kreiranja aplikacija i samo ga je trebalo odkomentirati unutar index.html datoteke kada je u pitaju Ionic 4 to više nije slučaj.

Ionic 3 PWA
How to make PWAs with Ionic 2017 – https://blog.ionicframework.com/how-to-make-pwas-with-ionic/

Pretpostavka za kreiranje PWA su Service Worker (ngsw-worker.js) i Web Manifest (manifest.json). Oboje ću u projekt dodati pomoću naredbe:

$ ng add @angular/pwa 

Pokretanjem te naredbe kreiraju se i ažuriraju razne datoteke.

Installed packages for tooling via npm.
CREATE src/ngsw-config.json (511 bytes)
CREATE src/manifest.json (1063 bytes)
CREATE src/assets/icons/icon-128x128.png (1253 bytes)
CREATE src/assets/icons/icon-144x144.png (1394 bytes)
CREATE src/assets/icons/icon-152x152.png (1427 bytes)
CREATE src/assets/icons/icon-192x192.png (1790 bytes)
CREATE src/assets/icons/icon-384x384.png (3557 bytes)
CREATE src/assets/icons/icon-512x512.png (5008 bytes)
CREATE src/assets/icons/icon-72x72.png (792 bytes)
CREATE src/assets/icons/icon-96x96.png (958 bytes)
UPDATE angular.json (5754 bytes)
UPDATE package.json (1809 bytes)
UPDATE src/app/app.module.ts (970 bytes)
UPDATE src/index.html (851 bytes)

– kreirana je konfiguracijska datoteka Service Workera ngsw-config.json. Trenutno se ovdje nalazi zadani naziv aplikacije “name”: “app” koji mogu promijeniti u “name”: “ionic4pwa”.

{
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/*.js"
        ]
      }
    }, {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
        ]
      }
    }
  ]
}

– ažurirana je angular.json datoteka kako bi podržavala Service Worker

angular.json pwa

– ažurirana je package.json datoteka s ranije dodanim @angular/pwa tj. @angular/service-worker paketom

ng add @angular/pwa

– ažurirana je app.module.ts datoteka koja služi za registraciju Service Workera. Ovdje se spominje ngsw-worker.js koja trenutno ne postoji unutar Ionic projekta, ali će se kreirati pokretanjem $ ionic build –prod naredbe.

ServiceWorkerModule

– ažurirana je index.html datoteka tj. <head> tag unutar kojega se spominje manifest.json datoteka. Ovdje također mogu promijeniti “name”: “app”, “short_name”: “app”, u “name”: “Ionic4PWA”, “short_name”: “Ionic4PWA”,.

{
  "name": "app",
  "short_name": "app",
  "theme_color": "#1976d2",
  "background_color": "#fafafa",
  "display": "standalone",
  "scope": "/",
  "start_url": "/",
  "icons": [
    {
      "src": "assets/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
...

index.html pwa manifest

– kreirane su ikone za različite platforme (zadane Angular ikone koje je za produkcijski projekt potrebno promijeniti)

PWA ikone

Produkcijska PWA aplikacija

Produkcijska PWA aplikacija kreira se naredbom:

$ ionic build --prod

Pokretanjem ove naredbe kreirat će se www mapa sa produkcijskom verzijom aplikacije spremnom za postavljanje na server.

ionic build --prod

Ovu PWA aplikaciju mogu pokrenuti i testirati naredbom:

$ http-server ./www -o

Zaključak

Struktura projekta prema package.json

{
  "name": "Ionic4PWA",
  "version": "0.0.1",
  "author": "Tomislav Stanković",
  "homepage": "https://www.tomislavstankovic.com/",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/common": "^7.2.2",
    "@angular/core": "^7.2.2",
    "@angular/forms": "^7.2.2",
    "@angular/http": "^7.2.2",
    "@angular/platform-browser": "^7.2.2",
    "@angular/platform-browser-dynamic": "^7.2.2",
    "@angular/pwa": "^0.12.4",
    "@angular/router": "^7.2.2",
    "@angular/service-worker": "^7.2.2",
    "@ionic-native/core": "^5.0.0",
    "@ionic-native/splash-screen": "^5.0.0",
    "@ionic-native/status-bar": "^5.0.0",
    "@ionic/angular": "^4.1.0",
    "core-js": "^2.5.4",
    "rxjs": "~6.5.1",
    "tslib": "^1.9.0",
    "zone.js": "~0.8.29"
  },
  "devDependencies": {
    "@angular-devkit/architect": "~0.13.8",
    "@angular-devkit/build-angular": "~0.13.8",
    "@angular-devkit/core": "~7.3.8",
    "@angular-devkit/schematics": "~7.3.8",
    "@angular/cli": "~7.3.8",
    "@angular/compiler": "~7.2.2",
    "@angular/compiler-cli": "~7.2.2",
    "@angular/language-service": "~7.2.2",
    "@ionic/angular-toolkit": "~1.5.1",
    "@types/node": "~12.0.0",
    "@types/jasmine": "~2.8.8",
    "@types/jasminewd2": "~2.0.3",
    "codelyzer": "~4.5.0",
    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~4.1.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "~1.1.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.4.0",
    "ts-node": "~8.1.0",
    "tslint": "~5.16.0",
    "typescript": "~3.1.6"
  },
  "description": "An Ionic project"
}

Ionic 4 Slides – prikaz galerije slika

O ovom ću blog postu pokazati nekoliko načina na koje je moguće koristiti ion-slides komponentu unutar Ionic 4 aplikacije.

ion-slides komponenta sastoji se od više zasebnih dijelova. Svaki od tih dijelova je zapravo jedna ion-slide komponenta.

Ionic 4 Slides
https://ionicframework.com/docs/api/slides

Ova se komponenta može koristiti za prikaz uputa prilikom prvog pokretanja mobilne aplikacije, primjer čega se nalazi u dokumentaciji, ali se isto tako pomoću ove komponente može napraviti lijepa galerija slika. Slike se mogu pomicati lijevo-desno, ali ih je isto tako moguće postaviti da se pomiču automatski u određenim intervalima.

S obzirom da ion-slides svoje temelje ima u Swiper.js-u i ondje je moguće pronaći dodatne opcije za upravljanje slajdovima.

Kreiranje aplikacije

$ ionic start IonicSlidesGallery blank
$ cd IonicSlidesGaller
$ionic serve

Struktura ekrana na kojemu ću testirati ion-slides izgleda ovako:

<ion-header text-center>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic 4 Slides
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  //ion-slides će ići ovdje
</ion-content>

Slike se nalaze unutar assets/imgs mape.

Ionic 4 - Assets Images

import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  imageContainer = [
    {url:"assets/imgs/image1.jpg"},
    {url:"assets/imgs/image2.jpg"},
    {url:"assets/imgs/image3.jpg"},
    {url:"assets/imgs/image4.jpg"} ];
 
  slideOpts = {
    //
  };

  constructor(){
  }

}

Galerija preko cijelog ekrana

Galerija preko cijelog ekrana

U ovom primjeru slike zauzimaju cijeli ekran. Dolaskom do zadnje slike nije moguće ići dalje loop: ‘false’.

<ion-header text-center>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic 4 Slides
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-slides pager="true" scrollbar="false" [options]="slideOpts" style="object-fit:cover;height:100%;background-color:#000;">
    <ion-slide *ngFor="let image of imageContainer; let i = index">
      <img src="{{image.url}}">
    </ion-slide>
  </ion-slides>
</ion-content>

Galerija preko jednog dijela ekrana

Galerija preko jednog dijela ekrana

U ovom primjeru galerija slika zauzima samo jedan dio ekrana ispod koje se onda vidi ostatak sadržaja. Dolaskom do zadnje slike moguće je ići dalje tj. napraviti puni krug do prve slike pomoću loop: ‘true’.

<ion-header text-center>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic 4 Slides
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-slides pager="true" [options]="slideOpts" scrollbar="false" style="background-size:cover;height:58%;background-color:#000;">
    <ion-slide *ngFor="let image of imageContainer; let i = index">
      <img src="{{image.url}}">
    </ion-slide>
  </ion-slides>
  <ion-card>
    <ion-card-header>
      <ion-card-subtitle>Ionic 4 Slides</ion-card-subtitle>
      <ion-card-title>Primjer galerije slika</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      <p>O ovom ću blog postu pokazati nekoliko načina na koje je moguće koristiti ion-slides komponentu unutar Ionic 4 aplikacije.</p>
      <p>ion-slides komponenta sastoji od više zasebnih dijelova. Svaki od tih dijelova je zapravo jedna ion-slide komponenta.</p>
    </ion-card-content>
  </ion-card>
</ion-content>

Zaključak

Struktura projekta prema package.json

{
  "name": "IonicSlidesGallery",
  "version": "0.0.1",
  "author": "Tomislav Stanković",
  "homepage": "https://www.tomislavstankovic.com/",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/common": "^7.2.2",
    "@angular/core": "^7.2.2",
    "@angular/forms": "^7.2.2",
    "@angular/http": "^7.2.2",
    "@angular/platform-browser": "^7.2.2",
    "@angular/platform-browser-dynamic": "^7.2.2",
    "@angular/router": "^7.2.2",
    "@ionic-native/core": "^5.0.0",
    "@ionic-native/splash-screen": "^5.0.0",
    "@ionic-native/status-bar": "^5.0.0",
    "@ionic/angular": "^4.0.0",
    "core-js": "^2.5.4",
    "rxjs": "~6.3.3",
    "zone.js": "~0.8.29"
  },
  "devDependencies": {
    "@angular-devkit/architect": "~0.12.3",
    "@angular-devkit/build-angular": "~0.12.3",
    "@angular-devkit/core": "~7.2.3",
    "@angular-devkit/schematics": "~7.2.3",
    "@angular/cli": "~7.2.3",
    "@angular/compiler": "~7.2.2",
    "@angular/compiler-cli": "~7.2.2",
    "@angular/language-service": "~7.2.2",
    "@ionic/angular-toolkit": "~1.4.0",
    "@types/node": "~10.12.0",
    "@types/jasmine": "~2.8.8",
    "@types/jasminewd2": "~2.0.3",
    "codelyzer": "~4.5.0",
    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~3.1.4",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "~1.1.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.4.0",
    "ts-node": "~8.0.0",
    "tslint": "~5.12.0",
    "typescript": "~3.1.6"
  },
  "description": "An Ionic 4 Slides project"
}

Otvaranje postavki mobilnog uređaja iz Ionic aplikacije

Prilikom izrade Ionic mobilnih aplikacija jedan od zahtjeva može biti da se iz aplikacije mogu otvoriti neke od postavki mobilnoj uređaja. Npr. postavke vezane uz geolokaciju, WiFi ili nešto treće. Puno je jednostavnije omogućiti korisniku da klikom na jedan gumb ode direktno na postavke koje su mu u tom trenutku potrebne kako bi unutar Ionic aplikacije nešto obavio nego da sam mora tražiti te iste postavke.

Od postavki kojima je moguće pristupiti koristeći ovaj plugin dostupno je sljedeće:

 "about", // ios
 "accessibility", // ios, android
 "account", // ios, android
 "airplane_mode", // ios, android
 "apn", // android
 "application_details", // ios, android
 "application_development", // android
 "application", // android
 "autolock", // ios
 "battery_optimization", // android
 "bluetooth", // ios, android
 "castle", // ios
 "captioning", // android
 "cast", // android
 "cellular_usage", // ios
 "configuration_list", // ios
 "data_roaming", // android
 "date", // ios, android
 "display", // ios, android
 "dream", // android
 "facetime", // ios
 "home", // android
 "keyboard", // ios, android
 "keyboard_subtype", // android
 "locale", // ios, android
"location", // ios, android
"locations", // ios
"manage_all_applications", // android
"manage_applications", // android
"memory_card", // android
"music", // ios
"music_equalizer", // ios
"music_volume", // ios
"network", // ios, android
"nike_ipod", // ios
"nfcsharing", // android
"nfc_payment", // android
"nfc_settings", // android
"notes", // ios
"notification_id", // ios
"passbook", // ios
"phone", // ios
"photos", // ios
"print", // android
"privacy", // android
"quick_launch", // android
"reset", // ios
"ringtone", // ios
"browser", // ios
"search", // ios, android
"security", // android
"settings", // ios, android
"show_regulatory_info",
"sound", // ios, android
"software_update", // ios
"storage", // ios, android
"store", // ios, android
"sync", // android
"tethering", // ios
"twitter", // ios
"touch", // ios
"usage", // ios, android
"user_dictionary", // android
"video", // ios
"voice_input", // android
"vpn", // ios
"wallpaper", // ios
"wifi_ip", // android
"wifi", // ios, android
"wireless" // android

Kreiranje aplikacije

Kreiram novi Ionic projekt i odmah dodajem Android platformu jer planiram aplikaciju pokrenuti na mobilnom uređaju.

$ ionic start IonicNativeSettings blank
$ cd IonicNativeSettings
$ ionic cordova platform add android

Open Native Settings

Plugin Open Native Settings instaliram sljedećim naredbama:

$ ionic cordova plugin add cordova-open-native-settings
$ npm install @ionic-native/open-native-settings

Nakon toga ovaj plugin deklariram unutar app.module.ts datoteke.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { OpenNativeSettings } from '@ionic-native/open-native-settings/ngx';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    OpenNativeSettings,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Funkcionalnost se nalazi unutar home.ts datoteke tj. unutar HomePage klase, a sastojat će se od funkcija od kojih svaka otvara neku od ranije navedenih postavki mobilnog uređaja.

Open Native Settings

import { Component } from '@angular/core';

import { OpenNativeSettings } from '@ionic-native/open-native-settings/ngx';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  constructor(private _openNativeSettings: OpenNativeSettings) { 
  }

   accessibility(){
     //this._openNativeSettings.open("accessibility");
     this._openNativeSettings.open(["accessibility", true]);
   }

   bluetooth(){
    //this._openNativeSettings.open("bluetooth");
    this._openNativeSettings.open(["bluetooth", true]);
   }

   date(){
    //this._openNativeSettings.open("date");
    this._openNativeSettings.open(["date", true]);
   }

   keyboard(){
    //this._openNativeSettings.open("keyboard");
    this._openNativeSettings.open(["keyboard", true]);
   }

   location(){
    //this._openNativeSettings.open("location");
    this._openNativeSettings.open(["location", true]);
   }

   manage_applications(){
    //this._openNativeSettings.open("manage_applications");
    this._openNativeSettings.open(["manage_applications", true]);
   }

   print(){
    //this._openNativeSettings.open("print");
    this._openNativeSettings.open(["print", true]);
   }

   storage(){
    //this._openNativeSettings.open("storage");
    this._openNativeSettings.open(["storage", true]);
   }

}

Ako neku od postavki pozivam na ovaj način this._openNativeSettings.open(“accessibility”) ekran s postavkama će se otvoriti umjesto ekrana Ionic aplikacije dok ako dodam parametar true na sljedeći način this._openNativeSettings.open([“accessibility”, true]) otvara se novi ekran s postavkama što se može vidjeti na sljedećoj slici.

Open Native Settings

Ovo na ekranu izgleda ovako:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Native Settings
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding text-center>
  <p>Plugin to open native screens of iOS/android settings.</p>
  <ion-row>
    <ion-col>
      <ion-button color="primary" (click)="accessibility()">Accessibility</ion-button>
      <ion-button color="secondary" (click)="bluetooth()">Bluetooth</ion-button>
    </ion-col>
  </ion-row>
  <ion-row>
    <ion-col>
      <ion-button color="tertiary" (click)="date()">Date</ion-button>
      <ion-button color="success" (click)="keyboard()">Keyboard</ion-button>
    </ion-col>
  </ion-row>
  <ion-row>
    <ion-col>
      <ion-button color="warning" (click)="location()">Location</ion-button>
      <ion-button color="danger" (click)="manage_applications()">Manage Applications</ion-button>
    </ion-col>
  </ion-row>
  <ion-row>
    <ion-col>
      <ion-button color="light" (click)="print()">Print</ion-button>
      <ion-button color="medium" (click)="storage()">Storage</ion-button>
    </ion-col>
  </ion-row>
</ion-content>

Open Native Settings

Zaključak

Struktura projekta prema package.json

{
  "name": "IonicNativeSettings",
  "version": "0.0.1",
  "author": "Tomislav Stanković",
  "homepage": "https://www.tomislavstankovic.com/",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/common": "^7.2.2",
    "@angular/core": "^7.2.2",
    "@angular/forms": "^7.2.2",
    "@angular/http": "^7.2.2",
    "@angular/platform-browser": "^7.2.2",
    "@angular/platform-browser-dynamic": "^7.2.2",
    "@angular/router": "^7.2.2",
    "@ionic-native/core": "^5.0.0",
    "@ionic-native/open-native-settings": "^5.0.0",
    "@ionic-native/splash-screen": "^5.0.0",
    "@ionic-native/status-bar": "^5.0.0",
    "@ionic/angular": "^4.0.0",
    "cordova-android": "7.1.4",
    "cordova-open-native-settings": "1.5.2",
    "cordova-plugin-device": "^2.0.2",
    "cordova-plugin-ionic-keyboard": "^2.1.3",
    "cordova-plugin-ionic-webview": "^3.1.2",
    "cordova-plugin-splashscreen": "^5.0.2",
    "cordova-plugin-statusbar": "^2.4.2",
    "cordova-plugin-whitelist": "^1.3.3",
    "core-js": "^2.5.4",
    "rxjs": "~6.3.3",
    "zone.js": "~0.8.29"
  },
  "devDependencies": {
    "@angular-devkit/architect": "~0.12.3",
    "@angular-devkit/build-angular": "~0.12.3",
    "@angular-devkit/core": "~7.2.3",
    "@angular-devkit/schematics": "~7.2.3",
    "@angular/cli": "~7.2.3",
    "@angular/compiler": "~7.2.2",
    "@angular/compiler-cli": "~7.2.2",
    "@angular/language-service": "~7.2.2",
    "@ionic/angular-toolkit": "~1.3.0",
    "@types/node": "~10.12.0",
    "@types/jasmine": "~2.8.8",
    "@types/jasminewd2": "~2.0.3",
    "codelyzer": "~4.5.0",
    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~3.1.4",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "~1.1.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.4.0",
    "ts-node": "~8.0.0",
    "tslint": "~5.12.0",
    "typescript": "~3.1.6"
  },
  "description": "An Ionic project",
  "cordova": {
    "plugins": {
      "cordova-open-native-settings": {},
      "cordova-plugin-whitelist": {},
      "cordova-plugin-statusbar": {},
      "cordova-plugin-device": {},
      "cordova-plugin-splashscreen": {},
      "cordova-plugin-ionic-webview": {
        "ANDROID_SUPPORT_ANNOTATIONS_VERSION": "27.+"
      },
      "cordova-plugin-ionic-keyboard": {}
    },
    "platforms": [
      "android"
    ]
  }
}