Firebase Transactions

First Published: 2 July 2018
Updated on: 24 July 2019

This is the fifth post of a 7 post series that will teach you how to build an app using Ionic Framework and Firebase from scratch.

We are going to keep working with lists, right now we will start creating a list of guest for an event.

We do not need to create any more pages than there are, we already have everything we need from the previous chapter.

The first thing we will need to do is to change some stuff from the previous chapter.

Go into event.service.ts and we will add a new function there to add the guests to the event so inside the provider add the function:

addGuest(
  guestName: string,
  eventId: string,
  eventPrice: number
): Promise<firebase.firestore.DocumentReference> {
  return this.eventListRef
    .doc(eventId)
    .collection('guestList')
    .add({ guestName });
}

Something simple, we are pushing a new guest to the event, it only has the guest’s name (for now).

This way we are saving an event’s guest list, we might need it later.

Now we can focus on the event-detail folder, go to event-detail.ts and let’s create an addGuest() function, we will use it for adding new guests to our events, go ahead and add it:

addGuest(guestName: string): void {
  this.eventService
    .addGuest(
      guestName,
      this.currentEvent.id,
      this.currentEvent.price,
    )
    .then(() => this.guestName = '' );
}

Nothing weird going on, we are calling the addGuest function from our event provider, then passing it the guestName, our event’s id and the event’s ticket price (we will use this last one for updating the revenue).

Remember to declare the guestName variable we’re using:

public guestName = '';
constructor(...){...}

Now we are going to be adding some things to our event-detail.page.html file.

The first thing we will add is a header to show our revenue updating in real time (Yes, we will get to the JS for that part soon) go ahead and add a header to the first card:

<ion-card>
  <ion-card-header>
    Event's Revenue:
    <span
      [class.profitable]="currentEvent?.revenue > 0"
      [class.no-profit]="currentEvent?.revenue < 0 "
    >
      {{currentEvent?.revenue | currency}}
    </span>
  </ion-card-header>
  <ion-card-content>
    <p>Ticket: <strong>${{currentEvent?.price}}</strong></p>
    <p>Date: <strong>{{currentEvent?.date}}</strong></p>
  </ion-card-content>
</ion-card>

There we are adding the event’s revenue, and we are telling Ionic, hey if the income is less than 0 add the no-profit CSS class, and if the income is greater than 0 go ahead and add the profitable CSS class.

By the way, profitable adds green color and no-profit red color, in fact, take the CSS for this file, add it straight into event-detail.page.scss

.add-guest-form {
  ion-card-header {
    text-align: center;
  }
  button {
    margin-top: 16px;
  }
}

.profitable {
  color: #22bb22;
}

.no-profit {
  color: #ff0000;
}

And lastly we will need a way to add our guests, create a text input for the guest’s name and a button to send the info:

<ion-card class="add-guest-form">
  <ion-card-header> Add Guests </ion-card-header>
  <ion-card-content>
    <ion-item>
      <ion-label position="stacked">Name</ion-label>
      <ion-input
        [(ngModel)]="guestName"
        type="text"
        placeholder="What's your guest's name?"
      ></ion-input>
    </ion-item>

    <ion-button
      color="primary"
      expand="block"
      (click)="addGuest(guestName)"
      [disabled]="!guestName"
    >
      Add Guest
    </ion-button>
  </ion-card-content>
</ion-card>

This calls the addGuest() function from event-detail.page.ts and adds the guest to the event.

Creating a Firestore Transaction

But Jorge, how does this updates the revenue for the event?

I wanted to leave this part for last because it needs a little explanation of why first.

I am going to use a feature from Firestore called transaction() is a way to update data to ensure there’s no corruption when being updated by multiple users.

For example, let’s say Mary downloads the app, but she soon realizes that she needs some help at the front door, there are way too many guests and if she is the only one registering them it is going to take too long.

So she asks Kate, Mark, and John for help, they download the app, log in with Mary’s password (Yeah, it could be a better idea to make it multi-tenant :P ), and they start registering users too.

What happens if Mark & Kate both register new users, when Mark’s click reads the revenue it was $300 so his app took those $300, added the $15 ticket price and the new revenue should be $315 right? Wrong!

It turns out that Kate registered someone else a millisecond earlier, so the revenue already was at $315 and Mark set it to $315 again, you see the problem here right?

This is where transactions come in. They update the data safely. The update function takes the current state of the data as an argument and returns the new desired state you would like to write.

If another client writes to the location before you store your new value, your update function is called again with the new current value, and then Firestore retries your write operation.

And they are not even hard to write, go ahead to event.service.ts and add a .then() function for the addGuest() it used to look like this:

addGuest(
  guestName: string,
  eventId: string,
  eventPrice: number
): Promise<firebase.firestore.DocumentReference> {
  return this.eventListRef
    .doc(eventId)
    .collection('guestList')
    .add({ guestName });
}

Now it should look like this:

addGuest(guestName: string, eventId: string, eventPrice: number): Promise<void> {
  return this.eventListRef
    .doc(eventId)
    .collection('guestList')
    .add({ guestName })
    .then((newGuest) => {
      return firebase.firestore().runTransaction(transaction => {
        return transaction.get(this.eventListRef.doc(eventId)).then(eventDoc => {
          const newRevenue = eventDoc.data().revenue + eventPrice;
          transaction.update(this.eventListRef.doc(eventId), { revenue: newRevenue });
        });
      });
    });
}

The transaction takes the current state of the event and updates the revenue property for it, and then it returns the new value making sure it is correct.

Next Steps

I need to stop this lesson here, mainly because I ran out of coffee, see you in the next lesson!

Next Lesson: Handling Firebase Cloud Storage.