How to validate a phone number by sending an SMS code using Ionic,Django and SmsApi ?

In this tutorial, we will learn how to enter an international phone number within a Ionic application, and how to validate this phone number by sending an SMS code using a Django backend and SMSAPI services.

Please notice, you should have read previous tutorials to know how to create and manage API services with Ionic (like checking the network, displaying loading alert, passing data to pages) or how to manage API with Django and Rest framework.

You can find source code on my Github

How to enter an international phone number using ionic ?

First we will create a new Ionic/Capacitor project called PhoneValidation

ionic start PhoneValidation blank --capacitor --project-id=phonevalidation --package-id=com.idevotion.phonevalidation

Next we will install and use an external library to deal with international phone number (no need to re-invent the wheel).

npm install --force ion-intl-tel-input ionic-selectable flag-icon-css google-libphonenumber --save

To use this library we need to import it. So we will modify the home.module.ts file to import the library and also to use Reactive Forms with Angular.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule,ReactiveFormsModule } from '@angular/forms';
import { HomePage } from './home.page';
import { HomePageRoutingModule } from './home-routing.module';
import { IonIntlTelInputModule } from 'ion-intl-tel-input';

 @NgModule({
  imports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    IonicModule,
    IonIntlTelInputModule,
    HomePageRoutingModule
  ],
  declarations: [HomePage]
})
export class HomePageModule {}

Then we will modify the home.page.html to use the library within a reactive form

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Phone Validation
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <form [formGroup]="form" (ngSubmit)="onSubmit()">
    <ion-item>
      <ion-label position="floating">Tel Input</ion-label>
      <ion-intl-tel-input inputPlaceholder="Your phone number"  
      [defaultCountryiso]="FR"
      [preferredCountries]="preferredCountries"
        formControlName="phoneNumber" >
      </ion-intl-tel-input>
    </ion-item>
    <ion-button mode="ios" expand="block" color="primary" type="submit">
      Validate
    </ion-button>
  </form>
</ion-content>

and the home.page.ts file to declare our form and select some preferred countries.

import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  formValue = {phoneNumber: '', test: ''};
  form: FormGroup;
  preferredCountries = ["fr","gb","us"]
  constructor() { }

  ngOnInit() {
    this.form = new FormGroup({
      phoneNumber: new FormControl({
        value: this.formValue.phoneNumber
      })
    });
  }

  get phoneNumber() { return this.form.get('phoneNumber'); }

  onSubmit() {
    console.log(this.phoneNumber.value);
  }

}

To display country flag into the modal list of countries, we need to import a specific css file. To do so, we need to edit our global.scss file and add the line

@import "node_modules/flag-icon-css/sass/flag-icon.scss";

Ok so now we can enter a phone number and when clicking on the validate button, the onSubmit function will be called and will display the phone number information

{internationalNumber: "+34 695 96 XX XX", nationalNumber: "695 96 XX XX", isoCode: "es", dialCode: "+34"}

At this point we need to send the phone number information to our Django backend, which will send an SMS with a code that the user will need to enter on a new page to validate his phone number.

Let’s see first how we can send an sms using Django and a sms provider like www.smsapi.com or whatever sms provider you will choose.

How to send an sms code using Django and SMSAPI ?

As we learned in previous tutorials how to create and manage a backend using Django (and deploying it using Clever cloud), we will just focus on how to send the Sms code.

To use SMSAPI with Django, we need to edit our requirements.txt file and add the library

smsapi-client==2.4.2

Then we will create a new endpoint in our API by adding in our urls.py file

 url(r'^sendSmsCode/$',
        sendSmsCode,
        name='sendSmsCode'),

And then we will write the view sendSmsCode which will generate a code, send it by sms and return a JSON response to our Ionic application with the code generated:

def generateSmsCode():
    # select a random sample without replacement
    codeSms = ''.join(random.choice(string.digits) for _ in range(4))
    return codeSms

@api_view(['GET'])
def sendSmsCode(request):
    from phonevalidation.settings import SMSAPI_API_KEY
    client = SmsApiComClient(access_token=SMSAPI_API_KEY)
    phone = request.GET.get('phone', None)
    code = generateSmsCode()
    message="SMS code:"+code
    try:
      client.sms.send(to=phone, message=message)
      smsSent = SMSSent()
      smsSent.phoneNumber = phone
      smsSent.codeEnvoye = code
      smsSent.save()
      json = {"status":"OK","code": code}
    except Exception as e:
        print(e)
        json = {"status": "KO"}

    return JsonResponse(json)

Of course you need to create and set a valid SMSAPI_API_KEY to use smsapi.com service.

How to check and validate an SMS code using Ionic ?

Now on our submit method, we can:

. check if network is available

. call our new API and passed the phone number that the user entered.

. check our JSON response status. If it’s ok we can go to a new page/url : sms/code otherwise we should display an error to the end user.

 onSubmit() {
    console.log(this.phoneNumber.value);
    if (this.apiService.networkConnected){
      let formatedNumber = this.phoneNumber.value.internationalNumber
      this.apiService.sendSmsCode(formatedNumber).subscribe((results)=>{
        if (results){
          let statuts = results["status"]
          if (statuts=="OK"){
            let code = results["code"]
            this.navData.setDataWithoutId({
                  "phoneNumber": formatedNumber,
                  "smscode": code
                })
                this.router.navigate(['/sms-code']).catch(err => console.error(err));
          }
          else{
            //Display error
          }

        }
        else{
          //Display error
        }
      })
    }    
  }

Now we need to create the page that will ask the sms code received and will verify it with the code sent by our backend.

First let’s add our new page

ionic g page SmsCode  

Now let’s edit our sms-code.page.html

<ion-header class="ion-no-border" mode="ios">
  <ion-toolbar mode="ios">
 <ion-buttons slot="start">
   <ion-back-button defaultHref="home"></ion-back-button>
 </ion-buttons>
 <ion-title>Code</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div class="page-flex-align code">
    <div class="top-content ion-text-center">
      <h1>Validation code</h1>
      <p>Please enter the code send to 
        <br>Phone: {{phoneNumber}}</p>
      <ion-list mode="ios" formCodeValidate lines="none">
        <ion-item >
          <ion-input #codesInpunt0 (keyup)="changeFocus(1)"  [(ngModel)]="code[0]" type="tel" maxlength="1"  ></ion-input>
        </ion-item>
        <ion-item >
          <ion-input #codesInpunt1 (keyup)="changeFocus(2)" [(ngModel)]="code[1]" type="tel" maxlength="1"  ></ion-input>
        </ion-item>
        <ion-item >
          <ion-input #codesInpunt2 (keyup)="changeFocus(3)" [(ngModel)]="code[2]" type="tel" maxlength="1"  ></ion-input>
        </ion-item>
        <ion-item >
          <ion-input #codesInpunt3  (keyup)="changeFocus(4)"   [(ngModel)]="code[3]" type="tel" maxlength="1"  ></ion-input>
        </ion-item>
      </ion-list>

    </div>
  </div>
</ion-content>

and the sms.code.page.scss tohave our digit input design

ion-list {
    &[form] {
        ion-item {
            &.item {
                --border-width: 1px;
                --border-radius: 5px;
                --min-height: 60px;
                --ion-border-color: rgba(0, 0, 0, 0.10);

                ion-select {
                    --padding-start: 0;
                    width: 100%;
                    //font-size: 1.8rem;
                    max-width: unset;
                }

                &+.item {
                    margin-top: 16px;
                }
            }

        }

        ion-row {
            &[lrm] {
                margin-left: -8px;
                margin-right: -8px;
            }

            ion-col {
                padding: 8px;
                padding-top: 16px;
            }
        }
    }

 &[formCodeValidate] {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-align: center;
    -ms-flex-align: center;
    align-items: center;
    -webkit-box-pack: justify;
    -ms-flex-pack: justify;
    justify-content: space-between;
    max-width: 310px;
    margin-left: auto;
    margin-right: auto;

    ion-item {
        &.item {
            --border-width: 1px;
            --border-radius: 5px;
            --min-height: 60px;
            max-width: 55px;
            text-align: center;
            font-size: 18px;
        }

    }
    }
}

and the sms-code.page.ts to verify if the code entered by the user is matching the code sent by our backend

import { ChangeDetectorRef, Component, NgZone, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { Platform, ToastController } from '@ionic/angular';
import { NavDataServiceService } from '../services/nav-data-service.service';

@Component({
  selector: 'app-sms-code',
  templateUrl: './sms-code.page.html',
  styleUrls: ['./sms-code.page.scss'],
})
export class SmsCodePage implements OnInit {

  phoneNumber= ''
  verificationId="1245";
  code = Array();
  started=false;
  verificationInProgress=false;
  @ViewChild('codesInpunt0') codesInpunt0;
  @ViewChild('codesInpunt1') codesInpunt1;
  @ViewChild('codesInpunt2') codesInpunt2;
  @ViewChild('codesInpunt3') codesInpunt3;

  constructor(
    public router: Router,
    public navData:NavDataServiceService,
    public platform : Platform,
    public toastCtrl: ToastController) {
      this.started=false;
  }

  ngOnInit() {

      let data = this.navData.getDataWithoutId()
      this.phoneNumber = data['phoneNumber'];
      this.verificationId = data['smscode'];
      console.log("route special "+this.phoneNumber+" verif code "+this.verificationId)

  }

  ionViewDidEnter() {
    this.codesInpunt0.setFocus();
  } 

  changeFocus(inputToFocus) {
    switch (inputToFocus) {
      case 1:
        this.codesInpunt1.setFocus();
        break;

      case 2:
        this.codesInpunt2.setFocus();
        break;

      case 3:
        this.codesInpunt3.setFocus();
        break;
       case 4:
        let enteredCode = this.code[0]+this.code[1] + this.code[2] + this.code[3]   ;
        this.resetCode()
        if (this.verificationInProgress==false){
          this.verificationInProgress=true;
          this.activate(enteredCode)
        }
       break;

    }

  }

  activate(enteredCode) {
    if (enteredCode){
      console.log("Compare code sms "+enteredCode+" avec "+this.verificationId)
      if (enteredCode.length == 4) {
        console.log(enteredCode);
        console.log("veificationCode is" + this.verificationId);
        if (enteredCode==this.verificationId){
          //Good job
        }
        else{

          this.presentToastError()
        }
      }
      else{
        this.presentToastError()
      }
    }
    else{
      this.presentToastAgain()
    }
  }

  resetCode(){
      this.code[0]="";
      this.code[1]="";
      this.code[2]="";
      this.code[3]="";

  }

  async presentToastAgain(){
    let toast = await this.toastCtrl.create({
      message: "Please enter code again",
      duration: 2000,
      position: 'bottom'
    });

    toast.present();
    this.verificationInProgress=false;
    this.resetCode()
    this.codesInpunt0.setFocus();
  }

  async presentToastError() {
    this.verificationInProgress=false;
    this.codesInpunt0.setFocus();
    let toast = await this.toastCtrl.create({
      message: "Code invalid",
      duration: 2000,
      position: 'bottom'
    });
   toast.present();
  }

}

Christophe Surbier