Ionic is a framework that allows you to build cross-platform for native iOS, Android, or the web, using one codebase. It provides a set of mobile UI components for use across iOS and Android.
Ionic is limited in that it provides a UI toolkit and access to some core APIs, but does not allow you to extend or write custom native functionality, so the Ionic team created Capacitor. Capacitor provides a consistent, web-focused set of APIs that enable an app to stay as close to web standards as possible while accessing rich native device features on platforms that support them.
In this tutorial, you'll learn how to add passwordless authentication to your Ionic 5 Capacitor projects using the new tru.ID (IDlayr) Ionic / Capacitor plugin and the IDlayr PhoneCheck API.
Before you begin
To follow along with this tutorial, you'll need:
- A mobile phone with an active data SIM card
- Ionic Tooling installed
- Android Studio
- XCode >=12
- JDK >=8
Getting Started
To install Ionic tooling, run the following in the terminal:
npm install -g @ionic/cli native-run cordova-res
Clone the Github repository and check out the starter-files branch to follow this tutorial using the command below in your Terminal:
git clone -b starter-files https://github.com/tru-ID/ionic-plugin-example-app.git
If you're only interested in the finished code in main, then run:
git clone -b main https://github.com/tru-ID/ionic-plugin-example-app.git
npm install -g @tru_id/cli
tru login <YOUR_IDENTITY_PROVIDER> (this is one of google, github, or microsoft) using the Identity Provider you used when signing up. This command will open a new browser window and ask you to confirm your login. A successful login will show something similar to the below:Success. Tokens were written to /Users/user/.config/@tru_id/cli/config.json. You can now close the browser
config.json file contains some information you won't need to modify. This config file includes your Workspace Data Residency (EU, IN, US), your Workspace ID, and token information such as your scope.Create a new IDlayr project within the root directory with the following command:tru projects:create ionic-auth --project-dir .
ionic-auth directory and run the following command to clone the dev-server:git clone [email protected]:tru-ID/dev-server.git
dev-server directory, run the following command to create a.env copy of .env.example:cp .env.example .env
.env file, update the values of TRU_ID_CLIENT_ID andTRU_ID_CLIENT_SECRET with your client_id and client_secret in yourtru.json file.dev-server to the Internet for your mobile application to access your backend server. For this tutorial, we're using ngrok, which we've included in the dev-server functionality. So to start with, uncomment the following and populate them with your ngrok credentials:NGROK_ENABLED=true#NGROK_SUBDOMAIN=a-subdomain # Uncommenting this is optional. It is only available if you have a paid ngrok account.NGROK_AUTHTOKEN=<YOUR_NGROK_AUTHTOKEN> # This is found in the ngrok dashboard: https://dashboard.ngrok.com/get-started/your-authtokenNGROK_REGION=eu # This is where your ngrok URL will be hosted. If you're using IDlayr's `eu` data residency; leave it as is. Otherwise, you could specify `in` or `us`.
dev-server directory, run the following two commands:npm install # Installs all third-party dependencies in package.jsonnpm run dev # Starts the server
With the ngrok URL provided, copy this value and update the BASE_URL value found within src/pages/Home.tsx.
Finally, install dependencies in your terminal using the following:
npm install
Get the user's phone number
To get the user's phone number, you need to update the UI and the state in the file src/pages/Home.tsx.
Inside the Home function, add the definition of two states – phoneNumber and loading – as shown below:
const Home: React.FC = () => {const [phoneNumber, setPhoneNumber] = useState('')const [loading, setLoading] = useState(false)...
Now add the following UI components below the line: </IonHeader>:
<IonContent fullscreen><IonItem className="center margin-top"><IonInputvalue={phoneNumber}className="input"autocapitalize="off"type="tel"placeholder="Phone Number ex. +4433234454"onIonChange={(e) => setPhoneNumber(e.detail.value!)}clearInput></IonInput></IonItem>{loading ? (<div className="center margin-top"><IonSpinner /></div>) : (<IonButtonexpand="block"color="success"shape="round"onClick={submitHandler}>Submit</IonButton>)}</IonContent>
The file starts with the phoneNumber and loading states. The phoneNumber is bound to the IonInput element. This element is a special Input component Ionic provides, which sets the value of the Input to the value provided by the user.
The loading state indicates that something (an HTTP request) is happening in the background, and the application gives the user a visual cue (<IonSpinner/>).
The last thing needed is to bind the empty submitHandler function to an IonButton button.
Submit the user's phone number
Before submitting the user's phone number and creating the PhoneCheck, the application must check if the user's mobile operator supports the PhoneCheck API. For this, the app will use the tru.ID (IDlayr) Ionic / Capacitor plugin. Install this plugin in the terminal with the following command:
npm install @tru_id/tru-plugin-ionic-capacitor
To use this plugin in the project, add the import to the top of src/pages/Home.tsx
import { TruPluginIonicCapacitor } from '@tru_id/tru-plugin-ionic-capacitor'
Now, replace the line const submitHandler = async () => {}; with the following:
const submitHandler = async () => {const body = JSON.stringify({ phone_number: phoneNumber })console.log(body)try {setLoading(true)const reachabilityDetails = await TruPluginIonicCapacitor.isReachable()console.log('Reachability details are', reachabilityDetails.result)const info: {networkId: stringnetworkName: stringcountryCode: stringproducts?: { productId: string; productType: string }[]error?: {type: stringtitle: stringstatus: numberdetail: string}} = JSON.parse(reachabilityDetails.result)} catch (e: any) {setLoading(false)console.log(JSON.stringify(e))present({cssClass: 'alert-style',header: 'Something went wrong.',message: `${e.message}`,buttons: ['Cancel', { text: 'Got It!' }],onDidDismiss: (e) => console.log('Alert Hook dismissed'),})}}
This code block starts by setting the variable body to a JSON encoded variable containing the phoneNumber received from the IonInput. The loading state is then set to true to give a visual indicator to the user.
The isReachable function is then called functionality exposed by the plugin. This function returns the networkId, networkName, countryCode, and products. The products array is an optional array supported by the MNO. There is also an error object which contains any potential errors.
The next step is to check if there are any errors and whether the UI needs updating to inform the user. For this, add the details constants to the slices of state at the top of the Home function:
const Home: React.FC = () => {...const [details, setDetails] = useState("")...}
Now inside your return(), above the line </IonContent>, add the following new lines to reflect these new states:
{details && <div>isReachable? : {details === 'true' ? '✔' : 'No'}</div>}
The application must handle a new error within the submitHandler. If the reachability API returns the HTTP status of 400, the Mobile Network Operator (MNO) is not supported. The authentication request needs to stop, and an error displays to the user. In the submitHandler, above the line } catch (e: any) {, add the following:
if (info.error?.status === 400) {present({cssClass: 'alert-style',header: 'Something went wrong.',message: 'Mobile Operator not supported.',buttons: ['Cancel', { text: 'Got It!' }],onDidDismiss: (e) => console.log('Alert Hook dismissed'),})setDetails('MNO not supported')setLoading(false)return}
The next step is to loop through the array of products and see if the PhoneCheck API is supported. If it isn't supported, stop trying to create the PhoneCheck. Continue updating the submitHandler by adding the following code:
let isPhoneCheckSupported = falseif (info.error?.status !== 412) {isPhoneCheckSupported = falsefor (const { productType } of info.products!) {console.log('supported products are', productType)if (productType === 'PhoneCheck') {isPhoneCheckSupported = true}}} else {isPhoneCheckSupported = true}if (!isPhoneCheckSupported) {present({cssClass: 'alert-style',header: 'Something went wrong.',message: 'PhoneCheck is not supported on MNO.',buttons: ['Cancel', { text: 'Got It!' }],onDidDismiss: (e) => console.log('Alert Hook dismissed'),})setDetails('PhoneCheck is not supported on MNO')setLoading(false)return}setDetails(JSON.stringify(isPhoneCheckSupported))
Here, the application creates the variable isPhoneCheckSupported. If the error status is not a 412, the application loops through the products and checks whether the productType equals PhoneCheck; if it does, then isPhoneCheckSupported is set to true.
If the isPhoneCheckSupported variable gets set to false by the end of the loop. Then this value means the MNO does not support PhoneCheck, and an alert will be rendered to explain as much.
Creating the PhoneCheck
At this point, the app can detect whether the PhoneCheck is supported or not using the isReachable function. It's time to create the PhoneCheck.
Below the line setDetails(JSON.stringify(isPhoneCheckSupported)), update the submitHandler with the following:
const response = await fetch(`${BASE_URL}/phone-check`, {headers: {'Content-Type': 'application/json',},method: 'POST',body,})const resp: { check_id: string; check_url: string } = await response.json()console.log('Server response is: ', JSON.stringify(resp))const check_url = resp.check_urlconst check_id = resp.check_idconsole.log('check url is', check_url)console.log('check_id is', check_id)
The example above makes a POST request to the endpoint /phone-check. When the request receives a response, an object is created with these two keys, check_url and check_id.
The next step is to open the Check URL using the check function. The UI also needs updating with the result from opening the check URL to let the user know it occurred. For this, define checked at the top of the Home function. This gets defined along with the phoneNumber, loading, and details as shown below:
const Home: React.FC = () => {...const [checked, setChecked] = useState("")...}
Update the return to reflect this new piece of state:
In the return(), above the line </IonContent> add the following:
{checked && <div>Check URL opened ✔</div>}
Time to open the Check URL using the check function. In the submitHandler, above the line } catch (e: any) {, add the following:
const isChecked = await TruPluginIonicCapacitor.check({ url: check_url })console.log(isChecked)console.log('isChecked (check) Result', isChecked.result)setChecked(JSON.stringify(isChecked))
Get the check result
The last step is to get the check result. If the result is a match, then update the UI to inform the user that a match occurred. For this, add the match constant at the top of the Home function along with phoneNumber, loading, details, and checked:
const [match, setMatch] = useState('')
The return function now needs the ability to output whether the match was a success or not. Above </IonContent>, add the following:
{match && <div>We have a match? {match === 'true' ? '✔' : 'No'}</div>}
All that is left is to make a GET request to the endpoint /api/register on our server. The request needs checkId as the check_id query parameter from the previous API response. Then, if match is true, the UI is updated to reflect this.
To do this, update submitHandler to include the following, above the line } catch (e: any) {:
const phoneCheckResponse = await fetch(`${BASE_URL}/phone-check?check_id=${check_id}`,{headers: {'Content-Type': 'application/json',},},)const phoneCheckResult: { match: boolean } = await phoneCheckResponse.json()console.log('PhoneCheck match', phoneCheckResult.match)setMatch(JSON.stringify(phoneCheckResult.match))setLoading(false)if (phoneCheckResult.match) {present({cssClass: 'alert-style',header: 'Success!',message: 'PhoneCheck Verification successful.',buttons: ['Cancel', { text: 'Got It!' }],onDidDismiss: (e) => console.log('Alert Hook dismissed'),})} else if (!phoneCheckResult.match) {present({cssClass: 'alert-style',header: 'Something went wrong.',message: 'PhoneCheck verification unsuccessful.',buttons: ['Cancel', { text: 'Got It!' }],onDidDismiss: (e) => console.log('Alert Hook dismissed'),})}
Building the project for native
Now that we've built our application, we have to build it for Android and iOS platforms. To do this, we'll leverage Capacitor, which ships with Ionic 5. Run the following in the terminal:
ionic build$ ionic cap add android$ ionic cap add ios
Here we create an optimized production build, then add native Android and iOS code. We should now have android and ios folders in the root.
Building the project for native - Android
Some further setup is required in order to use the tru.ID (IDlayr) Ionic / Capacitor plugin on Android. In the terminal, run the following:
ionic cap sync$ ionic cap open android
This command will create an optimized production build, sync native code, and open the Android project in Android Studio.
For this project, we'll use Kotlin. To convert to Kotlin, right-click app/src/main/java/io/ionic/starter/MainActivity and press "Convert Java File to Kotlin File", ensuring that you convert project-wide (the entire module, not only the file).
This action will convert any existing Java files to Kotlin, and update your Gradle files to use Kotlin.
Next, update the following in android/build.gradle to:
allprojects {repositories {google()jcenter()maven {url "https://gitlab.com/api/v4/projects/22035475/packages/maven"}}}
Update the following in android/app/build.gradle to:
defaultConfig {configurations.all {resolutionStrategy { force 'androidx.core:core-ktx:1.6.0' }}}
dependencies {implementation "com.squareup.okhttp3:okhttp:4.9.0"implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"implementation 'commons-io:commons-io:2.4'}
In your MainActivity, add the imports:
import com.trupluginioniccapacitor.TruPluginIonicCapacitorPluginimport android.os.Bundle
Lastly, we need to register the plugin. Add the plugin to your app's MainActivity onCreate method:
public override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)registerPlugin(TruPluginIonicCapacitorPlugin::class.java)}
Building the project for native – iOS
Some further setup is required to use the tru.ID (IDlayr) Ionic / Capacitor plugin on iOS. In the terminal, run the following:
ionic cap sync$ ionic cap open ios
This command will create an optimized production build, sync native code, and open the iOS project in XCode.
In XCode, click on App and set the bundle identifier to id.tru.example, as shown below:

Running the app
To run the Android app, press the play button in Android Studio with your physical Android device connected. Alternatively, from the terminal, run:
ionic capacitor run android --livereload --external
To run the iOS app, press the play button in XCode with your physical device connected. Alternatively, in the terminal, run:
ionic capacitor run ios --livereload --external
The complete workflow for a successful PhoneCheck creation looks like this:

If you've made it this far, you've now successfully created an Ionic application with passwordless authentication built-in using our PhoneCheck service.