Payment Request API: bezpieczne płatności w przeglądarce

Płatności online niejednokrotnie okazują się być problematyczne. W szczególności na telefonach – przypomnij sobie kiedy ostatni raz chciałaś/eś za coś zapłacić, ale zrezygnowałaś/eś, bo wpisywanie wszystkich danych i numeru karty na telefonie Cię zmęczyło? Właśnie. Ja miewam tak często. Na szczęście koniec tej męki wydaje się być bliski: Wchodzi Payment Request API!

Problemy z płatnościami

O ile Polacy bardzo często płacą po prostu szybkimi przelewami lub BLIK (jedynym sensownym sposobem płatności online), to jednak coraz częściej zamawiamy też rzeczy z zagranicy – a tam przelew zwyczajnie się nie opłaca lub jest w ogóle niemożliwy. Co wtedy? Podajesz numer karty. I tak za każdym razem, na każdej stronie, na której chcesz zapłacić. Oczywiście wielu sprzedawców oferuje opcję zapamiętania numeru karty, ale czy jesteś pewien, że możesz mu do końca ufać? Ja nigdy.

Z punktu widzenia programistów, sprzedawców czy startuperów sprawa wcale nie jest prostsza – i to co najmniej z kilku powodów. Posługiwanie się Twoim numerem karty to jedno, ale aby móc ten numer zapisać i przechować trzeba mieć już specjalne zgody – w Polsce bodajże od GIODO, a na rynek światowy sprawa chyba jeszcze bardziej skomplikowana…

Dodatkowo, aby wpisywanie danych było maksymalnie uproszczone, programiści muszą poprawnie skonfigurować pola formularza, aby działało autouzupełnianie. Wbrew pozorom, nie jest to tak proste, jak brzmi, bo różne przeglądarki różnie interpretują te same wartości mimo prób standaryzacji.

Payment Request API

Payment Request API to standard mający na celu ułatwienie przeprowadzania transakcji między klientami a sprzedawcami. A konkretnie: Payment Request API zdecydowanie upraszcza krok, w którym musisz podać swoje dane w celu przeprowadzenia płatności. Otrzymujemy tutaj jakby w pakiecie 3 rzeczy:

  1. Spójny wygląd i interfejs dla użytkowników (zawsze ten sam).
  2. Bezpieczniejsze miejsce do przechowywania danych – przeglądarka, a nie serwer sprzedawcy.
  3. Spójne programistyczne API dla twórców aplikacji i sklepów.

Jak wygląda Payment Request API dla użytkownika?

Zależnie od urządzenia i przeglądarki, ale w danym środowisku zawsze tak samo 🙂 To jest ogromny plus tego standardu! Spójrzmy na kilka screenów z różnych urządzeń:

Niezależnie od sklepu, w którym chcesz dokonać płatności – Payment Request API będzie dla Ciebie wyglądał identycznie. Oczywiście, standard jest dość elastyczny, na podsumowaniu może się wyświetlać wiele produktów, a także osobno podatek czy rabat, możliwe jest także sprecyzowanie które karty kredytowe są akceptowane. Ale to nie wszystko! Google zaprezentował piękny przykład dodania zupełnie własnościowej bramki płatności, np. Android Pay poprzez Stripe. Wszystko jest możliwe! Czytaj dalej, aby dowiedzieć się jak 🙂

Bezpieczny sposób przechowywania danych

Na pierwszym screenie znajduje się przycisk z napisem „Add” (korzystam z angielskiej wersji przeglądarki). Po jego kliknięciu możemy skonfigurować dany rodzaj płatności (karta), dodać wszystkie potrzebne dane i zapisać. W tym wypadku Chrome poinformował mnie również, że informacje o płatności będą przechowywane w chmurze Google. Prawdopodobnie bezpieczniejsze to niż  przechowywanie tych danych w byle jakim sklepie internetowym, ale w razie czego tę funkcję również można wyłączyć. Co istotne, przy kolejnych transakcjach, we wszystkich sklepach internetowych, będziemy mieli szybki dostęp do zapisanych danych i nie będziemy musieli ich ponownie podawać. O ile korzystają one z Payment Request API, oczywiście 🙂

Warto jednak pamiętać, że po zaakceptowaniu dane są przekazywane do sprzedawcy. A więc sprzedawca nadal dostaje np. numer naszej karty i, jeśli podaliśmy go na podejrzanej stronie, to prawdopodobnie możemy się z pieniędzmi pożegnać. Payment Request API nie jest bramką płatności samo w sobie. Nie sprawia też, że transakcje stają się pewniejsze czy bezpieczniejsze w żaden sposób. Nie jest pośrednikiem w płaceniu. Payment Request API to tylko (albo aż) spójny wygląd, interfejs i API.

Programowanie Payment Request API

Specyfikacja Payment Request API jest długa, ale całkiem interesująca i pełna przykładów – warto do niej zajrzeć, jeśli zainteresujesz się głębiej tematem: https://www.w3.org/TR/payment-request/. Ale omówmy sobie kilka podstawowych rzeczy.

Tylko HTTPS

Przede wszystkim: Payment Request API nie działa bez HTTPS. Podobnie jak wiele nowych API. Jest to bardzo sensowne, bo przesyłanie naszych danych bez szyfrowania nie można nazwać inaczej niż skrajną głupotą – tutaj szyfrowanie jest wymuszone.

Promise

Całe Payment Request API opiera się o Promise’y. Jest to bardzo spójna i dobra decyzja, gdyż od pewnego czasu Promise jest częścią standardu ECMAScript, a więc jest wbudowany w JavaScript. Dzięki temu uzyskujemy jeden, wspólny sposób wchodzenia w interakcję z nowymi asynchronicznymi API.

Przykłady Payment Request API

Dość teorii! Praktyka. Payment Request API dodaje nowy globalny konstruktor, z którego możemy korzystać: PaymentRequest. Przyjmuje on 3 argumenty, ale ostatni jest opcjonalny:

const paymentMethods = [
  {supportedMethods: ['basic-card']}
];

const details = {
  total: {
    label: 'Dostęp do artykułów Type of Web', 
    amount: {currency: 'PLN', value: '99.99'}
  }
};

const options = {}; // opcjonalnie

new PaymentRequest(paymentMethods, details, options)
  .show()

W powyższym przykładzie robimy 3 rzeczy: Deklarujemy jakie metody płatności wspieramy (karty), dodajemy jedną pozycję na liście („Dostęp do artykułów…”) wraz z jej ceną i walutą, a na końcu każemy wyświetlić okno z płatnością. Jakie to proste!

Karty płatnicze

Zauważ jednak, że aktualnie informujemy użytkownika, że obsługujemy dowolnych dostawców kart:

Możliwe jest sprecyzowanie, że obsługujemy wyłącznie np. Visa i Mastercard. Nic prostszego:

const paymentMethods = [
  {
    supportedMethods: ['basic-card'],
    data: {  
      supportedNetworks: ['visa', 'mastercard']
    }
  }
];

Zmieniamy fragment konfiguracji i widzimy już tylko dwóch dostawców na liście:

Lista produktów i podatki

Bardzo często w zamówieniach musimy wylistować więcej niż jeden produkt, a do tego osobno wyświetlić naliczony podatek VAT. Znowu, Payment Request API przychodzi z pomocą. Dodamy sobie te rzeczy jako tablicę w details.displayItems:

const details = {
  displayItems: [{
    label: 'Abonament roczny',
    amount: { currency: 'PLN', value: '99.99' }
  }, {
    label: 'Rabat 10% dla stałych czytelników',
    amount: { currency: 'PLN', value: '-10.00' }
  }, {
    label: 'VAT 23%',
    amount: { currency: 'PLN', value: '16.83' }
  }],
  total: {
    label: 'Suma',
    amount: { currency: 'PLN', value: '89.99' }
  }
};

Tak przygotowana lista wyświetla się w następujący sposób. Zwróć uwagę, że w displayItems mogłem podać zupełnie dowolne wartości i opisy i nie ma to wpływu na sumę w total. Przeglądarka niczego za nas sama nie liczy i to my musimy poprawnie wykonać wszystkie obliczenia!

Jako dodatkową opcję, Payment Request API umożliwia nam oznaczenie niektórych pozycji na liście zakupów jako „oczekujące” – tzn. takie, których ceny nie są finalne. Przykładem podawanym w dokumentacji jest właśnie podatek lub koszt wysyłki, który może zależeć od wagi przesyłki albo kraju. W tym celu wystarczy dodać do odpowiedniego obiektu pole pending, a przeglądarka może (ale nie musi) wyświetlić je nieco inaczej niż pozostałe:

const details = {
  displayItems: [{
    label: 'Przesyłka',
    amount: { currency: 'PLN', value: '13.50' },
    pending: true // !
  }],
  ...
};

Obsługa danych

Jak wspomniałem, po wywołaniu funkcji show, dostajesz Promise, w którym następnie możesz obsłużyć płatność. Rezultatem jest obiekt, który zawiera w sobie wszystkie potrzebne dane, a także funkcję complete. Zakładając, że masz swoją funkcję doSomethingWithTheData, która np. wysyła dane do API:

new PaymentRequest(paymentMethods, details, options)
  .show()
  .then(result => {
    return doSomethingWithTheData(result)
      .then(response => {
        if (response.ok) {
          return result.complete('success'); // udało się zapłacić
        } else {
          return result.complete('fail'); // niepowodzenie
        }
      })
  })
  .catch(err => {
    // … obsłuż błąd
  })

Co dokładnie zawiera w sobie result? Przynajmniej dane karty i adres, a opcjonalnie kilka innych informacji (czytaj dalej). Więcej o konkretnych polach można przeczytać w specyfikacji Payment Request API.

Błąd wyświetlony w przeglądarce po wywołaniu result.complete('fail')

Najbardziej rozbudowany przykład

Do tej pory całkowicie pomijałem obiekt options. Muszę jednak o nim wspomnieć, gdyż pozwala on na dodanie tak przydatnych opcji jak wymuszenie wpisania adresu wysyłki, maila czy numeru telefonu. Dodatkowo, na zmiany tych wartości można odpowiednio reagować i, na przykład, zmieniać koszt przesyłki. Służy temu funkcja event.updateWith(…). Zobaczmy bardzo rozbudowany przykład – ten sam co w demo poniżej (skocz do demo ↓):

const shipmentItems = {
  economy: {
    id: 'economy',
    label: 'Ekonomiczna (5-30 dni)',
    selected: true,
    amount: {
      currency: 'PLN',
      value: '7.5',
    },
  },
  pickup: {
    id: 'pickup',
    label: 'Odbiór własny',
    amount: {
      currency: 'PLN',
      value: '0',
    },
  }
};

const bucket = [{
  label: 'Abonament roczny',
  amount: { currency: 'PLN', value: '99.99' }
}, {
  label: 'Rabat 10% dla stałych czytelników',
  amount: { currency: 'PLN', value: '-10.00' }
}];

const paymentMethods = [
  {
    supportedMethods: ['basic-card'],
    data: {
      supportedNetworks: ['visa']
    }
  }
];

const shippingOptions = [shipmentItems.economy, shipmentItems.pickup];
const displayItems = [...bucket, shipmentItems.economy];

const details = {
  displayItems,
  shippingOptions,
  total: getTotal(displayItems)
};

const options = {
  requestPayerName: true,
  requestPayerEmail: true,
  requestPayerPhone: true,
  requestShipping: true,
  shippingType: 'shipping'
};

function showPayment() {
  const payment = new PaymentRequest(paymentMethods, details, options);
  payment.addEventListener('shippingoptionchange', onShippingOptionChange);

  payment
    .show()
    .then(onPaymentSuccess)
    .catch(onPaymentError)
}

function onShippingOptionChange(e) {
  // … update

  event.updateWith({
    total: getTotal(displayItems),
    shippingOptions,
    displayItems,
  });
}

function onPaymentSuccess(result) {
  return doSomethingWithTheData(result)
    .then(response => {
      if (response.ok) {
        return result.complete('success'); // udało się zapłacić
      } else {
        return result.complete('fail'); // niepowodzenie
      }
    })
}

function onPaymentError(err) {
  console.error(err);
}

Demo

Poniższe demo przedstawia najciekawsze możliwości Payment Request API. Jeśli używasz Google Chrome (również na telefonie), Edge lub Opera to spróbuj:

  1. Dodać kartę. Ale nie swoją prawdziwą! Użyj numeru 4444 3333 2222 1111, który jest poprawnym numerem VISA do testowania. CVC i data ważności dowolne.
  2. Dodaj adres.
  3. Dodaj dane kontaktowe.
  4. Zmień sposób dostarczenia przesyłki na odbiór własny i z powrotem na przesyłkę ekonomiczną – suma do zapłaty ulega zmianie.
  5. Sfinalizuj płatność. Na 3 sekundy pojawi się spiner, a następnie okno zniknie. Udało się!

Wsparcie przeglądarek dla Payment Request API

Dobra wiadomość: Możesz zacząć używać Payment Request API już dzisiaj! API jest dostępne w najnowszych wersjach Google Chrome, Edge i Opera. Trwają prace nad dodaniem go do Safari, a testować można już w Safari Tech Preview.

Jak sobie poradzić z przeglądarkami, które jeszcze Payment Request API nie wspierają? Musisz polegać na dotychczasowych sposobach obsługi płatności, czyli zwykłych formularzach. Na szczęście wykrycie tego czy Payment Request API jest dostępne czy nie, jest bajecznie proste:

if (!window.PaymentRequest) {
  // nie ma, wyświetl stary formularz np:
  oldPaymentForm.hidden = false;
  // albo przekieruj:
  window.location.href = '/old-payment-form.html';
  return;
}

Podsumowanie

Krótkie. Pobaw się Demo, w szczególności na telefonie z mobilnym Chrome. Zobacz o ile łatwiejsze może być uzupełnianie danych do płatności online głównie na telefonie, ale też desktopie. Ja w to wierzę i mocno kibicuję Payment Request API, czyli nowemu sposobowi na obsługę płatności w przeglądarkach 🙂

  • API jest fajne, ale wydaje się aż zbyt uproszczone, co może być wynikiem kolejnych przepychanek wewnątrz W3C: http://manu.sporny.org/2016/browser-api-incubation-antipattern/

    TL;DR Google i MS stworzyły swoją wersję standardu pokazując środkowy palec Community Group, które od de facto lat pracowało nad tematem.

    • Nie lubię tego typu wpisów, w których ktoś wylewa swoją frustrację zamiast spróbować obiektywnie wyjaśnić problem. Mało konkretów, za dużo fochów.

      • Problem jest taki, że WHATWG i grupy skupione wokół twórców przeglądarek w W3C dyktują warunki i to widać nie tylko w tym standardzie, ale także w wielu innych. Co by daleko nie szukać: sytuacja z `main` w HTML, gdzie wersja WHATWG jest wyciągnięta z kosmosu, bo Hixie tak napisał i tak ma być i nie słuchamy argumentów, bo wszystkie są głupie i nieobiektywne → https://github.com/whatwg/html/issues/100

        A poziom argumentów ad personam czy upartego odpowiadania „nie” na wszystkie propozycje community osiągnął już swoje apogeum w dyskusji nad Web Components, zwłaszcza nad nieszczęsnym atrybutem `[is]`, w której z 10 razy powołano się na anegdotę o długopisie NASA i ołówku (tak, serio: https://github.com/w3c/webcomponents/issues/509#issuecomment-265542471 ) i w której 20 razy przedstawiciel Apple odpowiedział „nie”, bo nie i zaproponował rozwiązanie 20 razy bardziej skomplikowane (rozbicie wszystkich elementów HTML na poszczególne mixiny). Oczywiście ta propozycja zabiła całą dyskusję i `[is]` (w gruncie rzeczy nie aż tak koszmarne w implementacji) wisi w próżni, bo Apple zatkało uszy na komentarze community.

        Więc w sumie… jakie mają być konkrety? Konkret jest taki, że ktoś powiedział „nie” i odwrócił wzrok, gdy padły argumenty.

        • Śledziłem swego czasu listy dyskusyjne i zawsze mam wrażenie, że spora wina leży jednak po stronie społeczności – bo w wielu przypadkach to grupa bardzo aroganckich i zbyt pewnych siebie, ale zdolnych i inteligentnych ludzi. No i jest konflikt…

  • veranoo

    Zastanawiam się czy przeglądarka jest właściwym miejscem żeby trzymać tam tak wrażliwe dane, jakimi są dane karty kredytowej.

    • Dlaczego? 🙂

      • veranoo

        W windowsie bardzo łatwo zczytać informację, np historię przeglądania, zapisane hasła, kwestia szyfrowania tych danych.

        • Co to znaczy, żę „bardzo łatwo da się zczytać”? Jeśli wiesz o jakimś błędzie bezpieczeństwa, szczególnie tak poważnym, to powinieneś go natychmiast zgłosić do Microsoftu/Google o.O

          W innym wypadku – nie ma takiej możliwości, poza oczywiście przypadkami gdy jakiś 0-day czy inny bug się znajdzie… Ale jeśli jesteś użytkownikiem, który ma aktualnego Windowsa i aktualną przeglądarkę to powiedziałbym, że ryzyko jest zdecydowanie mniejsze niż gdyby zapisywać dane karty w różnych sklepach internetowych.