sms_insights_flutter 0.0.1-dev.2
sms_insights_flutter: ^0.0.1-dev.2 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:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sms_insights_flutter/sms_insights_flutter.dart';
import 'client_credentials.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
const purple = Color(0xFF6200EE);
return MaterialApp(
title: 'SMS Insights Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: false,
primaryColor: purple,
colorScheme: const ColorScheme.light(primary: purple),
tabBarTheme: const TabBarThemeData(
labelColor: purple,
unselectedLabelColor: Colors.grey,
indicatorColor: purple,
),
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin {
static const _prefsClientEmail = 'CLIENT_EMAIL';
static const _prefsAccessKey = 'ACCESS_KEY';
static const _prefsCustomerId = 'CUSTOMER_ID';
static const _prefsCompanyName = 'COMPANY_NAME';
static const _prefsSalary = 'SALARY';
late final TabController _tabController;
final _clientEmailCtrl = TextEditingController();
final _accessKeyCtrl = TextEditingController();
final _customerIdCtrl = TextEditingController();
final _customUrlCtrl = TextEditingController();
final _incomeCompanyCtrl = TextEditingController();
final _incomeSalaryCtrl = TextEditingController();
final _deviceMatchPhoneCtrl = TextEditingController();
SharedPreferences? _prefs;
StreamSubscription<SdkEvent>? _sub;
String _clientEmail = '';
String _accessKey = '';
String _customerId = '';
String _customUrl = '';
SdkEnvironmentKind _environment = SdkEnvironmentKind.staging;
bool _suppressEnvironmentCredentialUpdate = true;
bool _customUrlSet = false;
bool _incomePredictionChecked = false;
bool _incomePredictionSaved = false;
bool _incomeInputsEditable = true;
String? _savedIncomeCompany;
int? _savedIncomeSalary;
bool _deviceMatchSectionVisible = false;
bool _deviceMatchByMobile = false;
bool _deviceMatchSaved = false;
bool _deviceMatchInputsEditable = true;
bool _initEnabled = false;
bool _postInitEnabled = false;
bool _busy = false;
bool _environmentEnabled = true;
bool _incomePredictionEnabled = true;
bool _credentialsEnabled = true;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_sub = SmsSdkWrapper.instance.events.listen(_onEvent);
_loadPreferences();
}
@override
void dispose() {
_sub?.cancel();
_tabController.dispose();
_clientEmailCtrl.dispose();
_accessKeyCtrl.dispose();
_customerIdCtrl.dispose();
_customUrlCtrl.dispose();
_incomeCompanyCtrl.dispose();
_incomeSalaryCtrl.dispose();
_deviceMatchPhoneCtrl.dispose();
super.dispose();
}
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
_prefs = prefs;
_customerId = prefs.getString(_prefsCustomerId) ?? '';
_clientEmail =
prefs.getString(_prefsClientEmail) ?? ClientCredentials.clientEmail;
_accessKey = prefs.getString(_prefsAccessKey) ?? ClientCredentials.accessKey;
final company = prefs.getString(_prefsCompanyName);
final salaryStr = prefs.getString(_prefsSalary);
if (company != null && salaryStr != null) {
final salary = int.tryParse(salaryStr);
if (salary != null) {
_savedIncomeCompany = company;
_savedIncomeSalary = salary;
_incomeCompanyCtrl.text = company;
_incomeSalaryCtrl.text = salaryStr;
}
}
if (!mounted) return;
setState(() {
_clientEmailCtrl.text = _clientEmail;
_accessKeyCtrl.text = _accessKey;
_customerIdCtrl.text = _customerId;
_suppressEnvironmentCredentialUpdate = false;
_updateInitEnabled();
});
}
void _onEvent(SdkEvent event) {
if (event is InitSuccessEvent) {
setState(() {
_postInitEnabled = true;
_initEnabled = false;
_environmentEnabled = false;
_incomePredictionEnabled = false;
_credentialsEnabled = 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 UploadFailureEvent) {
_toast('Upload failure (${event.errorCode}): ${event.message}');
} else if (event is SmsPermissionNotAvailableEvent) {
_toast('SMS permission not available: ${event.message}');
} else if (event is InitNotCalledEvent) {
_toast('Init not called: ${event.message}');
} else if (event is ForgetUserFailureEvent) {
_toast('${event.errorCode} ${event.message}');
} else if (event is MatchDeviceFailureEvent) {
_toast('Device match failure (${event.errorCode}): ${event.message}');
} else if (event is RequestPermissionsEvent) {
unawaited(_handlePermissionRequest(event));
}
}
Future<void> _handlePermissionRequest(RequestPermissionsEvent event) async {
try {
final granted = await _requestPermissions(event.permissions);
await SmsSdkWrapper.instance.grantPermissions(
requestId: event.requestId,
grantedPermissions: granted,
);
} catch (e) {
_toast('Permission request failed: $e');
}
}
void _updateInitEnabled() {
final hasCredentials = _customerId.isNotEmpty &&
_clientEmail.isNotEmpty &&
_accessKey.isNotEmpty;
final customOk =
_environment != SdkEnvironmentKind.custom || _customUrlSet;
_initEnabled = hasCredentials && customOk && !_postInitEnabled;
}
Future<void> _savePref(String key, String value) async {
await _prefs?.setString(key, value);
}
void _onClientEmailChanged(String value) {
_clientEmail = value.trim();
_savePref(_prefsClientEmail, _clientEmail);
setState(_updateInitEnabled);
}
void _onAccessKeyChanged(String value) {
_accessKey = value.trim();
_savePref(_prefsAccessKey, _accessKey);
setState(_updateInitEnabled);
}
void _onCustomerIdChanged(String value) {
_customerId = value.trim();
_savePref(_prefsCustomerId, _customerId);
setState(_updateInitEnabled);
}
void _onEnvironmentChanged(SdkEnvironmentKind? value) {
if (value == null) return;
setState(() {
_environment = value;
if (value == SdkEnvironmentKind.custom) {
_customUrlSet = false;
if (_customUrlCtrl.text.trim().isEmpty) {
_customUrlCtrl.text = 'https://';
_customUrlCtrl.selection = TextSelection.collapsed(
offset: _customUrlCtrl.text.length,
);
}
_updateInitEnabled();
} else {
_customUrlSet = true;
if (!_suppressEnvironmentCredentialUpdate) {
final creds = ClientCredentials.credentialsForEnvironment(value);
if (creds.email.isNotEmpty) {
_clientEmail = creds.email;
_accessKey = creds.accessKey;
_clientEmailCtrl.text = _clientEmail;
_accessKeyCtrl.text = _accessKey;
_savePref(_prefsClientEmail, _clientEmail);
_savePref(_prefsAccessKey, _accessKey);
}
}
_updateInitEnabled();
}
});
}
void _submitCustomUrl() {
final input = _customUrlCtrl.text.trim();
if (input.isEmpty) {
_toast('URL should not be empty.');
return;
}
setState(() {
_customUrl = input;
_customUrlSet = true;
_updateInitEnabled();
});
_toast('URL set.');
}
void _onCustomUrlChanged(String value) {
if (_environment == SdkEnvironmentKind.custom && !_customUrlSet) {
setState(() {
_customUrlSet = false;
_updateInitEnabled();
});
}
}
void _updateIncomePredictionVisibility(bool visible) {
setState(() {
_incomePredictionChecked = visible;
if (!visible) {
_incomePredictionSaved = false;
_savedIncomeCompany = null;
_savedIncomeSalary = null;
_incomeCompanyCtrl.clear();
_incomeSalaryCtrl.clear();
_setIncomeInputsEditable(true);
}
});
}
void _saveIncomePrediction() {
final company = _incomeCompanyCtrl.text.trim();
final salaryText = _incomeSalaryCtrl.text.trim();
if (company.isEmpty || salaryText.isEmpty) {
_toast('Please enter company name and salary.');
return;
}
final salary = int.tryParse(salaryText);
if (salary == null) {
_toast('Please enter a valid salary.');
return;
}
_savePref(_prefsCompanyName, company);
_savePref(_prefsSalary, salary.toString());
setState(() {
_savedIncomeCompany = company;
_savedIncomeSalary = salary;
_incomePredictionSaved = true;
_setIncomeInputsEditable(false);
});
_toast('Income prediction saved.');
}
void _resetIncomePrediction() {
setState(() {
_incomePredictionSaved = false;
_savedIncomeCompany = null;
_savedIncomeSalary = null;
_incomeCompanyCtrl.clear();
_incomeSalaryCtrl.clear();
_setIncomeInputsEditable(true);
});
}
void _setIncomeInputsEditable(bool editable) {
_incomeInputsEditable = editable;
}
void _showDeviceMatchSection() {
setState(() {
_deviceMatchSectionVisible = true;
_deviceMatchByMobile = false;
_deviceMatchSaved = false;
_deviceMatchPhoneCtrl.clear();
_setDeviceMatchInputsEditable(true);
});
}
void _setDeviceMatchInputsEditable(bool editable) {
_deviceMatchInputsEditable = editable;
}
Future<void> _saveDeviceMatch() async {
if (_deviceMatchByMobile && _deviceMatchPhoneCtrl.text.trim().isEmpty) {
_toast('Please enter a mobile number');
return;
}
setState(() => _busy = true);
try {
await _applyDeviceMatch(
_deviceMatchByMobile ? _deviceMatchPhoneCtrl.text.trim() : null,
);
if (!mounted) return;
setState(() {
_deviceMatchSaved = true;
_setDeviceMatchInputsEditable(false);
});
FocusScope.of(context).unfocus();
} finally {
if (mounted) setState(() => _busy = false);
}
}
void _resetDeviceMatch() {
setState(() {
_deviceMatchByMobile = false;
_deviceMatchPhoneCtrl.clear();
_deviceMatchSaved = false;
_setDeviceMatchInputsEditable(true);
});
}
Future<void> _applyDeviceMatch(String? phoneNo) async {
try {
await SmsSdkWrapper.instance.setDeviceMatch(
DeviceMatch(
email: ClientCredentials.demoDeviceMatchEmail,
name: ClientCredentials.demoDeviceMatchName,
phoneNo: phoneNo?.isNotEmpty == true ? phoneNo : null,
),
);
} on SmsInsightsException catch (e) {
_toast(e.message);
}
}
Future<void> _checkPermissionAndInit() async {
var status = await Permission.sms.status;
if (!status.isGranted) {
if (!mounted) return;
final proceed = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
content: const Text(
'This feature requires permission to access your device\'s SMS. '
'Granting this permission enables interaction with the application '
'more efficiently. If you decline, you will not be able to proceed further.',
),
actions: [
TextButton(
onPressed: () async {
await Permission.sms.request();
if (mounted) Navigator.pop(context, true);
},
child: const Text('Grant Permission'),
),
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('No Thanks'),
),
],
),
);
if (proceed != true) return;
status = await Permission.sms.status;
if (!status.isGranted) {
_toast('SMS permission is required to initialise the SDK');
return;
}
}
await _initSdk();
}
SdkClientConfig _buildConfig() {
CustomEnvironmentConfig? customEnv;
if (_environment == SdkEnvironmentKind.custom) {
customEnv = CustomEnvironmentConfig(
testEmail: _clientEmail,
accessKey: _accessKey,
envUrl: _customUrl,
signozUrl: _customUrl,
);
}
IncomeEstimation? income;
if (_incomePredictionChecked &&
_incomePredictionSaved &&
_savedIncomeSalary != null) {
income = IncomeEstimation(
salary: _savedIncomeSalary!.toDouble(),
company: _savedIncomeCompany ?? '',
);
}
return SdkClientConfig(
clientEmail: _clientEmail,
accessId: _accessKey,
customerId: _customerId,
upperLimit: 190,
environment: _environment,
customEnvironment: customEnv,
incomePrediction: income,
);
}
Future<void> _initSdk() async {
setState(() => _busy = true);
try {
await SmsSdkWrapper.instance.init(_buildConfig());
} on SmsInsightsException catch (e) {
_toast('${e.code}: ${e.message}');
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _startSync() async {
if (_deviceMatchSectionVisible && !_deviceMatchSaved) {
if (_deviceMatchByMobile) {
final phone = _deviceMatchPhoneCtrl.text.trim();
if (phone.isEmpty) {
_toast('Please enter a mobile number');
return;
}
await _applyDeviceMatch(phone);
} else {
await _applyDeviceMatch(null);
}
setState(() {
_deviceMatchSaved = true;
_setDeviceMatchInputsEditable(false);
});
}
setState(() => _busy = true);
try {
await SmsSdkWrapper.instance.startSync();
} on SmsInsightsException catch (e) {
_toast(e.message);
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _forgetUser() async {
setState(() => _busy = true);
try {
await SmsSdkWrapper.instance.forgetUser();
setState(() {
_postInitEnabled = false;
_environmentEnabled = true;
_incomePredictionEnabled = true;
_credentialsEnabled = true;
_incomePredictionChecked = false;
_incomePredictionSaved = false;
_incomeInputsEditable = true;
_savedIncomeCompany = null;
_savedIncomeSalary = null;
_incomeCompanyCtrl.clear();
_incomeSalaryCtrl.clear();
_deviceMatchSectionVisible = false;
_deviceMatchByMobile = false;
_deviceMatchSaved = false;
_deviceMatchPhoneCtrl.clear();
_setDeviceMatchInputsEditable(true);
if (_environment == SdkEnvironmentKind.custom) {
_customUrlSet = false;
_customUrlCtrl.text =
_customUrl.isNotEmpty ? _customUrl : 'https://';
_customUrlCtrl.selection = TextSelection.collapsed(
offset: _customUrlCtrl.text.length,
);
}
_customUrl = '';
_updateInitEnabled();
});
await _prefs?.remove(_prefsCompanyName);
await _prefs?.remove(_prefsSalary);
} on SmsInsightsException catch (e) {
_toast(e.message);
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<List<String>> _requestPermissions(List<String> requested) async {
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 [];
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)),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'ACCOUNT DETAILS'),
Tab(text: 'SDK ACTIONS'),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildAccountDetailsTab(),
_buildSdkActionsTab(),
],
),
),
],
),
),
);
}
Widget _buildAccountDetailsTab() {
final isCustom = _environment == SdkEnvironmentKind.custom;
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_OutlinedField(
label: 'Client Email registered with PayU',
controller: _clientEmailCtrl,
enabled: _credentialsEnabled,
keyboardType: TextInputType.emailAddress,
onChanged: _onClientEmailChanged,
),
const SizedBox(height: 12),
_OutlinedField(
label: 'Access Key provided by PayU',
controller: _accessKeyCtrl,
enabled: _credentialsEnabled,
onChanged: _onAccessKeyChanged,
),
const SizedBox(height: 12),
_OutlinedField(
label: 'Customer Id',
controller: _customerIdCtrl,
enabled: _credentialsEnabled,
textInputAction: TextInputAction.done,
onChanged: _onCustomerIdChanged,
onSubmitted: (_) => FocusScope.of(context).unfocus(),
),
const SizedBox(height: 16),
Row(
children: [
const Text('Environment'),
const Spacer(),
DropdownButton<SdkEnvironmentKind>(
value: _environment,
underline: const SizedBox.shrink(),
onChanged: _environmentEnabled ? _onEnvironmentChanged : null,
items: const [
DropdownMenuItem(
value: SdkEnvironmentKind.staging,
child: Text('Staging'),
),
DropdownMenuItem(
value: SdkEnvironmentKind.custom,
child: Text('Custom'),
),
DropdownMenuItem(
value: SdkEnvironmentKind.production,
child: Text('Production'),
),
],
),
],
),
if (isCustom) ...[
const SizedBox(height: 8),
TextField(
controller: _customUrlCtrl,
enabled: _credentialsEnabled && !_customUrlSet,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding:
EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
onChanged: _onCustomUrlChanged,
onSubmitted: (_) => _submitCustomUrl(),
),
const SizedBox(height: 8),
Center(
child: ElevatedButton(
onPressed: (_credentialsEnabled && !_customUrlSet)
? _submitCustomUrl
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey.shade300,
foregroundColor: Colors.black87,
),
child: const Text('Set URL'),
),
),
],
const SizedBox(height: 16),
CheckboxListTile(
value: _incomePredictionChecked,
onChanged: _incomePredictionEnabled
? (v) => _updateIncomePredictionVisibility(v ?? false)
: null,
title: const Text('Income Prediction (Optional)'),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
if (_incomePredictionChecked) ...[
Row(
children: [
Expanded(
child: _OutlinedField(
label: 'Company name',
controller: _incomeCompanyCtrl,
enabled:
_incomePredictionEnabled && _incomeInputsEditable,
),
),
const SizedBox(width: 8),
Expanded(
child: _OutlinedField(
label: 'Salary',
controller: _incomeSalaryCtrl,
enabled:
_incomePredictionEnabled && _incomeInputsEditable,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textInputAction: TextInputAction.done,
onSubmitted: (_) => _saveIncomePrediction(),
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: (_incomePredictionEnabled && _incomeInputsEditable)
? _saveIncomePrediction
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey.shade300,
foregroundColor: Colors.black87,
),
child: const Text('Save'),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed:
(_incomePredictionEnabled && !_incomeInputsEditable)
? _resetIncomePrediction
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey.shade300,
foregroundColor: Colors.black87,
),
child: const Text('Reset'),
),
],
),
],
],
),
);
}
Widget _buildDeviceMatchSection() {
return ClipRect(
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: _deviceMatchSectionVisible
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 16),
CheckboxListTile(
value: _deviceMatchByMobile,
onChanged: _deviceMatchInputsEditable && !_busy
? (v) {
setState(() {
_deviceMatchByMobile = v ?? false;
if (!_deviceMatchByMobile) {
_deviceMatchPhoneCtrl.clear();
}
});
}
: null,
title: const Text('Mobile no.'),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
ClipRect(
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: _deviceMatchByMobile
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 8),
_OutlinedField(
label: 'Mobile number',
controller: _deviceMatchPhoneCtrl,
enabled: _deviceMatchInputsEditable,
keyboardType: TextInputType.phone,
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _deviceMatchInputsEditable &&
!_busy
? _saveDeviceMatch
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey.shade300,
foregroundColor: Colors.black87,
),
child: const Text('Save'),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: !_deviceMatchInputsEditable &&
!_busy
? _resetDeviceMatch
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey.shade300,
foregroundColor: Colors.black87,
),
child: const Text('Reset'),
),
],
),
],
)
: const SizedBox.shrink(),
),
),
],
)
: const SizedBox.shrink(),
),
);
}
Widget _buildSdkActionsTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_SdkButton(
label: 'INIT SDK',
enabled: _initEnabled && !_busy,
onPressed: _checkPermissionAndInit,
),
const SizedBox(height: 16),
_SdkButton(
label: 'DEVICE MATCH (OPTIONAL)',
enabled: _postInitEnabled && !_busy,
onPressed: _showDeviceMatchSection,
),
_buildDeviceMatchSection(),
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 _OutlinedField extends StatelessWidget {
final String label;
final TextEditingController controller;
final bool enabled;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final ValueChanged<String>? onChanged;
final ValueChanged<String>? onSubmitted;
final List<TextInputFormatter>? inputFormatters;
const _OutlinedField({
required this.label,
required this.controller,
this.enabled = true,
this.keyboardType,
this.textInputAction,
this.onChanged,
this.onSubmitted,
this.inputFormatters,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
enabled: enabled,
keyboardType: keyboardType,
textInputAction: textInputAction,
onChanged: onChanged,
onSubmitted: onSubmitted,
inputFormatters: inputFormatters,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
);
}
}
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(
backgroundColor: Colors.grey.shade300,
disabledBackgroundColor: Colors.grey.shade200,
foregroundColor: Colors.black87,
disabledForegroundColor: Colors.grey.shade500,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 2,
textStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 1.0,
),
),
child: Text(label),
);
}
}