A complete Flutter plugin for handling USSD codes on Android and iOS with advanced features including multi-USSD sessions.
- Standard USSD code execution - Native functionality on Android and iOS
- Direct USSD - Execute USSD codes in background (Android 8+)
- Direct USSD with iOS fallback - Unified cross-platform functionality
- Support verification - Detect if device supports USSD
- System information - Complete diagnostics of permissions and status
- Cross-platform support - Full Android, iOS with documented limitations
- Multi-USSD Sessions - Keep USSD dialogs open for multiple interactions
- Accessibility Service - Automatic capture of USSD responses from system
- Automatic permission management - Smart request for necessary permissions
- Multi-SIM Support - Execute USSD on specific SIMs with
subscriptionId
Add this to your pubspec.yaml:
dependencies:
ussd_handler: ^1.0.0Add these permissions in android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />Add the accessibility service in android/app/src/main/AndroidManifest.xml inside <application>:
<service
android:name="com.example.ussd_handler.UssdAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/ussd_service" />
</service>For iOS, the plugin provides limited functionality due to system restrictions:
<!-- No specific permissions required for iOS -->
<!-- The plugin uses available public APIs -->Behavior on iOS:
- USSD codes open the native phone application
- Responses cannot be captured automatically
- System information is available
- Basic support for Dual SIM devices
- Optional fallback for
executeUssdDirectusingexecuteUssd
The plugin uses a completely static API:
import 'package:ussd_handler/ussd_handler.dart';
// ✅ Correct - Static methods
final response = await UssdHandler.executeUssd('*#06#');
// ❌ Incorrect - Don't instantiate the class
// final handler = UssdHandler(); // Error!Description: Execute USSD codes using native system functionality. On Android shows standard USSD dialog, on iOS opens phone app.
// Execute basic USSD code
final response = await UssdHandler.executeUssd('*#06#');
if (response.success) {
print('USSD Response: ${response.response}');
} else {
print('Error: ${response.errorMessage}');
}With Multi-SIM:
// Specify specific SIM (Android)
final response = await UssdHandler.executeUssd(
'*#06#',
subscriptionId: 1, // Use SIM in slot 1
);
// Automatic (will show native selector if multiple SIMs)
final response = await UssdHandler.executeUssd('*#06#');Behavior by platform:
- Android: Shows native USSD dialog, captures response automatically
- iOS: Opens phone application, doesn't capture response (system limitation)
Description: Execute USSD codes in background without showing system dialogs. Only works on Android 8+ with special permissions.
// Execute USSD in background
final response = await UssdHandler.executeUssdDirect('*123#');
if (response != null) {
print('Direct response: $response');
} else {
print('No response received or not supported');
}// Works on Android and iOS using fallback
final response = await UssdHandler.executeUssdDirect(
'*#06#',
iosFallbackToStandard: true, // ✨ New parameter
);
if (response != null) {
print('Response: $response');
// Android: Direct USSD response
// iOS: Descriptive message of submission
}final response = await UssdHandler.executeUssdDirect(
'*123#',
timeoutSeconds: 45, // Custom timeout
subscriptionId: 2, // Specific SIM (Android)
iosFallbackToStandard: true, // Fallback for iOS
);Behavior by platform:
| Platform | iosFallbackToStandard: false | iosFallbackToStandard: true |
|---|---|---|
| Android | Direct USSD (normal) | Direct USSD (ignores parameter) |
| iOS | Returns null |
Uses executeUssd internally |
Advantages of iOS fallback:
- ✅ Unified code for both platforms
- ✅ No need for
if (Platform.isIOS)conditionals - ✅ Backward compatibility (default
false) - ✅ Optional functionality as needed
Multi-USSD sessions allow keeping USSD dialogs open to navigate complex menus automatically. This functionality requires accessibility services on Android.
1. Verify Support
final systemInfo = await UssdHandler.getSystemInfo();
final supportsMultiSession = systemInfo?.multiSessionSupported ?? false;
if (!supportsMultiSession) {
print('Multi-session not supported on this device');
return;
}2. Enable Accessibility Service
// Check if enabled
final isEnabled = await UssdHandler.isAccessibilityServiceEnabled();
if (!isEnabled) {
// Open settings automatically
final opened = await UssdHandler.openAccessibilitySettings();
if (opened) {
print('Please enable "USSD Handler" service and return to the app');
}
}Execute a complete multi-session flow in a single method:
Future<void> executeBankingFlow() async {
try {
String result = '';
// 1. Start session with bank code
final startResult = await UssdHandler.startMultiSessionUssd('*123#');
if (startResult.success) {
result += 'Session started: ${startResult.message}\n';
// 2. Wait for initial response and navigate
await Future.delayed(Duration(seconds: 3));
// 3. Select option "1" (example: check balance)
final message1 = await UssdHandler.sendMessageInSession('1');
if (message1.success) {
result += 'Option 1 sent: ${message1.message}\n';
await Future.delayed(Duration(seconds: 2));
// 4. Confirm with another option if necessary
final message2 = await UssdHandler.sendMessageInSession('1');
if (message2.success) {
result += 'Confirmation sent: ${message2.message}\n';
}
}
// 5. Cancel session
await Future.delayed(Duration(seconds: 2));
final cancelResult = await UssdHandler.cancelMultiSession();
result += 'Session finished: ${cancelResult.message}';
}
print('Flow completed:\n$result');
} catch (e) {
print('Flow error: $e');
}
}For cases requiring custom logic between steps:
Future<void> customNavigation() async {
try {
// 1. Start multi-USSD session
final startResult = await UssdHandler.startMultiSessionUssd('*123#');
if (startResult.success) {
print('Session started: ${startResult.message}');
// 2. Check session status
final isActive = await UssdHandler.isMultiSessionActive();
print('Session active: $isActive');
if (isActive) {
// 3. Wait for initial response
await Future.delayed(Duration(seconds: 3));
// 4. Make decision based on response
String option = '1'; // Custom logic here
final msgResult = await UssdHandler.sendMessageInSession(option);
if (msgResult.success) {
print('Message sent: ${msgResult.message}');
// 5. Continue navigation based on response
await Future.delayed(Duration(seconds: 2));
// More messages if necessary
await UssdHandler.sendMessageInSession('2');
// 6. Finish when done
await UssdHandler.cancelMultiSession();
print('Session finished');
}
}
} else {
print('Error starting session: ${startResult.error}');
}
} catch (e) {
print('Error: $e');
// Ensure session is cancelled in case of error
await UssdHandler.cancelMultiSession();
}
}Control Methods:
startMultiSessionUssd(String ussdCode)- Start sessionsendMessageInSession(String message)- Send messagesisMultiSessionActive()- Check statuscancelMultiSession()- End session
import 'package:flutter/material.dart';
import 'package:ussd_handler/ussd_handler.dart';
class USSDExample extends StatefulWidget {
@override
_USSDExampleState createState() => _USSDExampleState();
}
class _USSDExampleState extends State<USSDExample> {
String result = '';
Future<void> executeMultiSessionUSSD() async {
try {
// Check accessibility service
final isServiceEnabled = await UssdHandler.isAccessibilityServiceEnabled();
if (!isServiceEnabled) {
await UssdHandler.openAccessibilitySettings();
return;
}
// Start session
final startResult = await UssdHandler.startMultiSessionUssd('*123#');
if (startResult.success) {
setState(() {
result = 'Session started successfully';
});
// Wait and send message
await Future.delayed(Duration(seconds: 2));
final msgResult = await UssdHandler.sendMessageInSession('1');
if (msgResult.success) {
setState(() {
result += '\\nMessage sent: ${msgResult.message}';
});
}
// Cancel after some time
await Future.delayed(Duration(seconds: 3));
await UssdHandler.cancelMultiSession();
setState(() {
result += '\\nSession cancelled';
});
}
} catch (e) {
setState(() {
result = 'Error: $e';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Multi-Session USSD')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
ElevatedButton(
onPressed: executeMultiSessionUSSD,
child: Text('Execute Multi-Session USSD'),
),
SizedBox(height: 20),
Text(result),
],
),
),
);
}
}Result of standard USSD code execution.
class UssdResponse {
final String? response; // USSD response
final bool success; // If successful
final String? errorMessage; // Error message if failed
// Conversion methods
factory UssdResponse.fromMap(Map<String, dynamic> map);
Map<String, dynamic> toMap();
}Detailed system information for diagnostics.
class SystemInfo {
final String platform; // 'android' | 'ios'
final int? androidVersion; // API level (Android only)
final String? iosVersion; // iOS version (iOS only)
final String deviceModel; // Device model
final String deviceName; // Device name
final int simCount; // Number of SIMs
final bool supportsMultiSim; // Multi-SIM support
final bool ussdSupported; // Basic USSD support
final bool ussdDirectSupported; // Direct USSD support
final bool multiSessionSupported; // Multi-session support
final bool accessibilityServiceSupported; // Accessibility service
final bool canMakePhoneCalls; // Can make calls
final bool? hasCallPermission; // Call permission (Android)
final bool? hasReadPhoneStatePermission; // State permission (Android)
final List<String>? limitations; // Limitations (iOS)
final Map<String, dynamic> additionalInfo; // Additional info
// Conversion and display methods
factory SystemInfo.fromMap(Map<String, dynamic> map);
Map<String, dynamic> toMap();
@override String toString(); // Readable representation
}Result of accessibility event channel configuration.
class AccessibilityEventChannelResult {
final bool success; // If successful
final String message; // Result message
final String? error; // Error if failed
final bool serviceEnabled; // Service enabled
final String? channelId; // Channel ID
// Conversion methods
factory AccessibilityEventChannelResult.fromMap(Map<String, dynamic> map);
Map<String, dynamic> toMap();
}Result of multi-USSD session operations.
class MultiSessionResult {
final bool success; // If successful
final String message; // Result message
final String? error; // Error if failed
final String? sessionId; // Session ID
final String? ussdResponse; // USSD response
final bool sessionActive; // Session active
// Conversion methods
factory MultiSessionResult.fromMap(Map<String, dynamic> map);
Map<String, dynamic> toMap();
}Execute a USSD code using native system functionality.
Parameters:
ussdCode(String, required): USSD code to execute (e.g.: "*123#")subscriptionId(int?, optional): SIM ID to use on multi-SIM devices
Returns: Future<UssdResponse>
Example:
final response = await UssdHandler.executeUssd('*#06#', subscriptionId: 1);
if (response.success) {
print('IMEI: ${response.response}');
} else {
print('Error: ${response.errorMessage}');
}executeUssdDirect(String ussdCode, {int timeoutSeconds = 30, int? subscriptionId, bool iosFallbackToStandard = false})
Execute a direct USSD code without showing dialogs (Android) or with fallback on iOS.
Parameters:
ussdCode(String, required): USSD code to executetimeoutSeconds(int, optional): Timeout in seconds (default: 30)subscriptionId(int?, optional): Specific SIM IDiosFallbackToStandard(bool, optional): Use executeUssd as fallback on iOS (default: false)
Returns: Future<String?> - Direct response or null
Example:
// Android only (original behavior)
final response = await UssdHandler.executeUssdDirect('*123#');
// Cross-platform with fallback
final response = await UssdHandler.executeUssdDirect(
'*123#',
iosFallbackToStandard: true,
);Check if device supports USSD codes.
Returns: Future<bool>
Get platform version.
Returns: Future<String?>
Get detailed system information for diagnostics.
Returns: Future<SystemInfo?>
Usage example:
final systemInfo = await UssdHandler.getSystemInfo();
if (systemInfo != null) {
print('Platform: ${systemInfo.platform}');
print('Model: ${systemInfo.deviceModel}');
print('USSD supported: ${systemInfo.ussdSupported}');
print('Multi-session: ${systemInfo.multiSessionSupported}');
// Use toString() for complete representation
print(systemInfo.toString());
// Access specific information
if (systemInfo.platform == 'android') {
print('Android API: ${systemInfo.androidVersion}');
print('Call permissions: ${systemInfo.hasCallPermission}');
}
// Additional information
systemInfo.additionalInfo.forEach((key, value) {
print('$key: $value');
});
}Check if it has all necessary permissions for direct USSD.
Returns: Future<bool>
Android only: Checks CALL_PHONE and READ_PHONE_STATE permissions.
Start a multi-USSD session.
Parameters:
ussdCode(String): USSD code to executesubscriptionId(int?, optional): Specific SIM for multi-SIM
Returns: Future<MultiSessionResult>
Example:
final result = await UssdHandler.startMultiSessionUssd('*123#');
if (result.success) {
print('Session started: ${result.message}');
print('Session ID: ${result.sessionId}');
print('Initial response: ${result.ussdResponse}');
} else {
print('Error: ${result.error}');
}Send a message in the active multi-USSD session.
Parameters:
message(String): Message/option to send (e.g.: "1", "2", "*", "#")
Returns: Future<MultiSessionResult>
Example:
final result = await UssdHandler.sendMessageInSession('1');
if (result.success) {
print('Message sent: ${result.message}');
print('New response: ${result.ussdResponse}');
} else {
print('Error: ${result.error}');
}Check if there's an active multi-USSD session.
Returns: Future<bool>
Cancel the active multi-USSD session.
Returns: Future<MultiSessionResult>
Example:
final result = await UssdHandler.cancelMultiSession();
if (result.success) {
print('Session cancelled: ${result.message}');
} else {
print('Error cancelling: ${result.error}');
}Complete flow example:
// 1. Start
final start = await UssdHandler.startMultiSessionUssd('*123#');
if (start.success) {
print('Started: ${start.ussdResponse}');
// 2. Navigate
final nav = await UssdHandler.sendMessageInSession('1');
if (nav.success) {
print('Navigated: ${nav.ussdResponse}');
}
// 3. Finish
final end = await UssdHandler.cancelMultiSession();
print('Finished: ${end.message}');
}Gets detailed system information for diagnostics.
Returns: SystemInfo?
Checks if it has all necessary permissions.
Returns: bool
- Required permissions: CALL_PHONE and READ_PHONE_STATE
- Android 8+: Direct USSD requires additional permissions
- Accessibility services: Require manual user enablement
- Carrier-specific: Some USSD codes may vary by carrier
- No automatic capture: iOS doesn't allow intercepting USSD responses
- Native app only: USSD codes open the phone application
- No direct USSD: Not available due to system restrictions
- No multi-session: iOS doesn't support persistent USSD dialogs
- Limited information: SIM data restricted by the system
- Carrier dependent: Not all USSD codes work with all carriers
- Real device testing: Recommended for complete validation
- Regional variations: USSD codes may vary by country/region
*#06#- Device IMEI*#21#- Call forwarding status*#30#- Caller ID status*#31#- Caller ID blocking status
*#*#4636#*#*- Phone information (Android)*#12580*369#- Software/hardware information*#0*#- Service menu (Samsung)
*#*#7780#*#*- Factory reset (Caution!)*#*#34971539#*#*- Detailed camera information*#*#0588#*#*- Proximity sensor test
Contributions are welcome! Please read:
CONTRIBUTING.md - Complete contribution guide
- 📱 Testing on real iOS devices
- 🔧 Android API improvements
- 📚 Documentation and examples
- 🧪 Additional tests
- 🌍 Support for regional USSD codes
This project is under the MIT License. See the LICENSE file for more details.
- Issues: GitHub Issues
- Documentation: See
.mdfiles in the repository - Examples:
example/folder
Developed with ❤️ for the Flutter community