Capturing Moments: Click and Upload Photos to Firebase Cloud Storage using Capacitor Plugins and Firestore (Part 17) in Your Angular-15 Ionic-7 App

Welcome back to our ongoing series on building a multiplatform application with Angular-15 and Ionic-7! In our previous articles, we covered a wide range of topics, including authentication, data management, task organization, calendar integration, and event management. Today, in Part 17, we're diving into the world of media management by enabling users to click and upload photos to Firebase Cloud Storage using Capacitor plugins and Firestore.

In today's digital age, managing media assets is an essential aspect of many applications. By incorporating Capacitor plugins and Firestore in our Angular-15 Ionic-7 app, we can seamlessly integrate photo capture capabilities and enable users to upload their images to Firebase Cloud Storage. This functionality empowers users to share and store their visual content securely.


In this installment, we'll guide you through the process of integrating Capacitor plugins and Firestore into our app to enable photo capture and upload functionality. We'll start by setting up Capacitor and configuring the necessary plugins for accessing the device's camera and gallery. Next, we'll explore how to capture photos using the camera and select images from the gallery.

We'll then dive into the integration of Firebase Firestore, setting up the necessary configurations to store image metadata and references. By leveraging the power of Firebase Cloud Storage, we'll demonstrate how to upload captured or selected photos to the cloud, ensuring secure storage and accessibility for users.

By the end of this article, you'll have a solid understanding of how to leverage Capacitor plugins and Firestore to enable photo capture and upload capabilities in your Angular-15 Ionic-7 app.

So, join us in Part 17 of our series as we embark on a journey into the realm of media management. Together, let's empower our app with the ability to capture and upload photos, offering users a seamless and secure media-sharing experience. Get ready to enhance your app's functionality and user experience to new heights!

Tutorial

In the profile page, I have implemented a feature where the students can upload their profile either via their local file browser or via the in-built camera using some capacitor plugins and Firebase storage for storing image files. First, I will install the capacitor plugin from Installing Capacitor | Capacitor Documentation (capacitorjs.com) and Ionic PWA elements from @ionic/pwa-elements - npm (npmjs.com) which will allow the camera to work properly. I will use the Firebase Storage which I would have enabled from the Firebase console.

Next, I will create a service to handle the photo storage and upload in avatar.service.ts in the following way:

import { Injectable } from '@angular/core';
import { Auth } from '@angular/fire/auth';
import { doc, docData, Firestore, setDoc, getDoc,} from '@angular/fire/firestore';
import {getDownloadURL, ref, Storage, uploadString,} from '@angular/fire/storage';
import { Photo } from '@capacitor/camera';

@Injectable({
  providedIn: 'root',
})
export class AvatarService {
  constructor(private auth: Auth, private firestore: Firestore, private storage: Storage
  ) {}

  async getUserProfile() {
    const user = this.auth.currentUser;
    if (user) {
      const userDocRef = doc(this.firestore, `users/${user.uid}`);
      const userDoc = await getDoc(userDocRef);
      if (userDoc.exists()) {
        const userData = userDoc.data();
        return {
          imageUrl: userData['imageUrl'], // ensure the object has the imageUrl property
          // add other properties as needed
        };
      } else {
        return null;
      }
    } else {
      return null;
    }
  }

  async uploadAvatar(cameraFile: Photo) {
    // add the cameraFile parameter
    const user = this.auth.currentUser; // get the user's profile
    if (user) {
      const path = `uploads/${user.uid}/profile.png`; // create a path to store the image
      const storageRef = ref(this.storage, path); // get a reference to the storage bucket
      try {
        await uploadString(
          storageRef,
          cameraFile?.base64String || '',
          'base64'
        ); // upload the image to the storage bucket
        //  This expression uses optional chaining (?.) to safely access the base64String property only if cameraFile is not undefined. If cameraFile is undefined, the expression evaluates to the empty string (''), which can safely be passed as the third argument to uploadString.
        const imageUrl = await getDownloadURL(storageRef); // get the image url
        const userDocRef = doc(this.firestore, `users/${user.uid}`); // get the user's document reference
        await setDoc(userDocRef, { imageUrl }); // update the user's document with the image url
        return true; // return the image url
      } catch (e) {
        return null;
      }
    } else {
      return null;
    }
  }
}

The above class, AvatarService has three constructor parameters: auth, firestore, and storage, which are used to handle user authentication, Firestore database operations, and storage operations respectively.

The getUserProfile() method retrieves the current user's profile data, including their profile image URL, from the Firestore database. If the user is not signed in, null is returned.

The uploadAvatar() method takes a cameraFile parameter, which is an object containing a base64-encoded image file. It uploads the image to the Firebase storage bucket and updates the user's profile in Firestore with the image URL. If the upload is successful, it returns true. If the user is not signed in or there is an error during the upload, null is returned.

Here's a brief overview of the code flow in uploadAvatar() method:

Retrieve the current user's profile. ⇒ Create a path for the image in the storage bucket using the user's UID. ⇒ Get a reference to the storage bucket using the path. ⇒ Upload the base64-encoded image to the storage bucket using the uploadString() function from the Firebase storage API. ⇒ Get the download URL of the uploaded image using the getDownloadURL() function from the Firebase storage API. ⇒ Update the user's profile in Firestore with the image URL using the setDoc() function from the Firestore API. ⇒ Return true if the upload is successful, or null if there is an error.

Next, I will import the service into the profile page in tab4.page.ts in the following way:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule, ToastController } from '@ionic/angular';
import { RouterLink, Route } from '@angular/router';
import { environment } from 'src/environments/environment';
import { AvatarService } from 'src/app/services/avatar.service';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import { AlertController, LoadingController } from '@ionic/angular';

// import { ProfileDataService } from 'src/app/services/profile-data.service';

@Component({
  selector: 'app-tab4',
  templateUrl: './tab4.page.html',
  styleUrls: ['./tab4.page.scss'],
  standalone: true,
  imports: [IonicModule, CommonModule, FormsModule, RouterLink],
})
export class Tab4Page implements OnInit {
  // set profile to null if no profile is found in the database and then set it to the profile variable if a profile is found in the database and initialize the profile variable to null
  profile: { imageUrl: string | null } = { imageUrl: null };

  constructor(
    private avatarService: AvatarService,
    private loadingController: LoadingController,
    private alertController: AlertController,
    private toastController: ToastController
  ) {}

  async ngOnInit() {
    try {
      // below code sets the profile to null if no profile is found in the database
      // wait for the profile to be retrieved from the database and then set it to the profile variable or set it to null if no profile is found
      this.profile = (await this.avatarService.getUserProfile()) || {
        imageUrl: null,
      }; // if no profile, set to null
    } catch (e) {
      console.log('Error getting user profile', e); // log error
    }
  }

  // function to open camera and take a photo with slight modifications to the above function
  async addCameraImage() {
    // async function because it uses await to wait for the camera to open and take a photo and then upload the photo to the database and then reload the profile to get the new image and then display a toast message to the user.

    // open camera from web browser and take a photo
    const image = await Camera.getPhoto({
      quality: 90,
      allowEditing: false,
      resultType: CameraResultType.Base64,
      source: CameraSource.Prompt, // Source of image is set to prompt so that the user can choose between camera and photo library
    });

    if (image) {
      // if image is not null
      const loading = await this.loadingController.create(); // create a loading controller
      await loading.present(); // present the loading controller

      const result = await this.avatarService.uploadAvatar(image); // upload the image to the database
      await loading.dismiss(); // dismiss the loading controller

      // reload the profile to get the new image
      this.profile = (await this.avatarService.getUserProfile()) || {
        // if no profile, set to null
        imageUrl: null,
      };
      // display a toast message to the user
      if (result) {
        // if result is true
        const toast = await this.toastController.create({
          message: 'Profile Pic Updated.', // display message
          duration: 3000, // display for 3 seconds
        });
        await toast.present(); // present the toast message
      }

      // display an alert message to the user
      // if the upload failed
      if (!result) {
        // if result is false
        const alert = await this.alertController.create({
          header: 'Upload failed', // display message
          message: 'There was a problem uploading your avatar.',
          buttons: ['OK'],
        });
        await alert.present(); // present the alert message
      }
    }
  }
}

The above code defines a component that displays a user's profile picture and allows them to update it by taking a photo with the device camera or selecting an existing photo from the photo library. The Tab4Page component implements the OnInit interface to define the ngOnInit() method, which is called when the component is initialized. It also defines a profile object with an imageUrl property that is initialized to null. The component injects the AvatarService, LoadingController, AlertController, and ToastController services in the constructor using dependency injection. The ngOnInit() method calls the getUserProfile() method of the AvatarService to retrieve the user's profile picture from the database. If the user has a profile picture, the imageUrl property of the profile object is set to the image URL. If the user does not have a profile picture, the imageUrl property is set to null. If there is an error retrieving the profile picture, an error message is logged to the console.

The addCameraImage() method is called when the user wants to update their profile picture. The method uses the Camera plugin to open the device camera and take a photo. The photo is then uploaded to the database using the uploadAvatar() method of the AvatarService. While the upload is in progress, a loading controller is displayed to the user. Once the upload is complete, the loading controller is dismissed, and the getUserProfile() method of the AvatarService is called again to retrieve the updated profile picture. If the upload is successful, a toast message is displayed to the user confirming that their profile picture has been updated. If the upload fails, an alert message is displayed to the user informing them of the problem.

Next, I will use the above logic on the profile page in tab4.page.html in the following way:

<ion-header [translucent]="false">
  <ion-toolbar>
    <ion-title>My Profile</ion-title>
    <ion-buttons slot="start">
      <ion-button routerLink="/tabs/notifications">
        <ion-icon slot="icon-only" name="notifications-outline"></ion-icon>
      </ion-button>
    </ion-buttons>
    <ion-buttons slot="end">
      <ion-menu-button menu="main-menu"></ion-menu-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">My Profile</ion-title>
    </ion-toolbar>
  </ion-header>

  <!-- My Profile details in an ion card -->
  <ion-card>
    <!-- Add a profile image which will be uploaded and updated from firebase cloud storage -->
    <div class="preview" (click)="addCameraImage()">
      <ion-img
        *ngIf="profile.imageUrl; else placeholder_avatar"
        [src]="profile.imageUrl"
      ></ion-img>
      <!-- Add a template which will be visible as placeholder if there is no uploaded file on storage database -->
      <ng-template #placeholder_avatar>
        <div class="fallback">
          <p>Upload Profile Pic</p>
        </div>
      </ng-template>
    </div>
    <ion-card-header>
      <ion-card-subtitle>D22124440</ion-card-subtitle>
      <ion-card-title>Karan Gupta</ion-card-title>
    </ion-card-header>

    <ion-card-content>
      <ion-accordion-group expand="full">
        <ion-accordion>
          <ion-item slot="header" color="light">
            <ion-label>View Details</ion-label>
          </ion-item>
          <div class="ion-padding" slot="content">
            <ion-list lines="none">
              <ion-item>
                <ion-label>Student ID: D22124440</ion-label>
              </ion-item>
              <ion-item>
                <ion-label>Course: Msc CDM & UX</ion-label>
              </ion-item>
              <ion-item>
                <ion-label>Year: 1</ion-label>
              </ion-item>
              <ion-item>
                <ion-label>Phone: 087 1234567</ion-label>
                <ion-label></ion-label>
              </ion-item>
              <ion-item>
                <ion-label>Email: D22124440@mytudublin.ie</ion-label>
              </ion-item>
            </ion-list>
          </div>
        </ion-accordion>
      </ion-accordion-group>
    </ion-card-content>
  </ion-card>

  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button (click)="addCameraImage()"
      ><ion-icon name="camera-outline"></ion-icon
    ></ion-fab-button>
  </ion-fab>
</ion-content>
<!-- tab4.page.scss file for placeholder graphic-->
.fallback {
height: 300px;
  background-color: #ccc;
  display: flex;
  justify-content: center;
  align-items: center;
  font-weight: 500;
}

The above HTML code uses an <ion-card> component to display the profile details, which includes a profile image. The image is stored in Firebase Cloud Storage and is retrieved through a URL stored in the profile.imageUrl variable. The profile.imageUrl variable is checked to see if it has a value using the *ngIf directive. If there is a value, the image is displayed using the <ion-img> component. Otherwise, a placeholder template is displayed.

When the user clicks on the profile image, the addCameraImage() function is called. This function opens the device's camera or photo library to allow the user to upload or take a new profile picture. A new image is uploaded to the Firebase Cloud Storage and the profile.imageUrl variable is updated with the new URL. If the upload is successful, a toast message is displayed to the user indicating that the profile picture has been updated. If the upload fails, an alert message is displayed to the user. There is also an <ion-fab> component that displays a camera icon button. When the user clicks on this button, the addCameraImage() function is called, which allows the user to upload or take a new profile picture defined previously.

And that’s it. The profile picture feature is functional as demonstrated in Figures 72, 73, and 74 below:

Figure 72: Upload Pic Options



Figure 73: Camera Opens



Figure 74: Pic Uploaded to User


Conclusion

In this seventeenth installment of our series on building a multiplatform application with Angular-15 and Ionic-7, we explored the integration of Capacitor plugins and Firestore to enable photo capture and upload functionality within our app. By leveraging these powerful technologies, we created a seamless and secure media management system that allows users to click photos using their device's camera or select images from their gallery and upload them to Firebase Cloud Storage.

In today's digital landscape, managing and sharing visual content is a fundamental aspect of many applications. By incorporating Capacitor plugins and Firestore in our Angular-15 Ionic-7 app, we provided users with a comprehensive solution for capturing and uploading photos. Users can effortlessly capture moments using their device's camera or select images from their gallery, and securely store them in Firebase Cloud Storage.

Throughout this tutorial, we guided you through the process of setting up the Capacitor and configuring the necessary plugins for photo capture and gallery access. We also explored the integration of Firestore, demonstrating how to store image metadata and references. By leveraging the power of Firebase Cloud Storage, we enabled seamless and secure photo uploads, ensuring the preservation and accessibility of visual content.

We hope this tutorial has provided you with valuable insights and practical knowledge on integrating Capacitor plugins and Firestore into your Angular-15 Ionic-7 app to enable photo capture and upload capabilities. By embracing the power of these technologies, you can create an app that offers a seamless and secure media-sharing experience for your users.

Thank you for joining us on this journey as we explored the fascinating world of app development with Angular-15, Ionic-7, Capacitor plugins, Firestore, and photo management. Stay tuned for future installments where we will continue to explore new features and functionalities to make our app even more robust and user-friendly.

Happy coding!

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript