Tap to Pay on any device by using an NFC tag
Tags: webdev, showdev, ios, android
Published: 2023-11-14 08:05:19

> _A web app implementing the ideas described in this post can be found at [Tappie.money](tappie.money)._

A while ago, Apple released [Tap to Pay on iPhone](https://developer.apple.com/tap-to-pay/), which allows an iPhone to act as a payment terminal (also known as SoftPOS). There are [several](https://www.adyen.com/devices/tap-to-pay-on-android) [third](https://www.sumup.com/en-gb/tap-first/visa/) [parties](https://www.ccv.eu/en/solutions/payment-services/ccvsoftpos/) offering similar functionality for Android.

The major selling point is that it allows the user to accept payments without the need for additional hardware. However, there are some downsides to the current offerings:
* Requires the user to sign an agreement with a third party payment provider
* Often high transaction costs
* Only available in a few regions
* Only available for a few specific devices

In this post, we will attempt to create similar functionality, without the downsides listed above.

## Building our own Tap to Pay solution

When we break it down, Tap to Pay consists of two functionalities:
* Create a payment request on a phone
* Allow request to be paid by tapping the phone

### Creating a payment request

Fortunately, it’s already possible to create payment requests on your phone, either through most banking apps, or through third party services like [Tikkie](https://www.tikkie.me/).

> `https://mybank.com/paymentrequest/1234`

All we need now, is a way to offer these payment requests to a customer device by tapping the phone.

### Delivering a payment request

Tap to Pay on iPhone uses the NFC chip in the phone, as does the Android version. Unfortunately, using the NFC chip for payment is [restricted to participating payment providers](https://developer.apple.com/documentation/proximityreader).

We can work around this issue by using a standard NFC tag, instead of the NFC chip in the phone. These tags are very cheap, as they usually cost less than $0.20, or one could use an old hotel room key or public transport ticket. 

An advantage of this approach is that it allows us to physically separate the payment terminal from the phone, and get creative with the placement of the tappable surface. Instead of sticking the NFC tag to a phone, we could also stick the tag to the counter in a store, or on a product.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/svp6nu9cvy80js7vdvqz.jpeg">
  <figcaption>Fig.1 - NFC tags</figcaption>
</figure>

NFC tags can store URLs, which, when tapped by a device with NFC reading capabilities, usually redirect the user to the read link. Payment requests, as created in banking apps, often come in the form of a URL. This means we can store a payment request on an NFC chip, which, when tapped, sends the customer to the payment page.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zkhudwi8rtfiynxxfu39.png">
  <figcaption>Fig.2 - NFC tag with URL, read by phone</figcaption>
</figure>

### Reduce write actions to improve UX

Writing a new payment request to the NFC tag for each payment seems like a hassle. Fortunately, we can work around this issue by using a redirect URL. The URL we store on the tag points to a page we control. This page retrieves the URL of the current payment request from our database, and redirects the user to this URL. 

With a simple CRUD application, we can update the payment request the tag points to, without having to write anything to the tag. We can password protect this operation, so only the owner of the tag can update the current payment request.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6z3wr5mviovvoy42rm9g.png">
  <figcaption>Fig.3 - NFC tag with redirect URL, payment request URL retrieved from DB</figcaption>
</figure>

Now that we store the redirect URL on the tag, we don’t have to write to the tag for each new payment. However, we still need to do an initial write operation to store the redirect URL on the tag. We can use the [Web NFC API](https://developer.mozilla.org/en-US/docs/Web/API/Web_NFC_API) for this. We simply generate a unique redirect URL, and write this to the tag with the press of a button. In this process, we also allow the user to password protect their tag.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gyp12pjj7511yihl32ay.png">
  <figcaption>Fig.4 - Writing redirect URL to tag</figcaption>
</figure>

### Streamline process of updating payment request to improve UX

Although this works, updating a payment request is quite a lot of work, as it requires us to copy-paste the payment request URL from the banking app to our solution. We can make this easier by creating separate PWAs for separate tags, and register these PWAs as [share targets](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target). Now, if our banking app offers a Share button after creating a payment request, we can simply send the request to our app through the system share dialog.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ibjcl3du8k6kfonfsdz5.png">
  <figcaption>Fig.5 - Send payment request to tag through system share dialog</figcaption>
</figure>

## Conclusion

We now have a solution which is similar to Apple’s Tap to Pay functionality. Let’s list the pros and cons of this solution:

Pros:
* Does not require a third party payment provider
* Transaction costs your bank offers
* Available in all regions
* Available on all internet connected devices (although you do need a NFC supporting device for initial setup)
* Surface to tap can be your phone, but can also be placed on the counter of your business, or on the product itself

Cons:
* Requires additional hardware (the NFC tag)
* Payment process is less streamlined, as setting up a payment request requires using multiple services

I built a simple application around this idea, called [Tappie.money](tappie.money). It’s free to use, so, if you have an NFC tag, please give it a try :).