DEV Community

Sushan Dristi
Sushan Dristi

Posted on

Flutter USSD Integration: Guide for Developers

Bridging the Gap: Implementing USSD Functionality in Flutter

In the rapidly evolving landscape of mobile applications, developers are constantly seeking innovative ways to enhance user experience and broaden the reach of their platforms. While rich graphical interfaces and seamless online connectivity are paramount, there remains a significant segment of the global population that relies on the ubiquitous and robust nature of Unstructured Supplementary Service Data (USSD) for essential services. For Flutter developers, integrating USSD functionality presents a unique opportunity to tap into this market and offer powerful, accessible solutions.

This article delves into the intricacies of implementing USSD interactions within Flutter applications, exploring the technical considerations, common challenges, and practical approaches to creating a seamless bridge between your Flutter app and the USSD network.

Understanding the USSD Ecosystem

Before we dive into Flutter implementation, it's crucial to grasp the fundamental principles of USSD. USSD is a communication protocol used by GSM cellular telephones to communicate with mobile network operators' computers. Unlike SMS, which is store-and-forward, USSD provides a real-time, session-based connection. This means that when a user initiates a USSD code (e.g., *123#), a temporary connection is established, allowing for immediate interaction and data exchange.

The typical USSD flow involves:

  1. Initiation: The user dials a USSD code (e.g., *123#).
  2. Network Interaction: The mobile network operator's system receives the USSD request.
  3. Menu Display/Action: The network responds with a menu of options or performs an action.
  4. User Input: The user selects an option or provides further input.
  5. Response/Action: The network processes the input and provides a new menu or confirms an action.

This interactive, menu-driven nature makes USSD ideal for tasks such as checking account balances, recharging airtime, accessing mobile banking services, and participating in various promotional campaigns, especially in regions with limited internet penetration or where data costs are a barrier.

The Flutter Challenge: Direct USSD Access

Flutter, by design, operates within the application sandbox. For security and stability reasons, direct access to low-level telephony features like initiating USSD sessions is generally not permitted by the operating system (Android and iOS) for standard applications. This is a critical point: Flutter apps cannot directly dial USSD codes in the same way a native dialer app can.

This limitation means that any implementation of USSD functionality in Flutter will require a different approach. We cannot simply "trigger" a USSD code from our Dart code and expect the OS to handle it in the background while our app continues its operations.

Bridging the Gap: Native Integration and Platform Channels

The standard and most effective way to overcome this platform-specific limitation in Flutter is through Platform Channels. Platform Channels allow your Flutter code to communicate with native Android (Java/Kotlin) or iOS (Objective-C/Swift) code.

Here's the general strategy:

  1. Flutter (Dart) Side: Your Flutter UI will present options or input fields for the user to interact with USSD services. When a user action requires a USSD interaction, your Dart code will send a message to the native side via a Platform Channel. This message will likely contain the USSD code or the specific interaction details.

  2. Native (Android/iOS) Side: The native code will receive the message from Flutter. It will then use the platform's native APIs to initiate the USSD session.

    • On Android: You'll typically use the Intent.ACTION_DIAL or Intent.ACTION_CALL with a tel: URI that includes the USSD code. For example, Uri.parse("tel:*123#"). The system will then prompt the user to confirm the call or dial the USSD code.
    • On iOS: Similar to Android, you'll use the UIApplication.shared.open(_:) method with a tel: URL scheme.
  3. Native to Flutter Feedback (Optional but Recommended): For a more integrated user experience, the native side can also send messages back to Flutter. This might involve capturing the USSD response text, though this is significantly more complex and often not feasible due to OS restrictions on intercepting USSD responses. More practically, the native side can inform Flutter when a USSD session has been initiated or if there was an error.

Practical Implementation Steps

Let's outline the steps for implementing USSD functionality using Platform Channels.

1. Setting up the Platform Channel

In your Flutter project, you'll define a MethodChannel.

import 'package:flutter/services.dart';

class USSDService {
  static const MethodChannel _channel = MethodChannel('com.yourdomain.yourapp/ussd');

  static Future<void> dialUSSD(String ussdCode) async {
    try {
      await _channel.invokeMethod('dialUSSD', {'ussdCode': ussdCode});
    } on PlatformException catch (e) {
      print("Failed to dial USSD: ${e.message}");
      // Handle error, e.g., show a Snackbar to the user
    }
  }

  // You might add more methods for different USSD interactions
}
Enter fullscreen mode Exit fullscreen mode

2. Implementing the Native Side (Android Example - Kotlin)

Create a MethodCallHandler in your MainActivity.kt or a separate native class.

package com.yourdomain.yourapp

import android.content.Intent
import android.net.Uri
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val CHANNEL = "com.yourdomain.yourapp/ussd"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            when (call.method) {
                "dialUSSD" -> {
                    val ussdCode = call.argument<String>("ussdCode")
                    if (ussdCode != null) {
                        dialUSSDCode(ussdCode)
                        result.success(null) // Indicate success to Flutter
                    } else {
                        result.error("INVALID_ARGUMENT", "USSD code not provided", null)
                    }
                }
                else -> {
                    result.notImplemented()
                }
            }
        }
    }

    private fun dialUSSDCode(ussdCode: String) {
        // Ensure the USSD code starts with '*' and ends with '#' for proper dialing
        val formattedUssdCode = if (!ussdCode.startsWith("*")) "*$ussdCode" else ussdCode
        val finalUssdCode = if (!formattedUssdCode.endsWith("#")) "$formattedUssdCode#" else formattedUssdCode

        val intent = Intent(Intent.ACTION_DIAL)
        intent.data = Uri.parse("tel:$finalUssdCode")
        // If you want to automatically dial without user confirmation (less common and often restricted)
        // val intent = Intent(Intent.ACTION_CALL)
        // intent.data = Uri.parse("tel:$finalUssdCode")
        // You would need the CALL_PHONE permission for ACTION_CALL

        if (intent.resolveActivity(packageManager) != null) {
            startActivity(intent)
        } else {
            // Handle case where no app can handle the dial intent
            // This might happen on some emulators or specific device configurations
            println("No activity found to handle dial intent for $finalUssdCode")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Note on Permissions: For ACTION_DIAL, no special permissions are required beyond what is implicitly granted to apps to interact with the dialer. For ACTION_CALL, you would need to declare <uses-permission android:name="android.permission.CALL_PHONE" /> in your AndroidManifest.xml and request it at runtime. ACTION_DIAL is generally preferred as it shows the dialer with the number pre-filled, allowing the user to confirm.

3. Implementing the Native Side (iOS Example - Swift)

In your AppDelegate.swift (or a dedicated native file imported into your Flutter project), you'll implement the MethodCallHandler.

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    let controller: FlutterViewController = window.rootViewController as! FlutterViewController
    let ussdChannel = FlutterMethodChannel(name: "com.yourdomain.yourapp/ussd",
                                           binaryMessenger: controller.binaryMessenger)

    ussdChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // This method is invoked on the UI thread.
      guard call.method == "dialUSSD" else {
        result(FlutterMethodError(code: "UNIMPLEMENTED", message: "Unknown method", details: nil))
        return
      }

      guard let args = call.arguments as? [String: Any],
            let ussdCode = args["ussdCode"] as? String else {
        result(FlutterError(code: "INVALID_ARGUMENT", message: "USSD code not provided", details: nil))
        return
      }

      self.dialUSSDCode(ussdCode: ussdCode)
      result(nil) // Indicate success to Flutter
    })

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  private func dialUSSDCode(ussdCode: String) {
    // Ensure the USSD code starts with '*' and ends with '#' for proper dialing
    var formattedUssdCode = ussdCode
    if !formattedUssdCode.hasPrefix("*") {
        formattedUssdCode = "*\(formattedUssdCode)"
    }
    if !formattedUssdCode.hasSuffix("#") {
        formattedUssdCode = "\(formattedUssdCode)#"
    }

    if let url = URL(string: "tel://\(formattedUssdCode)") {
        if UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        } else {
            // Handle case where the URL cannot be opened
            print("Cannot open URL for USSD code: \(formattedUssdCode)")
        }
    } else {
        print("Invalid USSD URL generated: \(formattedUssdCode)")
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Triggering USSD from Flutter UI

Now, in your Flutter widget, you can call the USSDService.

import 'package:flutter/material.dart';
import 'package:your_app_name/ussd_service.dart'; // Assuming your service is here

class USSDScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('USSD Interaction')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: () async {
                // Example: Check balance
                await USSDService.dialUSSD("*123#");
              },
              child: Text('Check Balance'),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () async {
                // Example: Recharge airtime (hypothetical USSD code)
                await USSDService.dialUSSD("*555*YOUR_PIN#"); // Replace YOUR_PIN with actual input
              },
              child: Text('Recharge Airtime'),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Considerations and Best Practices

  • User Experience: While USSD is functional, it's often a less visually appealing experience than native app UIs. Clearly label what each USSD code does. Provide clear instructions on how to input data if your app is handling dynamic USSD codes (e.g., recharging with a PIN).
  • Error Handling: Implement robust error handling. What happens if the USSD code is invalid? What if the network is unavailable? Provide feedback to the user through SnackBars, dialogs, or dedicated text widgets.
  • Security: Be extremely cautious when handling sensitive information like PINs or account numbers. Never embed sensitive data directly in USSD codes that are hardcoded in your app. If you need to include dynamic data, ensure it's handled securely, perhaps by prompting the user for input within your Flutter app's UI before sending the USSD command to the native side.
  • USSD Response Parsing (The Hard Part): As mentioned, capturing and parsing the text response from a USSD session is notoriously difficult and often unreliable across different devices and network providers. Most platforms restrict apps from intercepting these USSD session messages. If your use case absolutely requires parsing USSD responses, you might need to explore more complex, potentially unreliable, techniques like OCR on screenshots (not recommended for production) or rely on alternative APIs provided by the mobile network operator (if available). For most practical purposes, consider your Flutter app as initiating an action, and the user will see the USSD menu directly on their phone.
  • Background Execution: USSD sessions are inherently foreground operations. Your Flutter app will be paused or become inactive while the USSD interaction is active. You cannot reliably perform USSD operations in the background.
  • Testing: Thoroughly test your USSD implementation on various devices and Android/iOS versions. USSD behavior can sometimes vary slightly.
  • Alternative: USSD Gateway APIs: For more sophisticated control and to avoid direct USSD dialing from the client, consider using a USSD gateway service. These services provide APIs that your Flutter app can call. The gateway then interacts with the mobile network operator on your behalf. This is often a more robust and scalable solution for business-critical applications, though it typically involves costs.

When to Use USSD in Flutter

Despite its limitations, integrating USSD into a Flutter app can be incredibly powerful in specific scenarios:

  • Emerging Markets: Reach users with feature phones or those in areas with limited internet infrastructure.
  • Offline Functionality: Provide essential services that don't require a constant internet connection (though the underlying network signal is still needed for USSD).
  • Cost-Effective Services: Offer services where data costs might be prohibitive for users who rely on USSD for basic transactions.
  • Legacy System Integration: Connect with existing systems that primarily use USSD for communication.

Conclusion

Implementing USSD functionality in Flutter, while not a direct API call, is achievable and can unlock significant opportunities for your applications. By leveraging the power of Platform Channels to interact with native device capabilities, you can create a bridge between your Flutter app's rich UI and the foundational USSD network.

Remember that the primary role of your Flutter app will be to initiate and guide the user through the USSD interaction, rather than to completely abstract it away. Focus on a smooth user experience, robust error handling, and understanding the inherent limitations of USSD. As mobile technology continues to evolve, maintaining the ability to connect through fundamental protocols like USSD ensures your applications remain inclusive and accessible to a wider audience.

Top comments (0)