Validating user identity is incredibly important in mobile applications. The most common approaches to this are social login or, as seen with apps such as WhatsApp, Telegram, Line, WeChat, and many more, the mobile phone number. Using a mobile phone number as a unique identifier on a mobile application, verified via an SMS PIN code, makes logical sense. Unfortunately, there are flaws with SMS-based verification that result in poor UX, such as long waits, failed delivery, and the risk of SIM Swap attacks.
The solution? Authentication based on the SIM card can confirm the ownership of a mobile phone number by verifying the possession of an active SIM with the same number. It also indicates when the SIM card associated with a phone number last changed as a signal for SIM swap detection.
Along with adding cryptographically secure phone verification to an application and protecting the user from SIM swap attacks, SIM card-based authentication improves the user experience. No more one-time code exchange, custom logic to handle mistyped PINS, or retries due to SMS delivery failure. The user never leaves the app, and the entire verification happens seamlessly.
Fewer dependencies, better security, and a better experience.
This tutorial will cover how to add SIM card-based phone authentication to an Android application using IDlayr SubscriberCheck.
You can find the completed Android sample app in the sim-card-auth-android repo on GitHub. The repo includes a README with documentation for running and exploring the code.
Let's run through building this application step by step.
Before you Begin
To complete this tutorial, you'll need an up-to-date version of Android Studio installed, some basic knowledge of Kotlin programming, and Node.js installed. You'll also need a physical Android device with an active SIM card because SubscriberCheck verifies a phone number by making a web request over a mobile data session.
Get Setup with IDlayr
A IDlayr Account is needed to make the SubscriberCheck API requests, so make sure you've created one.You're also going to need some Project credentials from IDlayr to make API calls. So sign up for a IDlayr account, which comes with some free credit. We've built a CLI for you to manage your IDlayr account, projects, and credentials within your Terminal. To install the IDlayr CLI run the following command: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 AuthDemo --project-dir .
AuthDemo
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
Creating a New Android Project
First, you have to create a new Android application using Android Studio. The app name is "SIMAuthentication". The package name is id.tru.authentication.demo
.
Click through the wizard, ensuring that "Empty Activity" is selected. Leave the "Activity Name" set to MainActivity
and the "Layout Name" set to activity_main
.
The tru-sdk-android
is available on Android devices with minimum Android SDK Version 21 (Lollipop); therefore, select minSdkVersion = 21 once creating the project.
Phone Number Authentication UI
The first screen will be our Verification screen, where the user has to enter their phone number. After adding their phone number, the user will click on a "Verify my phone number" button to initiate the verification workflow.
The user interface is straightforward: a ConstraintLayout
with one TextInputEditText
phone_number
inside a TextInputLayout
input_layout
for phone number input and a Button to trigger the verification, followed by a progress_bar
where the user is updated on the progress, as we will see later on. Update activity_main.xml
with the following:
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/container"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"android:paddingLeft="36dp"android:paddingTop="220dp"android:paddingRight="36dp"><TextViewandroid:id="@+id/sign_in_header"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="To experience IDlayr verification, please enter your phone number"android:textStyle="bold"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><com.google.android.material.textfield.TextInputLayoutandroid:id="@+id/input_layout"android:layout_width="match_parent"android:layout_height="wrap_content"app:errorEnabled="true"app:counterEnabled="true"app:counterMaxLength="13"android:layout_marginTop="20dp"style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/sign_in_header"><com.google.android.material.textfield.TextInputEditTextandroid:id="@+id/phone_number"android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="top|center"android:hint="Phone number"android:inputType="phone"android:imeOptions="actionDone"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/textView" /></com.google.android.material.textfield.TextInputLayout><Buttonandroid:id="@+id/verify"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_gravity="center"android:layout_marginTop="20dp"android:background="#6200EE"android:text="Verify my phone number"android:textAllCaps="false"android:textColor="@android:color/white"android:enabled="false"app:layout_constraintBottom_toBottomOf="@+id/input_layout"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="@+id/progress_bar"app:layout_constraintVertical_bias="0.698" /><com.google.android.material.progressindicator.LinearProgressIndicatorandroid:id="@+id/progress_bar"android:layout_width="match_parent"android:layout_height="wrap_content"android:indeterminate="true"android:visibility="invisible"android:layout_marginTop="100dp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/verify"/></androidx.constraintlayout.widget.ConstraintLayout>
Make sure to update the dependencies for ConstraintLayout
and Material Components with the latest available versions with app/build.gradle
:
implementation 'com.google.android.material:material:1.3.0'implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
The main screen will look like this:
View binding allows you to write code that interacts with views. To enable this, set the viewBinding
build option to true
in the app/build.gradle
file and :
android {buildFeatures {viewBinding true}}
Bind the verification workflow to the verify button within app/src/main/java/id/tru/authentication/demo/MainActivity.kt
:
class MainActivity : AppCompatActivity() {private var _binding: ActivityMainBinding? = nullprivate val binding get() = _binding!!override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)_binding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)binding.verify.setOnClickListener {initVerification()}}override fun onDestroy() {super.onDestroy()_binding = null}private fun initVerification() {Log.d(TAG, "Verify button clicked")}companion object {private const val TAG = "MainActivity"}}
Each time we start the verification process, we invalidate the UI, resetting all fields to cater for subsequent verification attempts:
/** Called when the user taps the verify button */private fun initVerification() {Log.d(TAG, "phoneNumber " + binding.phoneNumber.text)// close virtual keyboard when sign-in startsbinding.phoneNumber.onEditorAction(EditorInfo.IME_ACTION_DONE)invalidateVerificationUI(false)}private fun invalidateVerificationUI(isEnabled: Boolean) {if (isEnabled) {binding.progressBar.hide()binding.phoneNumber.setText("")} else {binding.progressBar.show()}binding.verify.isEnabled = isEnabledbinding.phoneNumber.isEnabled = isEnabled}
Now, let's proceed to initiate a SubscriberCheck workflow, as described in the next section.
SubscriberCheck Workflow
The sequence diagram below shows the complete SubscriberCheck Workflow. This tutorial will focus on the Device
and Device -> Server
logic.
Create a app/tru-id.properties
file and set the value of EXAMPLE_SERVER_BASE_URL
to be the public URL of your development server:
EXAMPLE_SERVER_BASE_URL="https://{subdomain}.{data_resdidency}.ngrok.io"
To perform network operations in your application, such as making requests to the verification server, your AndroidManifest.xml
must include android.permission.INTERNET
and android.permission.ACCESS_NETWORK_STATE
permissions. Also, some mobile network operator check requests are over HTTP (and not HTTPS), so the application must also allow cleartext network traffic. Note that no data is sent over the HTTP request. The MNO uses the request to verify that the device is connected to their mobile data network.
Your AndroidManifest.xml
will look as follows:
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"package="id.tru.authentication.demo"><uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><applicationandroid:usesCleartextTraffic="true"android:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.MaterialComponents.Light.NoActionBar"><activity android:name=".MainActivity"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application></manifest>
The Internet and ACCESS_NETWORK_STATE
permissions are normal permissions, which means they're granted at install time, and you don't need to request them at runtime.
To communicate with our local server, the app needs to read in the app/tru-id.properties
file containing the server URL and set a build configuration field. We want to include Retrofit, relevant JSON converters, and OkHttp. Finally, include kotlinx-coroutine:
Update app/build.gradle
with the following additions:
def props = new Properties()file("tru-id.properties").withInputStream { props.load(it) }android {defaultConfig {buildConfigField("String", "SERVER_BASE_URL", props.getProperty("EXAMPLE_SERVER_BASE_URL"))}dependencies {implementation 'com.squareup.retrofit2:retrofit:2.9.0'implementation 'com.squareup.retrofit2:converter-gson:2.9.0'implementation 'com.squareup.okhttp3:okhttp:4.9.0'implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'}}
1. Create a SubscriberCheck
To start the phone number verification flow in an Android app, the user enters their phone number and either presses the done keyboard key or touch the "Verify my phone number" button. The application then sends the phone number to your verification server that will create a SubscriberCheck resource on the IDlayr platform.
So, to create a SubscriberCheck, a phone_number
is required, and once created, the SubscriberCheck will contain a check_url
and a check_id
.
Let's create these required models for our network operations in a new file data/Model.kt
:
data class SubscriberCheckPost(@SerializedName("phone_number")val phone_number: String)data class SubscriberCheck(@SerializedName("check_url")val check_url: String,@SerializedName("check_id")val check_id: String)
Next, create an ApiService
interface in api/ApiService.kt
that makes use of the models, making a POST
request to the /subscriber-check
endpoint on the development server:
interface ApiService {@Headers("Content-Type: application/json")@POST("/subscriber-check")suspend fun createSubscriberCheck(@Body user: SubscriberCheckPost): Response<SubscriberCheck>}
Create a new file, api/RetrofitBuilder.kt
, and within it, create a Retrofit instance using Retrofit.Builder
, passing the ApiService
interface to generate an implementation.
We are also adding the HttpLoggingInterceptor
to check what is happening at every step.
object RetrofitBuilder {val apiClient: ApiService by lazy {val httpLoggingInterceptor = HttpLoggingInterceptor()httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);val okHttpClient = OkHttpClient().newBuilder()//httpLogging interceptor for logging network requests.addInterceptor(httpLoggingInterceptor).build()Retrofit.Builder().baseUrl(BuildConfig.SERVER_BASE_URL).client(okHttpClient).addConverterFactory(GsonConverterFactory.create()).build().create(ApiService::class.java)}}
Note that the BuildConfig.SERVER_BASE_URL
will be set with the value of EXAMPLE_SERVER_BASE_URL
you defined in tru-id.properties
.
Now we are ready to inform the user the verification process has started and create the SubscriberCheck using the provided phone number within MainActivity.kt
:
/** Called when the user taps the verify button */private fun initVerification() {Log.d(TAG, "phoneNumber " + binding.phoneNumber.text)// close the virtual keyboard when sign-in startsbinding.phoneNumber.onEditorAction(EditorInfo.IME_ACTION_DONE)invalidateVerificationUI(false)createSubscriberCheck()}private fun createSubscriberCheck() {CoroutineScope(Dispatchers.IO).launch {val subscriberCheck = RetrofitBuilder.apiClient.createSubscriberCheck(SubscriberCheckPost(binding.phoneNumber.text.toString()))}}
2. Use the IDlayr SDK to Request the SubscriberCheck URL
At this point, you will add the tru-sdk-android
SDK to your project to enable the SubscriberCheck URL to be requested over a mobile data connection.
Edit the build.gradle
at your project's root and add the following code snippet to the allprojects/repositories
section:
allprojects {repositories {google()jcenter()maven {url "https://gitlab.com/api/v4/projects/22035475/packages/maven"}maven { url 'https://jitpack.io' }}}
Add the SDK dependency to app/build.gradle
and sync the project.
implementation 'id.tru.sdk:tru-sdk-android:0.0.+'
Initialize the TruSDK
within onCreate
in MainActivity.kt
:
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)TruSDK.initializeSdk(applicationContext)_binding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)binding.verify.setOnClickListener {initVerification()}}
Create api/RedirectManager.kt
to encapsulate obtaining a TruSDK
instance and performing the network request with SubscriberCheck URL over the mobile data connection.
class RedirectManager {private val truSdk = TruSDK.getInstance()fun openCheckUrl(checkUrl: String) {truSdk.openCheckUrl(checkUrl)}}
Update the createSubscriberCheck
function in MainActivity.kt
to use the RedirectManager
to request the SubscriberCheck URL from the previous step. This update enables the mobile network operator and the IDlayr platform to verify the phone number is associated with the mobile data session.
private val redirectManager by lazy { RedirectManager() }private fun createSubscriberCheck() {CoroutineScope(Dispatchers.IO).launch {val response = RetrofitBuilder.apiClient.createSubscriberCheck(SubscriberCheckPost(binding.phoneNumber.text.toString()))if (response.isSuccessful && response.body() != null) {val subscriberCheck = response.body() as SubscriberCheckopenCheckURL(subscriberCheck)}else {updateUIonError("Error Occurred: ${response.message()}")}}}private fun openCheckURL(check: SubscriberCheck) {CoroutineScope(Dispatchers.IO).launch {redirectManager.openCheckUrl(check.check_url)}}private suspend fun updateUIonError(additionalInfo: String) {Log.e(TAG, additionalInfo)withContext(Dispatchers.Main) {invalidateVerificationUI(true)}}
3. Get the SubscriberCheck Results
Once the SubscriberCheck URL has been requested, the IDlayr platform now knows the check request has been performed. Your mobile application should query the result via your server application. This request will, in turn, trigger the IDlayr platform to fetch the SubscriberCheck match result from the MNO (Mobile Network Operator).
Add the SubscriberCheckResult model to Model.kt
:
data class SubscriberCheckResult(@SerializedName("match")val match: Boolean,@SerializedName("check_id")val check_id: String,@SerializedName("no_sim_change")val no_sim_change: Boolean)
Add a getSubscriberCheckResult
function to the ApiService
interface:
interface ApiService {@Headers("Content-Type: application/json")@POST("/subscriber-check")suspend fun createSubscriberCheck(@Body user: SubscriberCheckPost): Response<SubscriberCheck>@GET("/subscriber-check/{check_id}")suspend fun getSubscriberCheckResult(@Path("check_id") checkId: String): Response<SubscriberCheckResult>}
Now it's time to find out if the phone number was verified successfully. Create a new getSubscriberCheckResult
function in MainActivity.kt
and call it after the redirectManager.openCheckUrl
. The call to getSubscriberCheckResult
must be done within the coroutine to ensure the check URL request has been completed.
private fun openCheckURL(check: SubscriberCheck) {CoroutineScope(Dispatchers.IO).launch {redirectManager.openCheckUrl(check.check_url)getSubscriberCheckResult(check)}}private fun getSubscriberCheckResult(check: SubscriberCheck) {CoroutineScope(Dispatchers.IO).launch {val response = RetrofitBuilder.apiClient.getSubscriberCheckResult(check.check_id)if(response.isSuccessful && response.body() != null) {val subscriberCheckResult = response.body() as SubscriberCheckResultupdateUI(subscriberCheckResult)}}}private suspend fun updateUI(subscriberCheckResult: SubscriberCheckResult) {Log.d(TAG, "TODO: update the UI")}
There you go; based on the subscriberCheckResult
, you may notify the user that the authentication has been completed.
private suspend fun updateUI(subscriberCheckResult: SubscriberCheckResult) {withContext(Dispatchers.Main) {if (subscriberCheckResult.match && subscriberCheckResult.no_sim_change) {Snackbar.make(binding.container, "Phone verification complete", Snackbar.LENGTH_LONG).show()} else {Snackbar.make(binding.container, "Phone verification failed", Snackbar.LENGTH_LONG).show()}invalidateVerificationUI(true)}}
The value of no_sim_change
indicates if the SIM card has not been changed in the last seven days. For example, the phone number has been verified in the case of match = true
but no_sim_change = false
. Still, the fact the SIM card has changed recently means that it's good practice to perform additional user checks before allowing them to register or log in.
In an application that contains more than a login or signup activity, you would now show either a screen to capture more user information or the successfully registered/logged-in screen.
Try Out SIM Card-Based Authentication
Now that your code is complete, you can run the application on an actual device. Bear in mind that SIM card-based authentication is not possible against an emulator.
Enter the phone number for the mobile device in the UI in the format +{country_code}{number} e.g. +447900123456
.
Touch the "Verify my phone number" button.
Congratulations! You've finished the SIM Card Based Mobile Authentication for Android Tutorial.
Where next?
You can view a completed version of this sample app in the sim-card-auth-android repo on GitHub.
Optional step: Client side phone number pre-validation
The minimum phone number pre-validation that can be achieved on the client side is to validate the phone number format with libphonenumber
.
Update app/build.gradle
to include this library:
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.10'
And create a util/PhoneNumberUtil.kt
utility that validates the phone number format +{country_code}{number} e.g. +447900123456
fun isPhoneNumberFormatValid(phoneNumber: String): Boolean {val phoneNumberUtil = PhoneNumberUtil.getInstance()return try {phoneNumberUtil.isValidNumber(phoneNumberUtil.parse(phoneNumber, Phonenumber.PhoneNumber.CountryCodeSource.UNSPECIFIED.name))} catch (e: NumberParseException) {false}}
Now invoke the phone number pre-validation on createSubscriberCheck
:
private fun createSubscriberCheck() {if (!isPhoneNumberFormatValid(binding.phoneNumber.text.toString())) {invalidateVerificationUI(true)Snackbar.make(binding.container, "Phone number invalid", Snackbar.LENGTH_LONG).show()return}
Optional step: check if mobile data is enabled at runtime
The SubscriberCheck validation requires the device to have mobile data enabled. We can programmatically check if this is the case and launch the Internet Connectivity settings dialog if required.
Firstly, add a utility method isMobileDataEnabled
to your MainActivity.kt
to check if the mobile data is enabled:
@RequiresApi(Build.VERSION_CODES.O)private fun isMobileDataEnabled(): Boolean {val tm = getSystemService(TELEPHONY_SERVICE) as TelephonyManagerreturn tm.isDataEnabled}
The Activity Result APIs provide components for registering for a result, launching it, and handling it once the system dispatches it.
To get started, add the dependencies:
implementation 'androidx.activity:activity-ktx:1.2.0'implementation 'androidx.fragment:fragment-ktx:1.3.0'
The next step is to create a custom result contract data/ConnectivitySettingsContract.kt
, this class would extend ActivityResultContract <I,O> which requires defining input and output classes.
class ConnectivitySettingsContract : ActivityResultContract<Int, Uri?>() {@RequiresApi(Build.VERSION_CODES.Q)override fun createIntent(context: Context, input: Int) = Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY)override fun parseResult(resultCode: Int, result: Intent?): Uri? {println("Result code $resultCode")if (resultCode != Activity.RESULT_OK) {return null}return result?.data}}
Next, we register a callback for an activity result, we do that by defining registerForActivityResult()
which takes the ConnectivitySettingsContract
and ActivityResultCallback
and returns an ActivityResultLauncher
which is used to launch the other activity:
private val startSettingsForResult = registerForActivityResult(ConnectivitySettingsContract()) {Log.i(TAG, "Internet Connectivity Setting dismissed")initVerification()}
If the mobile data is disabled, we launch the previously created ActivityResultLauncher
as follows:
private fun initVerification() {Log.d(TAG, "phoneNumber " + binding.phoneNumber.text)// close virtual keyboard when sign-in startsbinding.phoneNumber.onEditorAction(EditorInfo.IME_ACTION_DONE)if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {if (!isMobileDataEnabled()) {startSettingsForResult.launch(CONNECTIVITY_SETTINGS_ACTION)return}}startVerification()}companion object {private const val TAG = "MainActivity"const val CONNECTIVITY_SETTINGS_ACTION = 1}
Programmatically reading the device Phone Number
If you're trying to create an even more magical phone number verification experience, you can make the phone number input part of the app's responsibility.
One option is to use Play Services auth-api-phone library hint picker that prompts the user to choose from the phone numbers stored on the device and thereby avoid having to type a phone number manually.
Troubleshooting
Mobile Data is Required
Don't forget the SubscriberCheck validation requires the device to enable mobile data.
Check the HTTP Logging Output
Because we have attached a HttpLoggingInterceptor, you can use adb logs to debug your SubscriberCheck:
I/okhttp.OkHttpClient: --> POST https://mylocalserver.example/subscriber-checkI/okhttp.OkHttpClient: {"phone_number":"+447XXXXXXXXX"}I/okhttp.OkHttpClient: --> END POST (32-byte body)I/okhttp.OkHttpClient: <-- 200 https://mylocalserver.example/check (1479ms)I/okhttp.OkHttpClient: {"check_id":"NEW_CHECK_ID","check_url":"https://{data_residency}.api.idlayr.com/subscriber_check/v0.1/checks/NEW_CHECK_ID/redirect"}I/okhttp.OkHttpClient: <-- END HTTP (157-byte body)D/RedirectManager: Triggering open check url https://{data_residency}.api.idlayr.com/subscriber_check/v0.1/checks/NEW_CHECK_ID/redirectI/SDK::checkUrl: Triggering check urlI/System.out: Response to https://{data_residency}.api.idlayr.com/subscriber_check/v0.1/checks/NEW_CHECK_ID/redirectD/LoginActivity: redirect done [7961ms]I/okhttp.OkHttpClient: --> GET https://mylocalserver.example/subscriber-check/NEW_CHECK_IDI/okhttp.OkHttpClient: --> END GETI/okhttp.OkHttpClient: <-- 200 https://mylocalserver.example/subscriber-check/NEW_CHECK_ID (749ms)I/okhttp.OkHttpClient: {"match":true,"check_id":"NEW_CHECK_ID","no_sim_change":true}I/okhttp.OkHttpClient: <-- END HTTP (64-byte body)
Check the Development Server Logging
The development server also logs information to the terminal, including request and response payloads. Check that output for any errors or unexpected values.
Get in touch
If you have questions, contact [email protected].