Managing your app without internet (React Native guide to USSD)

Ishara Abeykoon
ADL BOSS Skunks
Published in
7 min readJun 21, 2020

Handling native functions can be a big deal when it comes to developing mobile applications using a framework like react-native or flutter especially when you won’t find a library for that native function.

What is USSD and How It’s Handled?

USSD the short for “Unstructured Supplementary Service Data” is a protocol that is used to directly communicate with telecom providers, simply like a messaging service.

USSD Image (https://www.globitel.com/ussd-gateway/)

Usually USSD Handling is taken care by native telephony app, so IOS and Android has its own different ways to take care of USSD dialing.While on Android it’s possible to both dial and read, at the time of writing this article there isn’t a direct way to read response messages from IOS due to restrictions imposed by IOS

Here my main focus would be the USSD handling related to android

Here USSD handling in android can be done in two ways,

  1. Use Android builtin dialler to dial USSD code and use dialler to handle USSD responses. (In this way reading USSD responses may be limited)
  2. Use Android Telephony APIs USSD methods to dial USSD and get responses. (Available from Android Oreo API 26+)

In the above two methods using Android dialler to dial USSD may make your options limited as there won’t be a exact way to read up responses by your app from the android dialler.(I’ll explain a workaround in here for this)

Things are going to be long from here, if you want to directly go to the implementation using react-native-ussd library skip Using dialler section and move there.

Using Android Dialler (Using React Native)

USSD request for mobile balance using inbuilt dialler demo
USSD request for mobile balance using inbuilt dialler

First you need to make an native module for this, go through react native official docs on how to make a native module.which explains clearly on how to implement toast functionality using native modules.

Android dialler can be used to dial ussd by passing the ussd code with an intent to the android dialler app which in turn will run ussd code as it will run from native dialler app, and the app will have limited control over the responses receive from the ussd dial in fact app won’t have a way to read USSD replies.

Most usual way to handle this would be to use android’s accessibility service which will read window content after the ussd dial to read up content on the popup window.For this Accessibility service has to be defined in the Android manifest, and read up when a window popup happens.In addition to this app need to give access to accessibility access with the android settings manually (Well not a good way to handle a functionality :( )

We’ll go through this implementation

Android need Call permissions so define it in the manifest

<manifest>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<application>

Addition to permissions the accessibility service must be defined in the manifest within the <application></application> tags

<service   android:name=".USSDService"android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" /></intent-filter>
<meta-data android:name="android.accessibilityservice"
android:resource="@xml/ussd_service" />
</service>

And add a file with name ussd_service.xml to ‘res/xml’ folder with the following content

<?xml version=”1.0" encoding=”utf-8"?>
<accessibility-service xmlns:android=”http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes=”typeWindowStateChanged|typeWindowContentChanged”
android:accessibilityFeedbackType=”feedbackGeneric”
android:accessibilityFlags=”flagDefault”
android:canRetrieveWindowContent=”true”
android:description=”@string/accessibility_service_description”
android:notificationTimeout=”0"
android:packageNames=”com.android.phone” />

Make sure to add a string to ‘res/values/strings.xml’

<string name=”accessibility_service_description”>Accessibility Service Description</string>

Then comes to actual coding

Go to the ‘src/main’ inside your java package add the following class USSDService.java with the following content, make sure to change package names as for your own.(Here I used it as com.ussddial)

package com.ussddial;import android.text.TextUtils;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import javax.annotation.Nullable;
import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
public class USSDService extends AccessibilityService {
public static String TAG = USSDService.class.getSimpleName();
private static ReactApplicationContext reactContext;
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
Log.d(TAG, "onAccessibilityEvent");
AccessibilityNodeInfo source = event.getSource();
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && !String.valueOf(event.getClassName()).contains("AlertDialog")) {
return;
}
if(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && (source == null || !source.getClassName().equals("android.widget.TextView"))) {
return;
}
if(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && TextUtils.isEmpty(source.getText())) {
return;
}
List<CharSequence> eventText;if(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {eventText = event.getText();
} else {
eventText = Collections.singletonList(source.getText());

}
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this);
Intent customEvent= new Intent("my-custom-event");
customEvent.putExtra("my-extra-data", eventText.toString());
localBroadcastManager.sendBroadcast(customEvent);
String text = processUSSDText(eventText);if( TextUtils.isEmpty(text) ) return;// Close dialog
performGlobalAction(GLOBAL_ACTION_BACK); // This works on 4.1+ only
Log.d(TAG, text);
// Handle USSD response here
}private String processUSSDText(List<CharSequence> eventText) {
for (CharSequence s : eventText) {
String text = String.valueOf(s);
// Return text if text is the expected ussd response
if( true ) {
return text;
}
}
return null;
}
@Override
public void onInterrupt() {
}
@Override
protected void onServiceConnected() {
super.onServiceConnected();
Log.d(TAG, "onServiceConnected");
AccessibilityServiceInfo info = new AccessibilityServiceInfo();
info.flags = AccessibilityServiceInfo.DEFAULT;
info.packageNames = new String[]{"com.android.phone"};
info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
setServiceInfo(info);
}
}

Service sends ussd popup text via localbroadcast manager in this case we need to add a listener in our Module which is exported to react-native.

Here we implement a broadcast receiver which on receive will emit the event for react native.

package com.ussddial;import android.widget.Toast;
import android.net.Uri;
import android.content.Intent;
import android.content.IntentFilter;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import javax.annotation.Nullable;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import android.telephony.TelephonyManager;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.ussddial.USSDService;
public class USSDDialModule extends ReactContextBaseJavaModule {
private static ReactApplicationContext reactContext;
private LocalBroadcastReceiver mLocalBroadcastReceiver;
private static final String DURATION_SHORT_KEY = "SHORT";
private static final String DURATION_LONG_KEY = "LONG";
USSDDialModule(ReactApplicationContext context) {
super(context);
reactContext = context;
this.mLocalBroadcastReceiver = new LocalBroadcastReceiver();
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(reactContext);
localBroadcastManager.registerReceiver(mLocalBroadcastReceiver, new IntentFilter("my-custom-event"));
}
@Override
public String getName() {
return "USSDDial";
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
return constants;
}
@ReactMethod
public void dial(String code) {
Intent callIntent = new Intent(Intent.ACTION_CALL, ussdToCallableUri(code));
callIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
reactContext.startActivity(callIntent);
}
private void sendEvent(ReactContext reactContext,
String eventName,
@Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
public class LocalBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String someData = intent.getStringExtra("my-extra-data");
WritableMap params = Arguments.createMap();
params.putString("eventProperty", someData);
sendEvent(reactContext, "EventReminder", params);
}
}
private Uri ussdToCallableUri(String ussd) {
String uriString = "";
if(!ussd.startsWith("tel:")){
uriString += "tel:";
}
for(char c : ussd.toCharArray()) {
if(c == '#'){
uriString += Uri.encode("#");
}else{
uriString += c;
}
}
return Uri.parse(uriString);
}
}

Finally add an event emitter in your react native side to get the msgs emitted from the event emitter.

import * as React from 'react';
import { Platform, StyleSheet, Text, View, PermissionsAndroid, TouchableOpacity,NativeModules, NativeEventEmitter } from 'react-native';
import { NativeModules } from 'react-native';
const instructions = Platform.select({
ios: `Press Cmd+R to reload,\nCmd+D or shake for dev menu`,
android: `Double tap R on your keyboard to reload,\nShake or press menu button for dev menu`,
});
const {USSDDial} = NativeModules;
export default class App extends React.Component {
state = {
userBalance:0,
expiryDate:''
};
async checkBalance(){
let granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.CALL_PHONE,
{
'title': 'I need to make some calls',
'message': 'Give me permission to make calls '
}
)
const granted = await PermissionsAndroid.check( PermissionsAndroid.PERMISSIONS.CALL_PHONE );
if (granted) {
console.log( "CAN Make Calls" );
USSDDial.dial('*#456#');
console.log(this.state.userBalance);
}
else {
console.log( "CALL MAKING Permission Denied" );
}
}componentDidMount(){
const eventEmitter = new NativeEventEmitter(NativeModules.USSDDial);
this.eventListener = eventEmitter.addListener('EventReminder', (event) => {
console.log(event.eventProperty) // "someValue"
let balance = event.eventProperty.split("is")[1].split(".Valid")[0];
let date = event.eventProperty.split("until")[1].split(".")[0];
this.setState({
userBalance:balance,
expiryDate:date
})
console.log(balance);
});
}
componentWillUnmount(){
this.eventListener.remove();
}
render(){
return (
<View style={styles.container}>
<TouchableOpacity onPress={() => this.checkBalance()}>
<Text>Check Balance</Text>
</TouchableOpacity>
<Text>Your Balance is: {this.state.userBalance}</Text>
<Text>Expiry Date is: {this.state.expiryDate}</Text>
</View>
);
}
}

Using the Library react-native-ussd

USSD request for mobile balance using react-native-ussd library demo
USSD request for mobile balance using react-native-ussd library

Install the library using

npm install react-native-ussd --save

Basic Library use starts with adding user permissions to android manifest, which naturally need permission to make calls in order to continue

<manifest>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<application>

Then the Ussd module and ussdEventEmitter should be imported and event listener should be added either as a hook(in functional components) or in component mount in class based components

//Functional Components
import * as React from 'react';
import Ussd, {ussdEventEmitter} from 'react-native-ussd';

export default function App() {
useEffect(() => {
// Update the document title using the browser API
const eventListener = ussdEventEmitter.addListener('ussdEvent', (event) => {
console.log(event.ussdReply);
});
});
}

Same implementation can be used in class based components

//Class Based Components
import * as React from 'react';
import Ussd, {ussdEventEmitter} from 'react-native-ussd';

export default class App extends React.Component {
componentDidMount(){
this.eventListener = ussdEventEmitter.addListener('ussdEvent', (event) => {
console.log(balance);
});
}
}

Next the dialing can be done by dial method of Ussd module,Make sure to request for user permission at the runtime, better if this is handled at app startup

//Functional Components
import * as React from 'react';
import { PermissionsAndroid } from 'react-native';
import Ussd, {ussdEventEmitter} from 'react-native-ussd';

export default function App() {
const dial = async () =>{
let granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.CALL_PHONE,
{
'title': 'I need to make some calls',
'message': 'Give me permission to make calls '
}
)

if (granted) {
console.log( "CAN Make Calls" );
Ussd.dial('*#456#');//add your dilaing code instead of *#456#
}
else {
console.log( "CALL MAKING Permission Denied" );
}
}
}

In class based component

//Class Based Components
import * as React from 'react';
import { PermissionsAndroid } from 'react-native';
import Ussd, {ussdEventEmitter} from 'react-native-ussd';

export default class App extends React.Component {
async dial(){
let granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.CALL_PHONE,
{
'title': 'I need to make some calls',
'message': 'Give me permission to make calls '
}
)

if (granted) {
console.log( "CAN Make Calls" );
Ussd.dial('*#456#');

console.log(this.state.userBalance);
}
else {
console.log( "CALL MAKING Permission Denied" );
}
}
}

And that’s done it should get USSD responses printed out to the console for the dials made using the Ussd.dial() function.

References

--

--