sms_insights_flutter 0.0.1-dev.1
sms_insights_flutter: ^0.0.1-dev.1 copied to clipboard
Flutter plugin wrapping the PayU SMS Insights Android SDK (core + optional logger). Reads transactional SMS for credit assessment with consent-based permission flow.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:sms_insights_flutter/sms_insights_flutter.dart';
// Pass credentials at build time:
// flutter run --dart-define=CLIENT_EMAIL=you@co.com --dart-define=ACCESS_KEY=key
const _clientEmail = String.fromEnvironment('CLIENT_EMAIL');
const _accessKey = String.fromEnvironment('ACCESS_KEY');
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'SMS Insights Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: false),
home: const HomeScreen(),
);
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _customerIdCtrl = TextEditingController();
String _customerId = 'customer-123';
bool _initEnabled = true;
bool _postInitEnabled = false;
bool _busy = false;
StreamSubscription<SdkEvent>? _sub;
@override
void initState() {
super.initState();
_sub = SmsSdkWrapper.instance.events.listen(_onEvent);
}
@override
void dispose() {
_sub?.cancel();
_customerIdCtrl.dispose();
super.dispose();
}
// ── event listener ────────────────────────────────────────────────────────
void _onEvent(SdkEvent event) {
if (event is InitSuccessEvent) {
setState(() {
_postInitEnabled = true;
_initEnabled = false;
});
_toast('Init Success');
} else if (event is InitFailureEvent) {
_toast('Init Failure (${event.code}): ${event.message}');
} else if (event is SmsSyncSuccessEvent) {
_toast(event.message);
} else if (event is NoMessagesToSyncEvent) {
_toast(event.message);
} else if (event is AuthFailureEvent) {
_toast('Auth failure (${event.code}): ${event.message}');
} else if (event is UploadFailureEvent) {
_toast('Upload failure: ${event.message}');
} else if (event is PermissionNotAvailableEvent) {
_toast('Permission not available: ${event.message}');
} else if (event is InitNotCalledEvent) {
_toast('Init not called: ${event.message}');
} else if (event is ForgetUserFailureEvent) {
_toast('${event.code} ${event.message}');
} else if (event is MatchDeviceFailureEvent) {
_toast('Device match failure: ${event.message}');
} else if (event is SdkConfigFailureEvent) {
_toast(event.message);
}
}
// ── actions ───────────────────────────────────────────────────────────────
void _submitCustomerId() {
final val = _customerIdCtrl.text.trim();
if (val.isEmpty) {
_toast('Customer Id should not be empty.');
return;
}
setState(() => _customerId = val);
_customerIdCtrl.clear();
FocusScope.of(context).unfocus();
}
Future<void> _checkPermissionAndInit() async {
final status = await Permission.sms.status;
if (!status.isGranted) {
if (!mounted) return;
final proceed = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('SMS Permission'),
content: const Text(
'SMS access is needed to analyse your transactions. '
'You can still continue without granting permission.',
),
actions: [
TextButton(
onPressed: () async {
await Permission.sms.request();
if (mounted) Navigator.pop(context, true);
},
child: const Text('GRANT PERMISSION'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('NO THANKS'),
),
],
),
);
if (proceed != true) return;
}
await _initSdk();
}
Future<void> _initSdk() async {
if (_clientEmail.isEmpty || _accessKey.isEmpty) {
_toast('Credentials not set. Pass --dart-define=CLIENT_EMAIL=... --dart-define=ACCESS_KEY=...');
return;
}
setState(() => _busy = true);
try {
await SmsSdkWrapper.instance.init(
SdkClientConfig(
clientEmail: _clientEmail,
accessId: _accessKey,
customerId: _customerId,
upperLimit: 190,
salaryInRs: 100000,
companyName: 'PayU',
),
);
} on SmsInsightsException catch (e) {
_toast('${e.code}: ${e.message}');
} finally {
setState(() => _busy = false);
}
}
Future<void> _deviceMatch() async {
setState(() => _busy = true);
try {
await SmsSdkWrapper.instance.setDeviceMatch(
const DeviceMatch(phoneNo: '7307140364', email: 'rahularora'),
);
_toast('setDeviceMatch called');
} on SmsInsightsException catch (e) {
_toast(e.message);
} finally {
setState(() => _busy = false);
}
}
Future<void> _startSync() async {
setState(() => _busy = true);
try {
await SmsSdkWrapper.instance.startSyncAndHandlePermissions(
onPermissionsRequested: _requestPermissions,
);
} on SmsInsightsException catch (e) {
_toast(e.message);
} finally {
setState(() => _busy = false);
}
}
Future<void> _forgetUser() async {
setState(() => _busy = true);
try {
await SmsSdkWrapper.instance.forgetUser();
setState(() {
_postInitEnabled = false;
_initEnabled = true;
});
} on SmsInsightsException catch (e) {
_toast(e.message);
} finally {
setState(() => _busy = false);
}
}
Future<List<String>> _requestPermissions(List<String> requested) async {
// Build Permission → [android name, ...] mapping.
// ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION both map to Permission.location,
// so if location is granted we return both strings (SDK expects both in the list).
final mapping = <Permission, List<String>>{};
for (final p in requested) {
final handler = switch (p) {
'android.permission.READ_SMS' => Permission.sms,
'android.permission.ACCESS_FINE_LOCATION' => Permission.location,
'android.permission.ACCESS_COARSE_LOCATION' => Permission.location,
'android.permission.READ_PHONE_STATE' => Permission.phone,
'android.permission.READ_PHONE_NUMBERS' => Permission.phone,
_ => null,
};
if (handler != null) (mapping[handler] ??= []).add(p);
}
if (mapping.isEmpty) return [];
// Single batch request — one system dialog, same as Kotlin's
// requestPermissionsLauncher.launch(permissions.toTypedArray())
final statuses = await mapping.keys.toList().request();
return [
for (final entry in statuses.entries)
if (entry.value.isGranted) ...?mapping[entry.key],
];
}
void _toast(String msg) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(msg), duration: const Duration(seconds: 3)),
);
}
// ── UI ────────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Customer ID row
TextField(
controller: _customerIdCtrl,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _submitCustomerId(),
decoration: const InputDecoration(
hintText: 'Enter customerId',
enabledBorder: UnderlineInputBorder(),
focusedBorder: UnderlineInputBorder(),
),
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _submitCustomerId,
child: const Text('Set Customer ID'),
),
),
const SizedBox(height: 40),
_SdkButton(
label: 'INIT SDK',
enabled: _initEnabled && !_busy,
onPressed: _checkPermissionAndInit,
),
const SizedBox(height: 16),
_SdkButton(
label: 'DEVICE MATCH',
enabled: _postInitEnabled && !_busy,
onPressed: _deviceMatch,
),
const SizedBox(height: 16),
_SdkButton(
label: 'START SYNC',
enabled: _postInitEnabled && !_busy,
onPressed: _startSync,
),
const SizedBox(height: 16),
_SdkButton(
label: 'FORGET USER',
enabled: _postInitEnabled && !_busy,
onPressed: _forgetUser,
),
if (_busy) ...[
const SizedBox(height: 32),
const Center(child: CircularProgressIndicator()),
],
],
),
),
),
);
}
}
class _SdkButton extends StatelessWidget {
final String label;
final bool enabled;
final VoidCallback onPressed;
const _SdkButton({
required this.label,
required this.enabled,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: enabled ? onPressed : null,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
textStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
),
),
child: Text(label),
);
}
}