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:

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
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.
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.
Klikom na „Create Account“ kreiram Sandbox PayPal račun fortuno@example.com.
My Apps & Credentials
Nakon toga na adresi https://developer.paypal.com/developer/applications kreiram profil aplikacije putem koje ću izvršavati plaćanje.
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.
Nakon toga dobijem Client ID koji će mi biti potreban kasnije i bez kojega ne mogu izvršiti plaćanje.
Smart Payment Buttons & Ionic 4
Sada ću kreirati novi Ionic 4 projekt. To činim naredbom:
1 |
$ 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:
1 |
<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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<!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¤cy=EUR"></script> --> </head> <body> <app-root></app-root> </body> </html> |
Prikaz gumba na ekranu vrši se putem ID-a.
1 |
<div id="paypal-button-container"></div> |
Sve zajedno to sada izgleda ovako:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<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/#
Inicijalizacija transakcije vrši se klikom na gumb “PayPal” tj. funkcijom createOrder unutar koje ću proslijediti samo iznos koji želim na/platiti.
1 2 3 4 5 6 7 8 9 |
createOrder: function (data, actions) { return actions.order.create({ purchase_units: [{ amount: { value: _this.paymentAmount } }] }); }, |
Kada kupac odobri transakciju poziva se funkcija onApprove.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
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.
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.
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 ć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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
{ "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.
Ovdje mogu vidjeti koliki mi je trenutni tj. ukupni dostupan iznos na PayPal Sandbox računu kao i popis svih transakcija.
Klikom na „Informatika Fortuno’s Test Store“ mogu vidjeti detalje navedene transakcije.
Zaključak
Što se klijentskog dijela aplikacije tiče to bi bilo sve.
Ostaje još napraviti serverski, backend, dio gdje će se vršiti provjera plaćanja. Na taj API šaljem ranije spomenuti ID "id": "99D86747XH516732E".
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
{ "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/.