Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Angular Stripe Payments Part 3 - Sell Digital Content

Episode 26 written by Jeff Delaney
full courses for pro members

Health Check: This lesson was last reviewed on and tested with these packages:

  • Angular v4.2
  • AngularFire2 v4

Update Notes: Serious about Stripe Payments in your Angular Firebase app? Check out the Full Stack Stripe Payments course.

Find an issue? Let's fix it

This is part 3 of our Stripe Payments with Angular series. If you’re just getting started, check out:

Now that we have the ability to collect payments from customers, we need a way to apply those payments in the app. There are several ways you might approach this problem.

  • Account Deposit (our method)
  • Shopping cart
  • Single product purchase
  • Subscription

Each of these methods have their own benefits and drawbacks. In my opinion, the account deposit method is the most flexible and is relatively simple to implement.

demo of digital payment using stripe in Angular 4

Account Deposit Method

The account deposit payment strategy enables users to deposit funds on there account, which can be used to access restricted content. Sometimes developers might rename the underlying currency to coins, tokens, or credits to distinguish funds that can only be spent within a specific marketplace. This payment model works especially well for apps selling digital content or pay-per-use features.

For example, IconFinder requires users to deposit funds in specific increments, which can then be used to unlock digital content. Burner App allows users to purchase credits for private phone numbers, which get debited based on usage.

Database Structure

There are only two types of transactions that can affect a user’s balance in our app. (1) An successful charge via Stripe. (2) Digital content purchase. Here’s how we will structure the database.

-|users
-|$userId
balance: number

-|purchases
-|$userId
-|$itemId
amount: number
timestamp: number


-|charges
(see parts 1 and 2)

Enforcing an Atomic Operation in Firebase

When dealing with people’s money, you need to be especially careful to avoid data anomalies in NoSQL. Although unlikely, it is possible that one operation succeeds, while the other fails, causing a data mismatch or anomaly. In other words, the purchase would be recorded, but the balance would remain the same, or vice versa.

Thankfully, Firebase supports a multi location update technique that will force the operation to fail/succeed in unison, which is known as an atomic operation in database theory. In the following sections, we will use atomic updates to prevent anomalies in our payment and purchase data.

Update the Payment Cloud Function

Our cloud function needs to be modified to update the user’s balance after the charge is recorded. Let’s first add an extra variable to keep track of the user’s existing balance. When the charge is received from Stripe, we credit the user balance by the charge amount.

To perform an atomic update, we save the operations in an object where the database reference path is the key and the data is the value. Then you can reference the root of the database and pass this updates object to the update method.

functions/index.js

exports.stripeCharge = functions.database
.ref('/payments/{userId}/{paymentId}')
.onWrite(event => {



const payment = event.after.val();
const userId = event.params.userId;
const paymentId = event.params.paymentId;

let balance = 0;


// checks if payment exists or if it has already been charged
if (!payment || payment.charge) return;

return admin.database()
.ref(`/users/${userId}`)
.once('value')
.then(snapshot => {
return snapshot.val();
})
.then(customer => {

balance = customer.balance

const amount = payment.amount;
const idempotency_key = paymentId; // prevent duplicate charges
const source = payment.token.id;
const currency = 'usd';
const charge = {amount, currency, source};


return stripe.charges.create(charge, { idempotency_key });

})

.then(charge => {

if (!charge) return;

let updates = {}
updates[`/payments/${userId}/${paymentId}/charge`] = charge

// If successful charge, increase user balance
if (charge.paid) {
balance += charge.amount
updates[`/users/${userId}/balance`] = balance
}

// Run atomic update
admin.database().ref().update(updates)
})

});

Update the Payment Service

getUserBalace() - The service constructor is updated pull the current user’s balance and return it as an Observable, by using switchMap, instead of subscribe.

hasPurchased() - Returns a boolean observable telling us if an item was already purchased.

buyDigitalContent() - To complete a purchase, we need another atomic operation to simultaneously update the user’s balance and purchase history.

Note how we are using the Firebase server timestamp. This prevents data integrity issues with JavaScript date objects caused by timezones and local clock settings.

payment.service.ts

import { Injectable } from '@angular/core';
import { AngularFireDatabase } from 'angularfire2/database';
import { AngularFireAuth } from 'angularfire2/auth';

import * as firebase from 'firebase/app';
import 'rxjs/add/operator/switchMap';


@Injectable()
export class PaymentService {

userId: string;
balance: number;

constructor(private db: AngularFireDatabase, private afAuth: AngularFireAuth) {
this.getUserBalance()
.subscribe(balance => this.balance = balance)
}

getUserBalance() {
return this.afAuth.authState.switchMap(auth => {
this.userId = auth.uid
return this.db.object(`/users/${this.userId}/balance`)
.map(balance => balance.$value)
})
}

hasPurchased(buyableId) {
return this.db.object(`/purchases/${this.userId}/${buyableId}`)
.map(purchase => !!purchase.timestamp)
}

buyDigitalContent(buyableKey: any, amount: number) {
const timestamp = firebase.database.ServerValue.TIMESTAMP
const purchase = { timestamp, amount }
let updates = {}

const newBalance = this.balance - amount


updates[`/purchases/${this.userId}/${buyableKey}`] = purchase
updates[`/users/${this.userId}/balance`] = newBalance

return this.db.object('/').update(updates)
}


processPayment(token: any, amount: number) {
const payment = { token, amount }
return this.db.list(`/payments/${this.userId}`).push(payment)
}


}

Buy Now Component

ng g c payments/buy-now --module payments/payment

The buy-now component is designed for re-usability, so you can attach it to any “buyable” content by passing it a unique id and price. Most commonly, you would pass it a Firebase push $key and the price from a parent component.

<buy-now
[buyableId]="'angular-firebase-survival-guide'"
[price]="2000">
</buy-now>

To prevent accidental purchases, it uses two-steps to confirm the user’s intention. When the purchase component is clicked it brings up a confirmation window, showing the user the change to their current balance in a modal window. They can then click “Confirm” or “Cancel”.

buy-now.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { PaymentService } from '../payment.service';

@Component({
selector: 'buy-now',
templateUrl: './buy-now.component.html',
styleUrls: ['./buy-now.component.scss']
})
export class BuyNowComponent implements OnInit {

@Input() buyableId; // unique Id for any product
@Input() price;

showModal = false;

balance;
hasPurchased;


constructor(private paymentSvc: PaymentService) { }

ngOnInit() {
this.balance = this.paymentSvc.getUserBalance()

this.hasPurchased = this.paymentSvc.hasPurchased(this.buyableId)
}

toggleModal() {
this.showModal = !this.showModal
}

confirmPurchase() {
this.paymentSvc.buyDigitalContent(this.buyableId, this.price)
.then(() => {

this.showModal = false;

})
}

}

Create a Stripe Pipe

I created pipe to present the balance in a user-friendly format because Stripe uses integers representing 1/100th of their underlying currency amount. (500 == $5.00)

ng g pipe payments/stripe --module payments/payment

stripe.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'stripe'
})
export class StripePipe implements PipeTransform {

transform(value: number): string {
if (!value) { return "$0.00"}

return `$${(value/100).toFixed(2)}`;
}

}

HTML Template

In the template, we are using Bulma’s modal CSS, but this process works equally well with Bootstrap, Material, or Ionic.

In the template, we have a payment button that start the purchase process by firing the toggleModel() function. Bulma has an is-active CSS class that toggles the modal’s visibility. The modal window displays the change to the user’s balance and the confirmation button. When clicked it will perform the atomic update defined in the service, giving the user access to the digital content.

buy-now.component.html

<div class="modal" [class.is-active]="showModal">
<div class="modal-background"></div>
<div class="modal-content box" *ngIf="(balance | async) >= price">

<h2>Confirm your purchase!</h2>

<p>Current Balance: {{ balance | async | stripe }}</p>

<p>New balance: {{ (balance | async) - price | stripe }}.</p>

<button (click)="confirmPurchase()" class="button is-success">Confirm</button>
<button (click)="toggleModal()" class="button is-warning">Cancel</button>

</div>

<div class="modal-content box" *ngIf="(balance | async) < price">
Insufficient Funds. Current balance: {{ (balance | async) | stripe }}
</div>

<button class="modal-close is-large" (click)="toggleModal()"></button>
</div>

<button (click)="toggleModal()" class="button is-info" *ngIf="!(hasPurchased | async)">
Buy Now for {{ price | stripe }}
</button>

<div class="button is-success" *ngIf="hasPurchased | async" disabled>
Already Purchased!
</div>

<p>Your Current Balance: {{ balance | async | stripe }}</p>

Extra Backend Security

Don’t forget to add backend security rules. At the very least, you should have purchases locked down by auth UID. You might also want to keep records as “read only” after they are created to prevent accidental or malicious deletion/altering of purchase data.

"purchases": {
"$uid": {
"$itemId": {
".write": "auth.uid === $uid
&& !data.exists()"
}
}
}

Next Steps

In upcoming installments I will talk about building subscription models with stripe and processing refunds.