From c48d4aa5c73c274d0c8a65a6799488321927dcd5 Mon Sep 17 00:00:00 2001 From: Fahim Date: Sun, 24 May 2026 21:08:35 +0600 Subject: [PATCH 1/8] Add files via upload --- lib/main.dart | 83 ++++++++++++++++----------------- lib/main.dart.bak | 97 +++++++++++++++++++-------------------- test/widget_test.dart.bak | 26 ++--------- 3 files changed, 90 insertions(+), 116 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index e960ca5..c908e56 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -83,7 +83,7 @@ class _SetupScreenState extends State { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); if (url.isEmpty || key.isEmpty) { - setState(() { _testResult = 'Worker URL এবং API Key দিন।'; _testSuccess = false; }); + setState(() { _testResult = 'Please enter Worker URL and API Key.'; _testSuccess = false; }); return; } setState(() { _testing = true; _testResult = ''; }); @@ -99,14 +99,14 @@ class _SetupScreenState extends State { ).timeout(const Duration(seconds: 15)); if (res.statusCode == 200 || res.statusCode == 201) { - setState(() { _testResult = '✅ সংযোগ সফল! Worker সঠিকভাবে কাজ করছে।'; _testSuccess = true; }); + setState(() { _testResult = '✅ Connection successful! Worker is running correctly.'; _testSuccess = true; }); } else if (res.statusCode == 401) { - setState(() { _testResult = '❌ API Key ভুল।'; _testSuccess = false; }); + setState(() { _testResult = '❌ Invalid API Key.'; _testSuccess = false; }); } else { setState(() { _testResult = '⚠️ Server response: ${res.statusCode}'; _testSuccess = false; }); } } on TimeoutException { - setState(() { _testResult = '❌ Timeout। URL চেক করুন।'; _testSuccess = false; }); + setState(() { _testResult = '❌ Timeout. Please check the URL.'; _testSuccess = false; }); } catch (e) { setState(() { _testResult = '❌ Error: ${e.toString().substring(0, e.toString().length.clamp(0, 80))}'; _testSuccess = false; }); } @@ -116,9 +116,9 @@ class _SetupScreenState extends State { Future _saveAndContinue() async { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); - if (url.isEmpty) { _showSnack('Worker URL দিন'); return; } - if (key.isEmpty) { _showSnack('API Key দিন'); return; } - if (!url.startsWith('http')) { _showSnack('URL https:// দিয়ে শুরু হওয়া উচিত'); return; } + if (url.isEmpty) { _showSnack('Please enter Worker URL'); return; } + if (key.isEmpty) { _showSnack('Please enter API Key'); return; } + if (!url.startsWith('http')) { _showSnack('URL must start with https://'); return; } setState(() => _saving = true); await _requestPermissions(); @@ -129,7 +129,6 @@ class _SetupScreenState extends State { await prefs.setBool(kEnabled, true); await prefs.setBool(kSetupDone, true); - // Foreground Service চালু করো try { await _smsChannel.invokeMethod('startService'); } catch (_) {} if (mounted) { @@ -180,15 +179,15 @@ class _SetupScreenState extends State { const SizedBox(height: 32), _infoCard( icon: Icons.info_outline, color: kPrimary, - title: 'প্রথমবার Setup', - body: 'এই app আপনার bKash SMS গুলো স্বয়ংক্রিয়ভাবে আপনার Cloudflare Worker-এ forward করবে।', + title: 'Initial Setup', + body: 'This app will automatically forward your bKash SMS messages to your Cloudflare Worker.', ), const SizedBox(height: 20), _label('Worker URL'), _textField(controller: _urlCtrl, hint: 'https://your-worker.workers.dev', icon: Icons.link, keyboard: TextInputType.url), const SizedBox(height: 16), _label('API Key'), - _textField(controller: _keyCtrl, hint: 'আপনার Worker এর API Key', icon: Icons.key, obscure: true), + _textField(controller: _keyCtrl, hint: 'Your Worker API Key', icon: Icons.key, obscure: true), const SizedBox(height: 20), SizedBox( width: double.infinity, @@ -197,7 +196,7 @@ class _SetupScreenState extends State { icon: _testing ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: kPrimary)) : const Icon(Icons.wifi_tethering, size: 18), - label: Text(_testing ? 'Testing...' : 'Connection Test করুন'), + label: Text(_testing ? 'Testing...' : 'Test Connection'), style: OutlinedButton.styleFrom( foregroundColor: kPrimary, side: const BorderSide(color: kPrimary), padding: const EdgeInsets.symmetric(vertical: 14), @@ -229,16 +228,16 @@ class _SetupScreenState extends State { ), child: _saving ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) - : const Text('Save করুন ও শুরু করুন →', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white)), + : const Text('Save & Get Started →', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white)), ), ), const SizedBox(height: 32), - _sectionTitle('কীভাবে কাজ করে?'), + _sectionTitle('How It Works'), const SizedBox(height: 12), - _stepCard('১', 'bKash থেকে SMS আসলে instantly forward হয়'), - _stepCard('২', 'Background এ প্রতি ১৫ সেকেন্ডে SMS inbox check করে'), - _stepCard('৩', 'Notification bar এ সবসময় status দেখা যায়'), - _stepCard('৪', 'Phone restart হলেও স্বয়ংক্রিয়ভাবে চালু হয়'), + _stepCard('1', 'Instantly forwards SMS as soon as it arrives from bKash'), + _stepCard('2', 'Checks SMS inbox every 15 seconds in the background'), + _stepCard('3', 'Status is always visible in the notification bar'), + _stepCard('4', 'Automatically restarts after phone reboot'), ], ), ), @@ -341,7 +340,6 @@ class _HomeScreenState extends State with WidgetsBindingObserver { _loadPrefs(); _checkPermissions(); _refreshTimer = Timer.periodic(const Duration(seconds: 3), (_) => _loadPrefs()); - // Service চালু করো _startService(); } @@ -355,7 +353,6 @@ class _HomeScreenState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { - // App background এ গেলে service নিশ্চিত করো _startService(); } } @@ -372,7 +369,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { _apiKey = prefs.getString(kApiKey) ?? ''; _enabled = prefs.getBool(kEnabled) ?? true; _forwardCount = prefs.getInt(kForwardCount) ?? 0; - _lastLog = prefs.getString(kLastLog) ?? 'কোনো activity নেই'; + _lastLog = prefs.getString(kLastLog) ?? 'No activity yet'; }); } @@ -451,11 +448,11 @@ class _HomeScreenState extends State with WidgetsBindingObserver { _alertCard( color: kDanger, bgColor: const Color(0xFF2D1212), icon: Icons.warning_amber, - title: 'SMS Permission নেই!', - body: 'SMS forward করতে permission দিতে হবে।', + title: 'SMS Permission Required!', + body: 'Permission is required to forward SMS messages.', action: TextButton( onPressed: _requestPerms, - child: const Text('Permission দিন', style: TextStyle(color: kDanger, fontWeight: FontWeight.w600)), + child: const Text('Grant Permission', style: TextStyle(color: kDanger, fontWeight: FontWeight.w600)), ), ), const SizedBox(height: 16), @@ -502,8 +499,8 @@ class _HomeScreenState extends State with WidgetsBindingObserver { const SizedBox(height: 2), Text( _enabled && _hasPerms - ? 'Notification চলছে • প্রতি ১৫ সেকেন্ডে check' - : _hasPerms ? 'Toggle করে চালু করুন' : 'SMS permission দিন', + ? 'Notification running • Checks every 15 seconds' + : _hasPerms ? 'Toggle to enable' : 'Grant SMS permission', style: const TextStyle(fontSize: 12, color: kMuted), ), ], @@ -542,13 +539,13 @@ class _HomeScreenState extends State with WidgetsBindingObserver { borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFF3B82F6).withValues(alpha: 0.3)), ), - child: Row( + child: const Row( children: [ - const Icon(Icons.notifications_active, color: Color(0xFF60A5FA), size: 20), - const SizedBox(width: 10), - const Expanded( + Icon(Icons.notifications_active, color: Color(0xFF60A5FA), size: 20), + SizedBox(width: 10), + Expanded( child: Text( - 'Background Service চলছে। Notification bar এ সবসময় দেখা যাবে। প্রতি ১৫ সেকেন্ডে SMS inbox check করে।', + 'Background service is running. Always visible in the notification bar. Checks SMS inbox every 15 seconds.', style: TextStyle(fontSize: 12, color: Color(0xFF93C5FD), height: 1.5), ), ), @@ -594,7 +591,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { child: OutlinedButton.icon( onPressed: _openSettings, icon: const Icon(Icons.tune, size: 18), - label: const Text('Settings খুলুন'), + label: const Text('Open Settings'), style: OutlinedButton.styleFrom( foregroundColor: kPrimary, side: const BorderSide(color: kPrimary), padding: const EdgeInsets.symmetric(vertical: 14), @@ -652,7 +649,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { children: [ Text(title, style: TextStyle(fontWeight: FontWeight.w600, color: color, fontSize: 13)), Text(body, style: const TextStyle(fontSize: 12, color: kMuted)), - ?action, + if (action != null) action, ], )), ], @@ -692,7 +689,7 @@ class _SettingsScreenState extends State { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); if (url.isEmpty || key.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('URL এবং API Key দিন'), backgroundColor: kDanger)); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter URL and API Key'), backgroundColor: kDanger)); return; } final prefs = await SharedPreferences.getInstance(); @@ -705,7 +702,7 @@ class _SettingsScreenState extends State { Future _testConnection() async { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); - if (url.isEmpty || key.isEmpty) { setState(() { _testResult = 'URL এবং API Key দিন।'; _testSuccess = false; }); return; } + if (url.isEmpty || key.isEmpty) { setState(() { _testResult = 'Please enter URL and API Key.'; _testSuccess = false; }); return; } setState(() { _testing = true; _testResult = ''; }); try { final res = await http.post( @@ -715,7 +712,7 @@ class _SettingsScreenState extends State { ).timeout(const Duration(seconds: 15)); setState(() { _testSuccess = res.statusCode >= 200 && res.statusCode < 300; - _testResult = _testSuccess ? '✅ সংযোগ সফল! (HTTP ${res.statusCode})' : '❌ HTTP ${res.statusCode}'; + _testResult = _testSuccess ? '✅ Connection successful! (HTTP ${res.statusCode})' : '❌ HTTP ${res.statusCode}'; }); } catch (e) { setState(() { _testResult = '❌ Error: ${e.toString().substring(0, e.toString().length.clamp(0, 80))}'; _testSuccess = false; }); @@ -728,11 +725,11 @@ class _SettingsScreenState extends State { context: context, builder: (ctx) => AlertDialog( backgroundColor: kCard, - title: const Text('Reset করবেন?', style: TextStyle(color: Colors.white)), - content: const Text('সব settings মুছে setup screen-এ যাবে।', style: TextStyle(color: kMuted)), + title: const Text('Reset App?', style: TextStyle(color: Colors.white)), + content: const Text('All settings will be cleared and you will be taken to the setup screen.', style: TextStyle(color: kMuted)), actions: [ - TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('না')), - TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('হ্যাঁ, Reset', style: TextStyle(color: kDanger))), + TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Yes, Reset', style: TextStyle(color: kDanger))), ], ), ); @@ -766,7 +763,7 @@ class _SettingsScreenState extends State { Container( width: double.infinity, padding: const EdgeInsets.all(12), margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration(color: const Color(0xFF0F2D1A), borderRadius: BorderRadius.circular(8), border: Border.all(color: kSuccess.withValues(alpha: 0.5))), - child: const Text('✅ Settings save হয়েছে!', style: TextStyle(color: kSuccess, fontSize: 13)), + child: const Text('✅ Settings saved successfully!', style: TextStyle(color: kSuccess, fontSize: 13)), ), const Text('Worker Configuration', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: Colors.white)), const SizedBox(height: 14), @@ -776,7 +773,7 @@ class _SettingsScreenState extends State { child: Column(children: [ _settingsField('Worker URL', _urlCtrl, 'https://your-worker.workers.dev', Icons.link), const SizedBox(height: 14), - _settingsField('API Key', _keyCtrl, 'আপনার API Key', Icons.key, obscure: true), + _settingsField('API Key', _keyCtrl, 'Your API Key', Icons.key, obscure: true), ]), ), const SizedBox(height: 14), @@ -817,7 +814,7 @@ class _SettingsScreenState extends State { child: OutlinedButton.icon( onPressed: _resetApp, icon: const Icon(Icons.restore, size: 16), - label: const Text('App Reset করুন'), + label: const Text('Reset App'), style: OutlinedButton.styleFrom(foregroundColor: kDanger, side: const BorderSide(color: kDanger), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), ), ), diff --git a/lib/main.dart.bak b/lib/main.dart.bak index 70e0888..c908e56 100644 --- a/lib/main.dart.bak +++ b/lib/main.dart.bak @@ -83,7 +83,7 @@ class _SetupScreenState extends State { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); if (url.isEmpty || key.isEmpty) { - setState(() { _testResult = 'Worker URL এবং API Key দিন।'; _testSuccess = false; }); + setState(() { _testResult = 'Please enter Worker URL and API Key.'; _testSuccess = false; }); return; } setState(() { _testing = true; _testResult = ''; }); @@ -99,14 +99,14 @@ class _SetupScreenState extends State { ).timeout(const Duration(seconds: 15)); if (res.statusCode == 200 || res.statusCode == 201) { - setState(() { _testResult = '✅ সংযোগ সফল! Worker সঠিকভাবে কাজ করছে।'; _testSuccess = true; }); + setState(() { _testResult = '✅ Connection successful! Worker is running correctly.'; _testSuccess = true; }); } else if (res.statusCode == 401) { - setState(() { _testResult = '❌ API Key ভুল।'; _testSuccess = false; }); + setState(() { _testResult = '❌ Invalid API Key.'; _testSuccess = false; }); } else { setState(() { _testResult = '⚠️ Server response: ${res.statusCode}'; _testSuccess = false; }); } } on TimeoutException { - setState(() { _testResult = '❌ Timeout। URL চেক করুন।'; _testSuccess = false; }); + setState(() { _testResult = '❌ Timeout. Please check the URL.'; _testSuccess = false; }); } catch (e) { setState(() { _testResult = '❌ Error: ${e.toString().substring(0, e.toString().length.clamp(0, 80))}'; _testSuccess = false; }); } @@ -116,9 +116,9 @@ class _SetupScreenState extends State { Future _saveAndContinue() async { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); - if (url.isEmpty) { _showSnack('Worker URL দিন'); return; } - if (key.isEmpty) { _showSnack('API Key দিন'); return; } - if (!url.startsWith('http')) { _showSnack('URL https:// দিয়ে শুরু হওয়া উচিত'); return; } + if (url.isEmpty) { _showSnack('Please enter Worker URL'); return; } + if (key.isEmpty) { _showSnack('Please enter API Key'); return; } + if (!url.startsWith('http')) { _showSnack('URL must start with https://'); return; } setState(() => _saving = true); await _requestPermissions(); @@ -129,7 +129,6 @@ class _SetupScreenState extends State { await prefs.setBool(kEnabled, true); await prefs.setBool(kSetupDone, true); - // Foreground Service চালু করো try { await _smsChannel.invokeMethod('startService'); } catch (_) {} if (mounted) { @@ -180,15 +179,15 @@ class _SetupScreenState extends State { const SizedBox(height: 32), _infoCard( icon: Icons.info_outline, color: kPrimary, - title: 'প্রথমবার Setup', - body: 'এই app আপনার bKash SMS গুলো স্বয়ংক্রিয়ভাবে আপনার Cloudflare Worker-এ forward করবে।', + title: 'Initial Setup', + body: 'This app will automatically forward your bKash SMS messages to your Cloudflare Worker.', ), const SizedBox(height: 20), _label('Worker URL'), _textField(controller: _urlCtrl, hint: 'https://your-worker.workers.dev', icon: Icons.link, keyboard: TextInputType.url), const SizedBox(height: 16), _label('API Key'), - _textField(controller: _keyCtrl, hint: 'আপনার Worker এর API Key', icon: Icons.key, obscure: true), + _textField(controller: _keyCtrl, hint: 'Your Worker API Key', icon: Icons.key, obscure: true), const SizedBox(height: 20), SizedBox( width: double.infinity, @@ -197,7 +196,7 @@ class _SetupScreenState extends State { icon: _testing ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: kPrimary)) : const Icon(Icons.wifi_tethering, size: 18), - label: Text(_testing ? 'Testing...' : 'Connection Test করুন'), + label: Text(_testing ? 'Testing...' : 'Test Connection'), style: OutlinedButton.styleFrom( foregroundColor: kPrimary, side: const BorderSide(color: kPrimary), padding: const EdgeInsets.symmetric(vertical: 14), @@ -229,16 +228,16 @@ class _SetupScreenState extends State { ), child: _saving ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) - : const Text('Save করুন ও শুরু করুন →', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white)), + : const Text('Save & Get Started →', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white)), ), ), const SizedBox(height: 32), - _sectionTitle('কীভাবে কাজ করে?'), + _sectionTitle('How It Works'), const SizedBox(height: 12), - _stepCard('১', 'bKash থেকে SMS আসলে instantly forward হয়'), - _stepCard('২', 'Background এ প্রতি ১৫ সেকেন্ডে SMS inbox check করে'), - _stepCard('৩', 'Notification bar এ সবসময় status দেখা যায়'), - _stepCard('৪', 'Phone restart হলেও স্বয়ংক্রিয়ভাবে চালু হয়'), + _stepCard('1', 'Instantly forwards SMS as soon as it arrives from bKash'), + _stepCard('2', 'Checks SMS inbox every 15 seconds in the background'), + _stepCard('3', 'Status is always visible in the notification bar'), + _stepCard('4', 'Automatically restarts after phone reboot'), ], ), ), @@ -341,7 +340,6 @@ class _HomeScreenState extends State with WidgetsBindingObserver { _loadPrefs(); _checkPermissions(); _refreshTimer = Timer.periodic(const Duration(seconds: 3), (_) => _loadPrefs()); - // Service চালু করো _startService(); } @@ -355,7 +353,6 @@ class _HomeScreenState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { - // App background এ গেলে service নিশ্চিত করো _startService(); } } @@ -372,7 +369,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { _apiKey = prefs.getString(kApiKey) ?? ''; _enabled = prefs.getBool(kEnabled) ?? true; _forwardCount = prefs.getInt(kForwardCount) ?? 0; - _lastLog = prefs.getString(kLastLog) ?? 'কোনো activity নেই'; + _lastLog = prefs.getString(kLastLog) ?? 'No activity yet'; }); } @@ -451,11 +448,11 @@ class _HomeScreenState extends State with WidgetsBindingObserver { _alertCard( color: kDanger, bgColor: const Color(0xFF2D1212), icon: Icons.warning_amber, - title: 'SMS Permission নেই!', - body: 'SMS forward করতে permission দিতে হবে।', + title: 'SMS Permission Required!', + body: 'Permission is required to forward SMS messages.', action: TextButton( onPressed: _requestPerms, - child: const Text('Permission দিন', style: TextStyle(color: kDanger, fontWeight: FontWeight.w600)), + child: const Text('Grant Permission', style: TextStyle(color: kDanger, fontWeight: FontWeight.w600)), ), ), const SizedBox(height: 16), @@ -472,14 +469,14 @@ class _HomeScreenState extends State with WidgetsBindingObserver { begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(14), - border: Border.all(color: _enabled && _hasPerms ? kSuccess.withOpacity(0.4) : kBorder), + border: Border.all(color: _enabled && _hasPerms ? kSuccess.withValues(alpha: 0.4) : kBorder), ), child: Row( children: [ Container( width: 52, height: 52, decoration: BoxDecoration( - color: (_enabled && _hasPerms ? kSuccess : kMuted).withOpacity(0.15), + color: (_enabled && _hasPerms ? kSuccess : kMuted).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(26), ), child: Icon( @@ -502,8 +499,8 @@ class _HomeScreenState extends State with WidgetsBindingObserver { const SizedBox(height: 2), Text( _enabled && _hasPerms - ? 'Notification চলছে • প্রতি ১৫ সেকেন্ডে check' - : _hasPerms ? 'Toggle করে চালু করুন' : 'SMS permission দিন', + ? 'Notification running • Checks every 15 seconds' + : _hasPerms ? 'Toggle to enable' : 'Grant SMS permission', style: const TextStyle(fontSize: 12, color: kMuted), ), ], @@ -512,8 +509,8 @@ class _HomeScreenState extends State with WidgetsBindingObserver { Switch( value: _enabled, onChanged: (_) => _toggleEnabled(), - activeColor: kSuccess, - activeTrackColor: kSuccess.withOpacity(0.3), + activeThumbColor: kSuccess, + activeTrackColor: kSuccess.withValues(alpha: 0.3), ), ], ), @@ -540,15 +537,15 @@ class _HomeScreenState extends State with WidgetsBindingObserver { decoration: BoxDecoration( color: const Color(0xFF0F1E2D), borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFF3B82F6).withOpacity(0.3)), + border: Border.all(color: const Color(0xFF3B82F6).withValues(alpha: 0.3)), ), - child: Row( + child: const Row( children: [ - const Icon(Icons.notifications_active, color: Color(0xFF60A5FA), size: 20), - const SizedBox(width: 10), - const Expanded( + Icon(Icons.notifications_active, color: Color(0xFF60A5FA), size: 20), + SizedBox(width: 10), + Expanded( child: Text( - 'Background Service চলছে। Notification bar এ সবসময় দেখা যাবে। প্রতি ১৫ সেকেন্ডে SMS inbox check করে।', + 'Background service is running. Always visible in the notification bar. Checks SMS inbox every 15 seconds.', style: TextStyle(fontSize: 12, color: Color(0xFF93C5FD), height: 1.5), ), ), @@ -594,7 +591,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { child: OutlinedButton.icon( onPressed: _openSettings, icon: const Icon(Icons.tune, size: 18), - label: const Text('Settings খুলুন'), + label: const Text('Open Settings'), style: OutlinedButton.styleFrom( foregroundColor: kPrimary, side: const BorderSide(color: kPrimary), padding: const EdgeInsets.symmetric(vertical: 14), @@ -641,7 +638,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { Widget _alertCard({required Color color, required Color bgColor, required IconData icon, required String title, required String body, Widget? action}) { return Container( padding: const EdgeInsets.all(14), - decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withOpacity(0.5))), + decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withValues(alpha: 0.5))), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -692,7 +689,7 @@ class _SettingsScreenState extends State { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); if (url.isEmpty || key.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('URL এবং API Key দিন'), backgroundColor: kDanger)); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter URL and API Key'), backgroundColor: kDanger)); return; } final prefs = await SharedPreferences.getInstance(); @@ -705,7 +702,7 @@ class _SettingsScreenState extends State { Future _testConnection() async { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); - if (url.isEmpty || key.isEmpty) { setState(() { _testResult = 'URL এবং API Key দিন।'; _testSuccess = false; }); return; } + if (url.isEmpty || key.isEmpty) { setState(() { _testResult = 'Please enter URL and API Key.'; _testSuccess = false; }); return; } setState(() { _testing = true; _testResult = ''; }); try { final res = await http.post( @@ -715,7 +712,7 @@ class _SettingsScreenState extends State { ).timeout(const Duration(seconds: 15)); setState(() { _testSuccess = res.statusCode >= 200 && res.statusCode < 300; - _testResult = _testSuccess ? '✅ সংযোগ সফল! (HTTP ${res.statusCode})' : '❌ HTTP ${res.statusCode}'; + _testResult = _testSuccess ? '✅ Connection successful! (HTTP ${res.statusCode})' : '❌ HTTP ${res.statusCode}'; }); } catch (e) { setState(() { _testResult = '❌ Error: ${e.toString().substring(0, e.toString().length.clamp(0, 80))}'; _testSuccess = false; }); @@ -728,11 +725,11 @@ class _SettingsScreenState extends State { context: context, builder: (ctx) => AlertDialog( backgroundColor: kCard, - title: const Text('Reset করবেন?', style: TextStyle(color: Colors.white)), - content: const Text('সব settings মুছে setup screen-এ যাবে।', style: TextStyle(color: kMuted)), + title: const Text('Reset App?', style: TextStyle(color: Colors.white)), + content: const Text('All settings will be cleared and you will be taken to the setup screen.', style: TextStyle(color: kMuted)), actions: [ - TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('না')), - TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('হ্যাঁ, Reset', style: TextStyle(color: kDanger))), + TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Yes, Reset', style: TextStyle(color: kDanger))), ], ), ); @@ -765,8 +762,8 @@ class _SettingsScreenState extends State { if (_saved) Container( width: double.infinity, padding: const EdgeInsets.all(12), margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration(color: const Color(0xFF0F2D1A), borderRadius: BorderRadius.circular(8), border: Border.all(color: kSuccess.withOpacity(0.5))), - child: const Text('✅ Settings save হয়েছে!', style: TextStyle(color: kSuccess, fontSize: 13)), + decoration: BoxDecoration(color: const Color(0xFF0F2D1A), borderRadius: BorderRadius.circular(8), border: Border.all(color: kSuccess.withValues(alpha: 0.5))), + child: const Text('✅ Settings saved successfully!', style: TextStyle(color: kSuccess, fontSize: 13)), ), const Text('Worker Configuration', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: Colors.white)), const SizedBox(height: 14), @@ -776,7 +773,7 @@ class _SettingsScreenState extends State { child: Column(children: [ _settingsField('Worker URL', _urlCtrl, 'https://your-worker.workers.dev', Icons.link), const SizedBox(height: 14), - _settingsField('API Key', _keyCtrl, 'আপনার API Key', Icons.key, obscure: true), + _settingsField('API Key', _keyCtrl, 'Your API Key', Icons.key, obscure: true), ]), ), const SizedBox(height: 14), @@ -805,7 +802,7 @@ class _SettingsScreenState extends State { const SizedBox(height: 10), Container( padding: const EdgeInsets.all(12), - decoration: BoxDecoration(color: _testSuccess ? const Color(0xFF0F2D1A) : const Color(0xFF2D1212), borderRadius: BorderRadius.circular(8), border: Border.all(color: (_testSuccess ? kSuccess : kDanger).withOpacity(0.5))), + decoration: BoxDecoration(color: _testSuccess ? const Color(0xFF0F2D1A) : const Color(0xFF2D1212), borderRadius: BorderRadius.circular(8), border: Border.all(color: (_testSuccess ? kSuccess : kDanger).withValues(alpha: 0.5))), child: Text(_testResult, style: TextStyle(fontSize: 12, color: _testSuccess ? kSuccess : const Color(0xFFFCA5A5))), ), ], @@ -817,7 +814,7 @@ class _SettingsScreenState extends State { child: OutlinedButton.icon( onPressed: _resetApp, icon: const Icon(Icons.restore, size: 16), - label: const Text('App Reset করুন'), + label: const Text('Reset App'), style: OutlinedButton.styleFrom(foregroundColor: kDanger, side: const BorderSide(color: kDanger), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), ), ), diff --git a/test/widget_test.dart.bak b/test/widget_test.dart.bak index 636744b..13fb415 100644 --- a/test/widget_test.dart.bak +++ b/test/widget_test.dart.bak @@ -1,30 +1,10 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:paygate/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + testWidgets('SetupScreen renders correctly', (WidgetTester tester) async { + await tester.pumpWidget(const PayGateApp(setupDone: false)); + expect(find.text('PayGate SMS'), findsWidgets); }); } From deeaed37756602f1a1176f87114e85c136996f0c Mon Sep 17 00:00:00 2001 From: Fahim Date: Sun, 24 May 2026 21:09:59 +0600 Subject: [PATCH 2/8] Update build-apk.yml --- .github/workflows/build-apk.yml | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml index 356c712..6664c01 100644 --- a/.github/workflows/build-apk.yml +++ b/.github/workflows/build-apk.yml @@ -1,60 +1,44 @@ name: Flutter APK Build on: - # যখনই main ব্রাঞ্চে কোড পুশ করা হবে - push: - branches: [ main ] - # ইচ্ছে করলে Pull Request এর জন্যও চালানো যায় - pull_request: - branches: [ main ] + workflow_dispatch: jobs: build-android: name: Build Android APK - # Linux রানার ব্যবহার করুন (এটা ফ্রি এবং দ্রুত) [citation:4] runs-on: ubuntu-latest - + steps: - # ১. আপনার রিপোজিটরির কোড ডাউনলোড করা [citation:5] - name: Checkout code uses: actions/checkout@v4 - # ২. Java সেটআপ করা (Flutter Android বিল্ডের জন্য বাধ্যতামূলক) [citation:5][citation:8] - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '17' - # ৩. Flutter এনভায়রনমেন্ট সেটআপ করা (ক্যাশিং চালু করে, যেন পরের বার দ্রুত কাজ করে) [citation:5][citation:7] - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - # এই অপশনটা Flutter আর Pub ডিপেন্ডেন্সি ক্যাশ করে দেয়, বিল্ড স্পিড বাড়ায় - cache: true + cache: true - # ৪. প্রোজেক্টের ডিপেন্ডেন্সি ইনস্টল করা [citation:4][citation:8] - name: Get dependencies run: flutter pub get - # ৫. (অপশনাল) কোড চেক করা - প্রোডাকশনের জন্য ভালো অভ্যাস [citation:1][citation:4] - name: Analyze code run: flutter analyze - # ৬. (অপশনাল) টেস্ট রান করা [citation:1][citation:4] - name: Run tests run: flutter test - # ৭. রিলিজ APK বিল্ড করা [citation:5] - name: Build APK run: flutter build apk --release - # ৮. বিল্ড আউটপুট জমা রাখা (আপনি GitHub থেকেই ডাউনলোড করতে পারবেন) [citation:1][citation:8] - name: Upload APK Artifact uses: actions/upload-artifact@v4 with: name: release-apk path: build/app/outputs/flutter-apk/app-release.apk - # ৩০ দিন পর আউটপুট ডিলিট হয়ে যাবে, যা স্টোরেজ বাঁচায় [citation:1] - retention-days: 30 + retention-days: 3 From fc57cb86b66cde5032b04290460c5fa4f00f8449 Mon Sep 17 00:00:00 2001 From: Fahim Date: Sun, 24 May 2026 21:12:36 +0600 Subject: [PATCH 3/8] Add files via upload --- lib/main.dart | 2 +- lib/main.dart.bak | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c908e56..9037159 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -649,7 +649,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { children: [ Text(title, style: TextStyle(fontWeight: FontWeight.w600, color: color, fontSize: 13)), Text(body, style: const TextStyle(fontSize: 12, color: kMuted)), - if (action != null) action, + ?action, ], )), ], diff --git a/lib/main.dart.bak b/lib/main.dart.bak index c908e56..9037159 100644 --- a/lib/main.dart.bak +++ b/lib/main.dart.bak @@ -649,7 +649,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { children: [ Text(title, style: TextStyle(fontWeight: FontWeight.w600, color: color, fontSize: 13)), Text(body, style: const TextStyle(fontSize: 12, color: kMuted)), - if (action != null) action, + ?action, ], )), ], From cda30c38c8c8ab8eae9762b9f284b0c580284e9e Mon Sep 17 00:00:00 2001 From: Fahim Date: Mon, 25 May 2026 09:33:26 +0600 Subject: [PATCH 4/8] Add files via upload --- .../com/example/paygate/SmsPollingService.kt | 6 +- lib/main.dart | 886 ++++++++---------- 2 files changed, 412 insertions(+), 480 deletions(-) diff --git a/android/app/src/main/kotlin/com/example/paygate/SmsPollingService.kt b/android/app/src/main/kotlin/com/example/paygate/SmsPollingService.kt index b315322..fa5f695 100644 --- a/android/app/src/main/kotlin/com/example/paygate/SmsPollingService.kt +++ b/android/app/src/main/kotlin/com/example/paygate/SmsPollingService.kt @@ -49,7 +49,7 @@ class SmsPollingService : Service() { override fun onCreate() { super.onCreate() createNotificationChannel() - startForeground(NOTIF_ID, buildNotification("SMS Forwarding চলছে...")) + startForeground(NOTIF_ID, buildNotification("SMS Forwarding is running...")) Log.d(TAG, "Service created") } @@ -144,9 +144,9 @@ class SmsPollingService : Service() { } val notifText = if (forwarded > 0) - "✅ $forwarded টি নতুন SMS forward করা হয়েছে" + "✅ $forwarded new SMS(s) forwarded successfully" else - "SMS Forwarding চলছে... (প্রতি ১৫ সেকেন্ডে check)" + "SMS Forwarding is running... (checking every 15 seconds)" updateNotification(notifText) Log.d(TAG, "Poll done — forwarded: $forwarded") diff --git a/lib/main.dart b/lib/main.dart index 9037159..7b3e126 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,21 +9,21 @@ import 'dart:async'; const MethodChannel _smsChannel = MethodChannel('com.example.paygate/sms'); // ─── Constants ──────────────────────────────────────────────────────────────── -const String kWorkerUrl = 'worker_url'; -const String kApiKey = 'api_key'; -const String kEnabled = 'sms_forward_enabled'; +const String kWorkerUrl = 'worker_url'; +const String kApiKey = 'api_key'; +const String kEnabled = 'sms_forward_enabled'; const String kForwardCount = 'forward_count'; -const String kLastLog = 'last_log'; -const String kSetupDone = 'setup_done'; - +const String kLastLog = 'last_log'; +const String kSetupDone = 'setup_done'; + +const Color kBg = Color(0xFF0A0A0A); +const Color kSurface = Color(0xFF141414); +const Color kBorder = Color(0xFF242424); +const Color kMuted = Color(0xFF555555); +const Color kText = Color(0xFFE8E8E8); const Color kPrimary = Color(0xFFE2136E); -const Color kBg = Color(0xFF0F0F0F); -const Color kCard = Color(0xFF1A1A1A); -const Color kBorder = Color(0xFF2A2A2A); -const Color kMuted = Color(0xFF888888); -const Color kSuccess = Color(0xFF22C55E); -const Color kDanger = Color(0xFFEF4444); -const Color kWarning = Color(0xFFF59E0B); +const Color kGreen = Color(0xFF22C55E); +const Color kRed = Color(0xFFEF4444); void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -32,11 +32,9 @@ void main() async { statusBarIconBrightness: Brightness.light, )); final prefs = await SharedPreferences.getInstance(); - final setupDone = prefs.getBool(kSetupDone) ?? false; - runApp(PayGateApp(setupDone: setupDone)); + runApp(PayGateApp(setupDone: prefs.getBool(kSetupDone) ?? false)); } -// ─── App Root ───────────────────────────────────────────────────────────────── class PayGateApp extends StatelessWidget { final bool setupDone; const PayGateApp({super.key, required this.setupDone}); @@ -44,19 +42,97 @@ class PayGateApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'PayGate SMS', + title: 'PayGate', debugShowCheckedModeBanner: false, theme: ThemeData( brightness: Brightness.dark, scaffoldBackgroundColor: kBg, - fontFamily: 'sans-serif', - colorScheme: const ColorScheme.dark(primary: kPrimary, surface: kCard), + colorScheme: const ColorScheme.dark(primary: kPrimary, surface: kSurface), ), home: setupDone ? const HomeScreen() : const SetupScreen(), ); } } +// ─── Shared Widgets ─────────────────────────────────────────────────────────── +class _Field extends StatelessWidget { + final TextEditingController ctrl; + final String label; + final String hint; + final bool obscure; + final TextInputType? keyboard; + + const _Field({ + required this.ctrl, + required this.label, + required this.hint, + this.obscure = false, + this.keyboard, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontSize: 11, color: kMuted, letterSpacing: 0.8)), + const SizedBox(height: 6), + TextField( + controller: ctrl, + obscureText: obscure, + keyboardType: keyboard, + style: const TextStyle(color: kText, fontSize: 14), + decoration: InputDecoration( + hintText: hint, + hintStyle: const TextStyle(color: kMuted, fontSize: 13), + filled: true, + fillColor: kSurface, + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: kBorder), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: kBorder), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: kPrimary), + ), + ), + ), + ], + ); + } +} + +Widget _btn(String label, VoidCallback? onTap, {bool outline = false, bool danger = false}) { + final color = danger ? kRed : kPrimary; + return SizedBox( + width: double.infinity, + height: 46, + child: outline + ? OutlinedButton( + onPressed: onTap, + style: OutlinedButton.styleFrom( + foregroundColor: color, + side: BorderSide(color: color), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text(label, style: const TextStyle(fontSize: 14)), + ) + : ElevatedButton( + onPressed: onTap, + style: ElevatedButton.styleFrom( + backgroundColor: color, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text(label, style: const TextStyle(fontSize: 14, color: Colors.white)), + ), + ); +} + // ─── Setup Screen ───────────────────────────────────────────────────────────── class SetupScreen extends StatefulWidget { const SetupScreen({super.key}); @@ -68,25 +144,21 @@ class _SetupScreenState extends State { final _urlCtrl = TextEditingController(); final _keyCtrl = TextEditingController(); bool _testing = false; - bool _saving = false; - String _testResult = ''; - bool _testSuccess = false; + bool _saving = false; + String _testMsg = ''; + bool _testOk = false; @override - void dispose() { - _urlCtrl.dispose(); - _keyCtrl.dispose(); - super.dispose(); - } + void dispose() { _urlCtrl.dispose(); _keyCtrl.dispose(); super.dispose(); } - Future _testConnection() async { + Future _test() async { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); if (url.isEmpty || key.isEmpty) { - setState(() { _testResult = 'Please enter Worker URL and API Key.'; _testSuccess = false; }); + setState(() { _testMsg = 'Enter URL and API key first.'; _testOk = false; }); return; } - setState(() { _testing = true; _testResult = ''; }); + setState(() { _testing = true; _testMsg = ''; }); try { final res = await http.post( Uri.parse('${url.trimRight()}/api/sms/forward'), @@ -98,30 +170,29 @@ class _SetupScreenState extends State { }), ).timeout(const Duration(seconds: 15)); - if (res.statusCode == 200 || res.statusCode == 201) { - setState(() { _testResult = '✅ Connection successful! Worker is running correctly.'; _testSuccess = true; }); - } else if (res.statusCode == 401) { - setState(() { _testResult = '❌ Invalid API Key.'; _testSuccess = false; }); - } else { - setState(() { _testResult = '⚠️ Server response: ${res.statusCode}'; _testSuccess = false; }); - } + final ok = res.statusCode >= 200 && res.statusCode < 300; + setState(() { + _testOk = ok; + _testMsg = ok ? 'Connection successful.' : 'HTTP ${res.statusCode}'; + }); } on TimeoutException { - setState(() { _testResult = '❌ Timeout. Please check the URL.'; _testSuccess = false; }); + setState(() { _testMsg = 'Request timed out.'; _testOk = false; }); } catch (e) { - setState(() { _testResult = '❌ Error: ${e.toString().substring(0, e.toString().length.clamp(0, 80))}'; _testSuccess = false; }); + setState(() { _testMsg = 'Error: $e'; _testOk = false; }); } setState(() => _testing = false); } - Future _saveAndContinue() async { + Future _save() async { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); - if (url.isEmpty) { _showSnack('Please enter Worker URL'); return; } - if (key.isEmpty) { _showSnack('Please enter API Key'); return; } - if (!url.startsWith('http')) { _showSnack('URL must start with https://'); return; } - + if (url.isEmpty) { _snack('Enter Worker URL'); return; } + if (key.isEmpty) { _snack('Enter API Key'); return; } + if (!url.startsWith('http')) { _snack('URL must start with https://'); return; } setState(() => _saving = true); - await _requestPermissions(); + + try { await _smsChannel.invokeMethod('requestSmsPermission'); } catch (_) {} + await Permission.notification.request(); final prefs = await SharedPreferences.getInstance(); await prefs.setString(kWorkerUrl, url); @@ -132,186 +203,85 @@ class _SetupScreenState extends State { try { await _smsChannel.invokeMethod('startService'); } catch (_) {} if (mounted) { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const HomeScreen()), - ); + Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen())); } } - Future _requestPermissions() async { - try { await _smsChannel.invokeMethod('requestSmsPermission'); } catch (_) {} - await Permission.notification.request(); - } - - void _showSnack(String msg) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(msg), backgroundColor: kDanger), - ); - } + void _snack(String msg) => ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(msg), backgroundColor: kRed)); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: SingleChildScrollView( - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 24), - Row( - children: [ - Container( - width: 48, height: 48, - decoration: BoxDecoration(color: kPrimary, borderRadius: BorderRadius.circular(12)), - child: const Icon(Icons.bolt, color: Colors.white, size: 28), - ), - const SizedBox(width: 14), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('PayGate SMS', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Colors.white)), - Text('bKash SMS Forwarder', style: TextStyle(fontSize: 13, color: kMuted)), - ], - ), - ], - ), - const SizedBox(height: 32), - _infoCard( - icon: Icons.info_outline, color: kPrimary, - title: 'Initial Setup', - body: 'This app will automatically forward your bKash SMS messages to your Cloudflare Worker.', + // Logo + Container( + width: 40, height: 40, + decoration: BoxDecoration(color: kPrimary, borderRadius: BorderRadius.circular(10)), + child: const Icon(Icons.bolt, color: Colors.white, size: 22), ), const SizedBox(height: 20), - _label('Worker URL'), - _textField(controller: _urlCtrl, hint: 'https://your-worker.workers.dev', icon: Icons.link, keyboard: TextInputType.url), + const Text('Setup', style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700, color: kText)), + const SizedBox(height: 6), + const Text('Connect your Cloudflare Worker to start forwarding bKash SMS.', + style: TextStyle(fontSize: 14, color: kMuted, height: 1.5)), + const SizedBox(height: 36), + + _Field(ctrl: _urlCtrl, label: 'WORKER URL', hint: 'https://your-worker.workers.dev', + keyboard: TextInputType.url), const SizedBox(height: 16), - _label('API Key'), - _textField(controller: _keyCtrl, hint: 'Your Worker API Key', icon: Icons.key, obscure: true), - const SizedBox(height: 20), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: _testing ? null : _testConnection, - icon: _testing - ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: kPrimary)) - : const Icon(Icons.wifi_tethering, size: 18), - label: Text(_testing ? 'Testing...' : 'Test Connection'), - style: OutlinedButton.styleFrom( - foregroundColor: kPrimary, side: const BorderSide(color: kPrimary), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - ), - ), - if (_testResult.isNotEmpty) ...[ - const SizedBox(height: 12), + _Field(ctrl: _keyCtrl, label: 'API KEY', hint: 'Paste your API key', obscure: true), + const SizedBox(height: 24), + + // Test result + if (_testMsg.isNotEmpty) ...[ Container( - padding: const EdgeInsets.all(12), + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( - color: _testSuccess ? const Color(0xFF0F2D1A) : const Color(0xFF2D1212), + color: _testOk ? const Color(0xFF0D1F12) : const Color(0xFF1F0D0D), borderRadius: BorderRadius.circular(8), - border: Border.all(color: _testSuccess ? kSuccess : kDanger), + border: Border.all(color: _testOk ? const Color(0xFF1E4D2A) : const Color(0xFF4D1E1E)), ), - child: Text(_testResult, style: TextStyle(fontSize: 13, color: _testSuccess ? kSuccess : const Color(0xFFFCA5A5))), + child: Text(_testMsg, + style: TextStyle(fontSize: 13, color: _testOk ? kGreen : kRed)), ), ], - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _saving ? null : _saveAndContinue, - style: ElevatedButton.styleFrom( - backgroundColor: kPrimary, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - child: _saving - ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) - : const Text('Save & Get Started →', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white)), - ), - ), - const SizedBox(height: 32), - _sectionTitle('How It Works'), - const SizedBox(height: 12), - _stepCard('1', 'Instantly forwards SMS as soon as it arrives from bKash'), - _stepCard('2', 'Checks SMS inbox every 15 seconds in the background'), - _stepCard('3', 'Status is always visible in the notification bar'), - _stepCard('4', 'Automatically restarts after phone reboot'), - ], - ), - ), - ), - ); - } - Widget _label(String text) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text(text, style: const TextStyle(fontSize: 13, color: kMuted, fontWeight: FontWeight.w500)), - ); + _btn(_testing ? 'Testing…' : 'Test Connection', _testing ? null : _test, outline: true), + const SizedBox(height: 10), + _btn(_saving ? 'Saving…' : 'Save & Continue', _saving ? null : _save), - Widget _textField({ - required TextEditingController controller, - required String hint, - required IconData icon, - TextInputType? keyboard, - bool obscure = false, - }) { - return TextField( - controller: controller, - keyboardType: keyboard, - obscureText: obscure, - style: const TextStyle(color: Colors.white, fontSize: 14), - decoration: InputDecoration( - hintText: hint, - hintStyle: const TextStyle(color: kMuted, fontSize: 13), - prefixIcon: Icon(icon, color: kMuted, size: 20), - filled: true, fillColor: const Color(0xFF111111), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: kBorder)), - enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: kBorder)), - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: kPrimary)), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - ), - ); - } + const SizedBox(height: 40), + const Divider(color: kBorder), + const SizedBox(height: 20), - Widget _infoCard({required IconData icon, required Color color, required String title, required String body}) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: kCard, borderRadius: BorderRadius.circular(12), border: Border.all(color: kBorder)), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(icon, color: color, size: 22), - const SizedBox(width: 12), - Expanded(child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14, color: Colors.white)), - const SizedBox(height: 4), - Text(body, style: const TextStyle(fontSize: 13, color: kMuted, height: 1.6)), + const Text('How it works', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: kText)), + const SizedBox(height: 12), + ...[ + 'Forwards bKash SMS instantly when received', + 'Polls SMS inbox every 15 seconds as backup', + 'Runs in background, visible in notification bar', + 'Restarts automatically after phone reboot', + ].map((s) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('— ', style: TextStyle(color: kMuted, fontSize: 13)), + Expanded(child: Text(s, style: const TextStyle(color: kMuted, fontSize: 13, height: 1.4))), + ], + ), + )), ], - )), - ], - ), - ); - } - - Widget _sectionTitle(String text) => Text(text, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white)); - - Widget _stepCard(String num, String text) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - children: [ - Container( - width: 28, height: 28, - decoration: BoxDecoration(color: kPrimary, borderRadius: BorderRadius.circular(14)), - child: Center(child: Text(num, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: Colors.white))), ), - const SizedBox(width: 12), - Expanded(child: Text(text, style: const TextStyle(fontSize: 13, color: kMuted, height: 1.5))), - ], + ), ), ); } @@ -325,70 +295,68 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State with WidgetsBindingObserver { - String _workerUrl = ''; - String _apiKey = ''; - bool _enabled = true; - int _forwardCount = 0; - String _lastLog = ''; - bool _hasPerms = false; - Timer? _refreshTimer; + String _workerUrl = ''; + String _apiKey = ''; + bool _enabled = true; + int _forwardCount = 0; + String _lastLog = ''; + bool _hasPerms = false; + Timer? _timer; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); - _loadPrefs(); - _checkPermissions(); - _refreshTimer = Timer.periodic(const Duration(seconds: 3), (_) => _loadPrefs()); + _load(); + _checkPerms(); + _timer = Timer.periodic(const Duration(seconds: 3), (_) => _load()); _startService(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); - _refreshTimer?.cancel(); + _timer?.cancel(); super.dispose(); } @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { - _startService(); - } + void didChangeAppLifecycleState(AppLifecycleState s) { + if (s == AppLifecycleState.paused || s == AppLifecycleState.detached) _startService(); } Future _startService() async { try { await _smsChannel.invokeMethod('startService'); } catch (_) {} } - Future _loadPrefs() async { - final prefs = await SharedPreferences.getInstance(); + Future _load() async { + final p = await SharedPreferences.getInstance(); if (!mounted) return; setState(() { - _workerUrl = prefs.getString(kWorkerUrl) ?? ''; - _apiKey = prefs.getString(kApiKey) ?? ''; - _enabled = prefs.getBool(kEnabled) ?? true; - _forwardCount = prefs.getInt(kForwardCount) ?? 0; - _lastLog = prefs.getString(kLastLog) ?? 'No activity yet'; + _workerUrl = p.getString(kWorkerUrl) ?? ''; + _apiKey = p.getString(kApiKey) ?? ''; + _enabled = p.getBool(kEnabled) ?? true; + _forwardCount = p.getInt(kForwardCount) ?? 0; + _lastLog = p.getString(kLastLog) ?? '—'; }); } - Future _checkPermissions() async { + Future _checkPerms() async { try { - final granted = await _smsChannel.invokeMethod('hasSmsPermission') ?? false; - if (mounted) setState(() => _hasPerms = granted); + final ok = await _smsChannel.invokeMethod('hasSmsPermission') ?? false; + if (mounted) setState(() => _hasPerms = ok); } catch (_) { - final sms = await Permission.sms.status; - if (mounted) setState(() => _hasPerms = sms.isGranted); + final s = await Permission.sms.status; + if (mounted) setState(() => _hasPerms = s.isGranted); } } Future _toggleEnabled() async { - final prefs = await SharedPreferences.getInstance(); - final newVal = !_enabled; - await prefs.setBool(kEnabled, newVal); - setState(() => _enabled = newVal); - if (newVal) { + final p = await SharedPreferences.getInstance(); + final next = !_enabled; + await p.setBool(kEnabled, next); + setState(() => _enabled = next); + if (next) { _startService(); } else { try { await _smsChannel.invokeMethod('stopService'); } catch (_) {} @@ -401,41 +369,39 @@ class _HomeScreenState extends State with WidgetsBindingObserver { } await Permission.notification.request(); await Future.delayed(const Duration(seconds: 1)); - await _checkPermissions(); - } - - void _openSettings() { - Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())) - .then((_) => _loadPrefs()); + await _checkPerms(); } @override Widget build(BuildContext context) { + final active = _enabled && _hasPerms; + return Scaffold( body: SafeArea( child: Column( children: [ // Top bar - Container( + Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), - decoration: const BoxDecoration( - color: Color(0xFF111111), - border: Border(bottom: BorderSide(color: kBorder)), - ), child: Row( children: [ Container( - width: 36, height: 36, + width: 32, height: 32, decoration: BoxDecoration(color: kPrimary, borderRadius: BorderRadius.circular(8)), - child: const Icon(Icons.bolt, color: Colors.white, size: 20), + child: const Icon(Icons.bolt, color: Colors.white, size: 18), ), const SizedBox(width: 10), - const Text('PayGate SMS', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Colors.white)), + const Text('PayGate', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: kText)), const Spacer(), - IconButton(onPressed: _openSettings, icon: const Icon(Icons.settings, color: kMuted)), + GestureDetector( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())) + .then((_) => _load()), + child: const Icon(Icons.settings_outlined, color: kMuted, size: 22), + ), ], ), ), + const Divider(height: 1, color: kBorder), Expanded( child: SingleChildScrollView( @@ -443,64 +409,55 @@ class _HomeScreenState extends State with WidgetsBindingObserver { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Permission warning if (!_hasPerms) ...[ - _alertCard( - color: kDanger, bgColor: const Color(0xFF2D1212), - icon: Icons.warning_amber, - title: 'SMS Permission Required!', - body: 'Permission is required to forward SMS messages.', - action: TextButton( - onPressed: _requestPerms, - child: const Text('Grant Permission', style: TextStyle(color: kDanger, fontWeight: FontWeight.w600)), + _notice( + 'SMS permission required.', + action: GestureDetector( + onTap: _requestPerms, + child: const Text('Grant →', style: TextStyle(color: kRed, fontSize: 13, fontWeight: FontWeight.w600)), ), + color: kRed, ), const SizedBox(height: 16), ], - // Status card + // Status row Container( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( - gradient: LinearGradient( - colors: _enabled && _hasPerms - ? [const Color(0xFF0F2D1A), const Color(0xFF1A4A2A)] - : [const Color(0xFF1A1A1A), const Color(0xFF222222)], - begin: Alignment.topLeft, end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: _enabled && _hasPerms ? kSuccess.withValues(alpha: 0.4) : kBorder), + color: kSurface, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: kBorder), ), child: Row( children: [ Container( - width: 52, height: 52, + width: 8, height: 8, decoration: BoxDecoration( - color: (_enabled && _hasPerms ? kSuccess : kMuted).withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(26), - ), - child: Icon( - _enabled && _hasPerms ? Icons.sms : Icons.sms_failed, - color: _enabled && _hasPerms ? kSuccess : kMuted, size: 26, + color: active ? kGreen : kMuted, + shape: BoxShape.circle, ), ), - const SizedBox(width: 14), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _enabled && _hasPerms ? 'SMS Forwarding Active' : 'SMS Forwarding Inactive', + active ? 'Forwarding active' : 'Forwarding inactive', style: TextStyle( - fontSize: 15, fontWeight: FontWeight.w700, - color: _enabled && _hasPerms ? kSuccess : kMuted, + fontSize: 14, + fontWeight: FontWeight.w600, + color: active ? kGreen : kMuted, ), ), const SizedBox(height: 2), Text( - _enabled && _hasPerms - ? 'Notification running • Checks every 15 seconds' - : _hasPerms ? 'Toggle to enable' : 'Grant SMS permission', + active + ? 'Running · checks every 15s' + : _hasPerms ? 'Toggle to enable' : 'SMS permission needed', style: const TextStyle(fontSize: 12, color: kMuted), ), ], @@ -509,96 +466,67 @@ class _HomeScreenState extends State with WidgetsBindingObserver { Switch( value: _enabled, onChanged: (_) => _toggleEnabled(), - activeThumbColor: kSuccess, - activeTrackColor: kSuccess.withValues(alpha: 0.3), + activeColor: kGreen, + activeTrackColor: const Color(0xFF0D2A14), + inactiveThumbColor: kMuted, + inactiveTrackColor: kSurface, ), ], ), ), - const SizedBox(height: 16), + const SizedBox(height: 12), - // Stats + // Stats row Row( children: [ - Expanded(child: _statCard('Forwarded', '$_forwardCount', Icons.send, kPrimary)), - const SizedBox(width: 12), - Expanded(child: _statCard('Permission', _hasPerms ? 'Granted' : 'Denied', - _hasPerms ? Icons.check_circle : Icons.cancel, _hasPerms ? kSuccess : kDanger)), - const SizedBox(width: 12), - Expanded(child: _statCard('Service', _enabled ? 'Running' : 'Stopped', - _enabled ? Icons.circle : Icons.pause_circle, _enabled ? kSuccess : kWarning)), + _stat('Forwarded', '$_forwardCount'), + const SizedBox(width: 10), + _stat('Permission', _hasPerms ? 'Granted' : 'Denied'), + const SizedBox(width: 10), + _stat('Service', _enabled ? 'On' : 'Off'), ], ), - const SizedBox(height: 16), - - // Service info card - Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: const Color(0xFF0F1E2D), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFF3B82F6).withValues(alpha: 0.3)), - ), - child: const Row( - children: [ - Icon(Icons.notifications_active, color: Color(0xFF60A5FA), size: 20), - SizedBox(width: 10), - Expanded( - child: Text( - 'Background service is running. Always visible in the notification bar. Checks SMS inbox every 15 seconds.', - style: TextStyle(fontSize: 12, color: Color(0xFF93C5FD), height: 1.5), - ), - ), - ], - ), - ), - const SizedBox(height: 16), + const SizedBox(height: 20), - // Last log - _sectionTitle('Last Activity'), + // Last activity + const Text('Last activity', style: TextStyle(fontSize: 11, color: kMuted, letterSpacing: 0.8)), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: const Color(0xFF111111), - borderRadius: BorderRadius.circular(10), + color: kSurface, + borderRadius: BorderRadius.circular(8), border: Border.all(color: kBorder), ), child: Text(_lastLog, - style: const TextStyle(fontSize: 13, color: kMuted, fontFamily: 'monospace', height: 1.5)), + style: const TextStyle(fontSize: 12, color: kMuted, fontFamily: 'monospace', height: 1.5)), ), - const SizedBox(height: 16), + const SizedBox(height: 20), // Config - _sectionTitle('Current Config'), + const Text('Configuration', style: TextStyle(fontSize: 11, color: kMuted, letterSpacing: 0.8)), const SizedBox(height: 8), Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: kCard, borderRadius: BorderRadius.circular(12), border: Border.all(color: kBorder)), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: kSurface, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: kBorder), + ), child: Column( children: [ - _configRow('Worker URL', _workerUrl.isEmpty ? 'Not set' : _workerUrl), - const Divider(color: kBorder, height: 20), - _configRow('API Key', _apiKey.isEmpty ? 'Not set' : '${_apiKey.substring(0, _apiKey.length.clamp(0, 6))}••••••'), + _cfgRow('URL', _workerUrl.isEmpty ? 'Not set' : _workerUrl), + const SizedBox(height: 10), + const Divider(color: kBorder, height: 1), + const SizedBox(height: 10), + _cfgRow('Key', + _apiKey.isEmpty + ? 'Not set' + : '${_apiKey.substring(0, _apiKey.length.clamp(0, 6))}••••••'), ], ), ), - const SizedBox(height: 24), - - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: _openSettings, - icon: const Icon(Icons.tune, size: 18), - label: const Text('Open Settings'), - style: OutlinedButton.styleFrom( - foregroundColor: kPrimary, side: const BorderSide(color: kPrimary), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - ), - ), ], ), ), @@ -609,49 +537,50 @@ class _HomeScreenState extends State with WidgetsBindingObserver { ); } - Widget _sectionTitle(String t) => Text(t, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: Colors.white)); - - Widget _statCard(String label, String value, IconData icon, Color color) { - return Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration(color: kCard, borderRadius: BorderRadius.circular(10), border: Border.all(color: kBorder)), - child: Column(children: [ - Icon(icon, color: color, size: 22), - const SizedBox(height: 6), - Text(value, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: color)), - const SizedBox(height: 2), - Text(label, style: const TextStyle(fontSize: 11, color: kMuted)), - ]), + Widget _stat(String label, String val) { + return Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration(color: kSurface, borderRadius: BorderRadius.circular(8), border: Border.all(color: kBorder)), + child: Column( + children: [ + Text(val, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: kText)), + const SizedBox(height: 2), + Text(label, style: const TextStyle(fontSize: 11, color: kMuted)), + ], + ), + ), ); } - Widget _configRow(String label, String value) { + Widget _cfgRow(String label, String val) { return Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(width: 90, child: Text(label, style: const TextStyle(fontSize: 13, color: kMuted))), - Expanded(child: Text(value, style: const TextStyle(fontSize: 13, color: Colors.white), overflow: TextOverflow.ellipsis, maxLines: 2)), + SizedBox(width: 36, child: Text(label, style: const TextStyle(fontSize: 12, color: kMuted))), + const SizedBox(width: 10), + Expanded( + child: Text(val, + style: const TextStyle(fontSize: 12, color: kText), + overflow: TextOverflow.ellipsis, maxLines: 1), + ), ], ); } - Widget _alertCard({required Color color, required Color bgColor, required IconData icon, required String title, required String body, Widget? action}) { + Widget _notice(String msg, {Widget? action, required Color color}) { return Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withValues(alpha: 0.5))), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: kSurface, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withValues(alpha: 0.4)), + ), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(icon, color: color, size: 20), + Icon(Icons.info_outline, color: color, size: 16), const SizedBox(width: 10), - Expanded(child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: TextStyle(fontWeight: FontWeight.w600, color: color, fontSize: 13)), - Text(body, style: const TextStyle(fontSize: 12, color: kMuted)), - ?action, - ], - )), + Expanded(child: Text(msg, style: TextStyle(fontSize: 13, color: color))), + if (action != null) action, ], ), ); @@ -668,79 +597,86 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { final _urlCtrl = TextEditingController(); final _keyCtrl = TextEditingController(); - bool _saved = false; - bool _testing = false; - String _testResult = ''; - bool _testSuccess = false; + bool _saved = false; + bool _testing = false; + String _testMsg = ''; + bool _testOk = false; @override - void initState() { super.initState(); _loadPrefs(); } + void initState() { super.initState(); _load(); } @override void dispose() { _urlCtrl.dispose(); _keyCtrl.dispose(); super.dispose(); } - Future _loadPrefs() async { - final prefs = await SharedPreferences.getInstance(); - _urlCtrl.text = prefs.getString(kWorkerUrl) ?? ''; - _keyCtrl.text = prefs.getString(kApiKey) ?? ''; + Future _load() async { + final p = await SharedPreferences.getInstance(); + _urlCtrl.text = p.getString(kWorkerUrl) ?? ''; + _keyCtrl.text = p.getString(kApiKey) ?? ''; } Future _save() async { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); if (url.isEmpty || key.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter URL and API Key'), backgroundColor: kDanger)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Enter URL and API Key'), backgroundColor: kRed)); return; } - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(kWorkerUrl, url); - await prefs.setString(kApiKey, key); + final p = await SharedPreferences.getInstance(); + await p.setString(kWorkerUrl, url); + await p.setString(kApiKey, key); setState(() => _saved = true); Future.delayed(const Duration(seconds: 2), () { if (mounted) setState(() => _saved = false); }); } - Future _testConnection() async { + Future _test() async { final url = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); - if (url.isEmpty || key.isEmpty) { setState(() { _testResult = 'Please enter URL and API Key.'; _testSuccess = false; }); return; } - setState(() { _testing = true; _testResult = ''; }); + if (url.isEmpty || key.isEmpty) { + setState(() { _testMsg = 'Enter URL and API key first.'; _testOk = false; }); + return; + } + setState(() { _testing = true; _testMsg = ''; }); try { final res = await http.post( Uri.parse('${url.trimRight()}/api/sms/forward'), headers: {'Content-Type': 'application/json', 'X-API-Key': key}, - body: jsonEncode({'sender': '01700000000', 'message': 'You have received Tk 10.00 from 01700000000. TrxID TESTCONN01. Balance Tk 100.00.', 'receivedAt': DateTime.now().toUtc().toIso8601String()}), + body: jsonEncode({ + 'sender': '01700000000', + 'message': 'You have received Tk 10.00 from 01700000000. TrxID TESTCONN01. Balance Tk 100.00.', + 'receivedAt': DateTime.now().toUtc().toIso8601String(), + }), ).timeout(const Duration(seconds: 15)); - setState(() { - _testSuccess = res.statusCode >= 200 && res.statusCode < 300; - _testResult = _testSuccess ? '✅ Connection successful! (HTTP ${res.statusCode})' : '❌ HTTP ${res.statusCode}'; - }); + final ok = res.statusCode >= 200 && res.statusCode < 300; + setState(() { _testOk = ok; _testMsg = ok ? 'Connection successful.' : 'HTTP ${res.statusCode}'; }); } catch (e) { - setState(() { _testResult = '❌ Error: ${e.toString().substring(0, e.toString().length.clamp(0, 80))}'; _testSuccess = false; }); + setState(() { _testMsg = 'Error: $e'; _testOk = false; }); } setState(() => _testing = false); } - Future _resetApp() async { - final confirm = await showDialog( + Future _reset() async { + final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( - backgroundColor: kCard, - title: const Text('Reset App?', style: TextStyle(color: Colors.white)), - content: const Text('All settings will be cleared and you will be taken to the setup screen.', style: TextStyle(color: kMuted)), + backgroundColor: kSurface, + title: const Text('Reset app?', style: TextStyle(color: kText, fontSize: 16)), + content: const Text('All settings will be cleared.', style: TextStyle(color: kMuted, fontSize: 14)), actions: [ - TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), - TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Yes, Reset', style: TextStyle(color: kDanger))), + TextButton(onPressed: () => Navigator.pop(ctx, false), + child: const Text('Cancel', style: TextStyle(color: kMuted))), + TextButton(onPressed: () => Navigator.pop(ctx, true), + child: const Text('Reset', style: TextStyle(color: kRed))), ], ), ); - if (confirm == true) { + if (ok == true) { try { await _smsChannel.invokeMethod('stopService'); } catch (_) {} - final prefs = await SharedPreferences.getInstance(); - await prefs.clear(); + final p = await SharedPreferences.getInstance(); + await p.clear(); if (mounted) { Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute(builder: (_) => const SetupScreen()), (_) => false, - ); + MaterialPageRoute(builder: (_) => const SetupScreen()), (_) => false); } } } @@ -749,9 +685,13 @@ class _SettingsScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - backgroundColor: const Color(0xFF111111), - title: const Text('Settings', style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.w600)), - leading: IconButton(icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context)), + backgroundColor: kBg, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: kText, size: 20), + onPressed: () => Navigator.pop(context), + ), + title: const Text('Settings', style: TextStyle(color: kText, fontSize: 15, fontWeight: FontWeight.w600)), bottom: const PreferredSize(preferredSize: Size.fromHeight(1), child: Divider(height: 1, color: kBorder)), ), body: SingleChildScrollView( @@ -759,92 +699,84 @@ class _SettingsScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (_saved) + + if (_saved) ...[ Container( - width: double.infinity, padding: const EdgeInsets.all(12), margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration(color: const Color(0xFF0F2D1A), borderRadius: BorderRadius.circular(8), border: Border.all(color: kSuccess.withValues(alpha: 0.5))), - child: const Text('✅ Settings saved successfully!', style: TextStyle(color: kSuccess, fontSize: 13)), + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: const Color(0xFF0D1F12), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFF1E4D2A)), + ), + child: const Text('Settings saved.', style: TextStyle(color: kGreen, fontSize: 13)), ), - const Text('Worker Configuration', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: Colors.white)), - const SizedBox(height: 14), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: kCard, borderRadius: BorderRadius.circular(12), border: Border.all(color: kBorder)), - child: Column(children: [ - _settingsField('Worker URL', _urlCtrl, 'https://your-worker.workers.dev', Icons.link), - const SizedBox(height: 14), - _settingsField('API Key', _keyCtrl, 'Your API Key', Icons.key, obscure: true), - ]), - ), + ], + + _Field(ctrl: _urlCtrl, label: 'WORKER URL', hint: 'https://your-worker.workers.dev', + keyboard: TextInputType.url), const SizedBox(height: 14), + _Field(ctrl: _keyCtrl, label: 'API KEY', hint: 'Paste your API key', obscure: true), + const SizedBox(height: 20), + + if (_testMsg.isNotEmpty) ...[ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + margin: const EdgeInsets.only(bottom: 14), + decoration: BoxDecoration( + color: _testOk ? const Color(0xFF0D1F12) : const Color(0xFF1F0D0D), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: _testOk ? const Color(0xFF1E4D2A) : const Color(0xFF4D1E1E)), + ), + child: Text(_testMsg, style: TextStyle(fontSize: 13, color: _testOk ? kGreen : kRed)), + ), + ], + Row( children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: _testing ? null : _testConnection, - icon: _testing ? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: kPrimary)) : const Icon(Icons.wifi_tethering, size: 16), - label: const Text('Test'), - style: OutlinedButton.styleFrom(foregroundColor: kPrimary, side: const BorderSide(color: kPrimary), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + Expanded(child: SizedBox( + height: 46, + child: OutlinedButton( + onPressed: _testing ? null : _test, + style: OutlinedButton.styleFrom( + foregroundColor: kPrimary, + side: const BorderSide(color: kBorder), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text(_testing ? 'Testing…' : 'Test', style: const TextStyle(fontSize: 14)), ), - ), + )), const SizedBox(width: 10), - Expanded( - child: ElevatedButton.icon( + Expanded(child: SizedBox( + height: 46, + child: ElevatedButton( onPressed: _save, - icon: const Icon(Icons.save, size: 16, color: Colors.white), - label: const Text('Save', style: TextStyle(color: Colors.white)), - style: ElevatedButton.styleFrom(backgroundColor: kPrimary, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + style: ElevatedButton.styleFrom( + backgroundColor: kPrimary, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: const Text('Save', style: TextStyle(fontSize: 14, color: Colors.white)), ), - ), + )), ], ), - if (_testResult.isNotEmpty) ...[ - const SizedBox(height: 10), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration(color: _testSuccess ? const Color(0xFF0F2D1A) : const Color(0xFF2D1212), borderRadius: BorderRadius.circular(8), border: Border.all(color: (_testSuccess ? kSuccess : kDanger).withValues(alpha: 0.5))), - child: Text(_testResult, style: TextStyle(fontSize: 12, color: _testSuccess ? kSuccess : const Color(0xFFFCA5A5))), - ), - ], - const SizedBox(height: 24), + + const SizedBox(height: 32), const Divider(color: kBorder), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: _resetApp, - icon: const Icon(Icons.restore, size: 16), - label: const Text('Reset App'), - style: OutlinedButton.styleFrom(foregroundColor: kDanger, side: const BorderSide(color: kDanger), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), - ), - ), const SizedBox(height: 20), - const Center(child: Text('PayGate SMS Forwarder v1.0.0', style: TextStyle(fontSize: 11, color: kMuted))), + + _btn('Reset App', _reset, outline: true, danger: true), + + const SizedBox(height: 24), + const Center( + child: Text('PayGate SMS Forwarder v1.0.0', + style: TextStyle(fontSize: 11, color: kMuted)), + ), ], ), ), ); } - - Widget _settingsField(String label, TextEditingController ctrl, String hint, IconData icon, {bool obscure = false}) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: const TextStyle(fontSize: 12, color: kMuted, fontWeight: FontWeight.w500)), - const SizedBox(height: 6), - TextField( - controller: ctrl, obscureText: obscure, - style: const TextStyle(color: Colors.white, fontSize: 13), - decoration: InputDecoration( - hintText: hint, hintStyle: const TextStyle(color: kMuted, fontSize: 12), - prefixIcon: Icon(icon, color: kMuted, size: 18), filled: true, fillColor: const Color(0xFF111111), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: kBorder)), - enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: kBorder)), - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: kPrimary)), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - ), - ), - ], - ); - } } From a1964f666fbd5ae33a5c8e24ff9de4a1c2a55ca1 Mon Sep 17 00:00:00 2001 From: Fahim Date: Mon, 25 May 2026 09:47:25 +0600 Subject: [PATCH 5/8] Add files via upload --- lib/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 7b3e126..8300413 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -466,7 +466,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { Switch( value: _enabled, onChanged: (_) => _toggleEnabled(), - activeColor: kGreen, + activeThumbColor: kGreen, activeTrackColor: const Color(0xFF0D2A14), inactiveThumbColor: kMuted, inactiveTrackColor: kSurface, @@ -580,7 +580,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { Icon(Icons.info_outline, color: color, size: 16), const SizedBox(width: 10), Expanded(child: Text(msg, style: TextStyle(fontSize: 13, color: color))), - if (action != null) action, + ?action, ], ), ); From adffe03cc1eb3cd51b488b6e6f7b362b899faf1c Mon Sep 17 00:00:00 2001 From: Fahim Date: Mon, 25 May 2026 09:50:20 +0600 Subject: [PATCH 6/8] Add files via upload --- test/widget_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/widget_test.dart b/test/widget_test.dart index 13fb415..7ee0706 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,6 +5,6 @@ import 'package:paygate/main.dart'; void main() { testWidgets('SetupScreen renders correctly', (WidgetTester tester) async { await tester.pumpWidget(const PayGateApp(setupDone: false)); - expect(find.text('PayGate SMS'), findsWidgets); + expect(find.text('Setup'), findsOneWidget); }); } From be8a6bc528ff6b02aa5438903fa5db1c7133d4fa Mon Sep 17 00:00:00 2001 From: Fahim Date: Mon, 25 May 2026 10:07:10 +0600 Subject: [PATCH 7/8] Delete android/app/src/main/kotlin/com/example/paygate directory --- .../com/example/paygate/BootReceiver.kt | 17 -- .../com/example/paygate/MainActivity.kt | 51 ---- .../com/example/paygate/SmsPollingService.kt | 249 ------------------ .../kotlin/com/example/paygate/SmsReceiver.kt | 144 ---------- 4 files changed, 461 deletions(-) delete mode 100644 android/app/src/main/kotlin/com/example/paygate/BootReceiver.kt delete mode 100644 android/app/src/main/kotlin/com/example/paygate/MainActivity.kt delete mode 100644 android/app/src/main/kotlin/com/example/paygate/SmsPollingService.kt delete mode 100644 android/app/src/main/kotlin/com/example/paygate/SmsReceiver.kt diff --git a/android/app/src/main/kotlin/com/example/paygate/BootReceiver.kt b/android/app/src/main/kotlin/com/example/paygate/BootReceiver.kt deleted file mode 100644 index f76643a..0000000 --- a/android/app/src/main/kotlin/com/example/paygate/BootReceiver.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.paygate - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.util.Log - -class BootReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action - if (action == Intent.ACTION_BOOT_COMPLETED || action == Intent.ACTION_MY_PACKAGE_REPLACED) { - Log.d("PayGateBoot", "Boot/update completed — starting polling service") - // Phone restart বা app update এর পর Foreground Service চালু করো - SmsPollingService.start(context) - } - } -} diff --git a/android/app/src/main/kotlin/com/example/paygate/MainActivity.kt b/android/app/src/main/kotlin/com/example/paygate/MainActivity.kt deleted file mode 100644 index 57e2a74..0000000 --- a/android/app/src/main/kotlin/com/example/paygate/MainActivity.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.paygate - -import android.Manifest -import android.content.pm.PackageManager -import android.os.Build -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodChannel -import androidx.core.app.ActivityCompat - -class MainActivity : FlutterActivity() { - private val CHANNEL = "com.example.paygate/sms" - - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> - when (call.method) { - "hasSmsPermission" -> { - val granted = ActivityCompat.checkSelfPermission(this, Manifest.permission.RECEIVE_SMS) == PackageManager.PERMISSION_GRANTED - result.success(granted) - } - "requestSmsPermission" -> { - val perms = mutableListOf( - Manifest.permission.RECEIVE_SMS, - Manifest.permission.READ_SMS - ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - perms.add(Manifest.permission.POST_NOTIFICATIONS) - } - ActivityCompat.requestPermissions(this, perms.toTypedArray(), 100) - result.success(null) - } - "startService" -> { - SmsPollingService.start(this) - result.success(true) - } - "stopService" -> { - SmsPollingService.stop(this) - result.success(true) - } - else -> result.notImplemented() - } - } - } - - override fun onResume() { - super.onResume() - // App foreground এ আসলে service চালু করো (যদি না থাকে) - SmsPollingService.start(this) - } -} diff --git a/android/app/src/main/kotlin/com/example/paygate/SmsPollingService.kt b/android/app/src/main/kotlin/com/example/paygate/SmsPollingService.kt deleted file mode 100644 index fa5f695..0000000 --- a/android/app/src/main/kotlin/com/example/paygate/SmsPollingService.kt +++ /dev/null @@ -1,249 +0,0 @@ -package com.example.paygate - -import android.app.* -import android.content.Context -import android.content.Intent -import android.database.Cursor -import android.net.Uri -import android.os.IBinder -import android.util.Log -import androidx.core.app.NotificationCompat -import kotlinx.coroutines.* -import java.io.OutputStreamWriter -import java.net.HttpURLConnection -import java.net.URL -import java.text.SimpleDateFormat -import java.util.* - -class SmsPollingService : Service() { - - companion object { - const val TAG = "PayGatePoller" - const val CHANNEL_ID = "paygate_foreground" - const val NOTIF_ID = 1001 - // Flutter shared_preferences uses FlutterSharedPreferences file with "flutter." prefix - const val PREFS_NAME = "FlutterSharedPreferences" - const val KEY_WORKER_URL = "flutter.worker_url" - const val KEY_API_KEY = "flutter.api_key" - const val KEY_ENABLED = "flutter.sms_forward_enabled" - const val KEY_FORWARD_COUNT = "flutter.forward_count" - const val KEY_LAST_LOG = "flutter.last_log" - // Internal tracking key — no flutter. prefix (not a Flutter pref) - const val KEY_LAST_SMS_DATE = "last_sms_date_polled" - const val PREFS_INTERNAL = "PayGateInternal" - const val POLL_INTERVAL_MS = 15_000L - - fun start(context: Context) { - val intent = Intent(context, SmsPollingService::class.java) - context.startForegroundService(intent) - } - - fun stop(context: Context) { - context.stopService(Intent(context, SmsPollingService::class.java)) - } - } - - private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private var pollingJob: Job? = null - - override fun onCreate() { - super.onCreate() - createNotificationChannel() - startForeground(NOTIF_ID, buildNotification("SMS Forwarding is running...")) - Log.d(TAG, "Service created") - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - startPolling() - return START_STICKY - } - - override fun onBind(intent: Intent?): IBinder? = null - - override fun onDestroy() { - pollingJob?.cancel() - serviceScope.cancel() - Log.d(TAG, "Service destroyed") - super.onDestroy() - } - - private fun startPolling() { - pollingJob?.cancel() - pollingJob = serviceScope.launch { - while (isActive) { - try { - pollSmsInbox() - } catch (e: Exception) { - Log.e(TAG, "Poll error: ${e.message}") - } - delay(POLL_INTERVAL_MS) - } - } - Log.d(TAG, "Polling started (interval: ${POLL_INTERVAL_MS}ms)") - } - - private suspend fun pollSmsInbox() { - val flutterPrefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - val internalPrefs = getSharedPreferences(PREFS_INTERNAL, Context.MODE_PRIVATE) - - val enabled = flutterPrefs.getBoolean(KEY_ENABLED, true) - if (!enabled) return - - val workerUrl = flutterPrefs.getString(KEY_WORKER_URL, "") ?: "" - val apiKey = flutterPrefs.getString(KEY_API_KEY, "") ?: "" - if (workerUrl.isEmpty() || apiKey.isEmpty()) return - - // Use internal prefs for polling state (not Flutter prefs) - val lastCheckedDate = internalPrefs.getLong(KEY_LAST_SMS_DATE, System.currentTimeMillis() - 60_000) - - val uri = Uri.parse("content://sms/inbox") - val cursor: Cursor? = contentResolver.query( - uri, - arrayOf("_id", "address", "body", "date"), - "date > ?", - arrayOf(lastCheckedDate.toString()), - "date ASC" // ASC so we process oldest first - ) - - var forwarded = 0 - var latestDate = lastCheckedDate - - cursor?.use { - while (it.moveToNext()) { - val body = it.getString(it.getColumnIndexOrThrow("body")) ?: continue - val sender = it.getString(it.getColumnIndexOrThrow("address")) ?: "Unknown" - val date = it.getLong(it.getColumnIndexOrThrow("date")) - - // Track latest date regardless of whether it's bKash - if (date > latestDate) latestDate = date - - if (!isBkashSms(sender, body)) continue - - Log.d(TAG, "Poller found bKash SMS from: $sender") - - val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) - sdf.timeZone = TimeZone.getTimeZone("UTC") - val receivedAt = sdf.format(Date(date)) - - val result = forwardSms(workerUrl, apiKey, sender, body, receivedAt) - if (result) { - forwarded++ - val count = flutterPrefs.getInt(KEY_FORWARD_COUNT, 0) - flutterPrefs.edit() - .putInt(KEY_FORWARD_COUNT, count + 1) - .putString(KEY_LAST_LOG, - "[${SimpleDateFormat("HH:mm:ss", Locale.US).format(Date())}] ✅ Polled & Forwarded: ${body.take(50)}...") - .apply() - } - } - } - - // Always advance the pointer to avoid re-processing same SMS - if (latestDate > lastCheckedDate) { - internalPrefs.edit().putLong(KEY_LAST_SMS_DATE, latestDate + 1).apply() - } - - val notifText = if (forwarded > 0) - "✅ $forwarded new SMS(s) forwarded successfully" - else - "SMS Forwarding is running... (checking every 15 seconds)" - - updateNotification(notifText) - Log.d(TAG, "Poll done — forwarded: $forwarded") - } - - private fun isBkashSms(sender: String, body: String): Boolean { - val lowerBody = body.lowercase() - val lowerSender = sender.lowercase() - - val isBkashSender = lowerSender.contains("bkash") || - lowerSender == "01769-420420" || - lowerSender == "01769420420" || - lowerSender == "16247" - - val hasTrxId = lowerBody.contains("trxid") || lowerBody.contains("trx id") - val hasBkashKeyword = lowerBody.contains("bkash") || - lowerBody.contains("received tk") || - lowerBody.contains("sent tk") || - lowerBody.contains("payment") || - lowerBody.contains("cash out") - - return isBkashSender || (hasTrxId && hasBkashKeyword) - } - - private fun forwardSms( - baseUrl: String, - apiKey: String, - sender: String, - message: String, - receivedAt: String - ): Boolean { - return try { - val url = baseUrl.trimEnd('/') + "/api/sms/forward" - val jsonBody = """{"sender":"$sender","message":${escapeJson(message)},"receivedAt":"$receivedAt"}""" - - val conn = URL(url).openConnection() as HttpURLConnection - conn.requestMethod = "POST" - conn.setRequestProperty("Content-Type", "application/json") - conn.setRequestProperty("X-API-Key", apiKey) - conn.doOutput = true - conn.connectTimeout = 10000 - conn.readTimeout = 10000 - - OutputStreamWriter(conn.outputStream).use { w -> - w.write(jsonBody) - w.flush() - } - - val code = conn.responseCode - Log.d(TAG, "Forward HTTP: $code") - code in 200..299 - } catch (e: Exception) { - Log.e(TAG, "Forward error: ${e.message}") - false - } - } - - private fun escapeJson(text: String): String { - return "\"" + text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t") + "\"" - } - - private fun createNotificationChannel() { - val channel = NotificationChannel( - CHANNEL_ID, - "PayGate SMS Forwarder", - NotificationManager.IMPORTANCE_LOW - ).apply { - description = "bKash SMS forwarding service" - setShowBadge(false) - } - val nm = getSystemService(NotificationManager::class.java) - nm.createNotificationChannel(channel) - } - - private fun buildNotification(text: String): Notification { - val openIntent = PendingIntent.getActivity( - this, 0, - Intent(this, MainActivity::class.java), - PendingIntent.FLAG_IMMUTABLE - ) - return NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("PayGate SMS") - .setContentText(text) - .setSmallIcon(android.R.drawable.ic_dialog_email) - .setContentIntent(openIntent) - .setOngoing(true) - .setSilent(true) - .build() - } - - private fun updateNotification(text: String) { - val nm = getSystemService(NotificationManager::class.java) - nm.notify(NOTIF_ID, buildNotification(text)) - } -} diff --git a/android/app/src/main/kotlin/com/example/paygate/SmsReceiver.kt b/android/app/src/main/kotlin/com/example/paygate/SmsReceiver.kt deleted file mode 100644 index f9d7c32..0000000 --- a/android/app/src/main/kotlin/com/example/paygate/SmsReceiver.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.example.paygate - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.provider.Telephony -import android.util.Log -import kotlinx.coroutines.* -import java.io.OutputStreamWriter -import java.net.HttpURLConnection -import java.net.URL -import java.text.SimpleDateFormat -import java.util.* - -class SmsReceiver : BroadcastReceiver() { - - companion object { - const val TAG = "PayGateSMS" - // Flutter shared_preferences uses FlutterSharedPreferences file with "flutter." prefix - const val PREFS_NAME = "FlutterSharedPreferences" - const val KEY_WORKER_URL = "flutter.worker_url" - const val KEY_API_KEY = "flutter.api_key" - const val KEY_ENABLED = "flutter.sms_forward_enabled" - const val KEY_FORWARD_COUNT = "flutter.forward_count" - const val KEY_LAST_LOG = "flutter.last_log" - } - - override fun onReceive(context: Context, intent: Intent) { - if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION) return - - val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - val enabled = prefs.getBoolean(KEY_ENABLED, true) - if (!enabled) return - - val workerUrl = prefs.getString(KEY_WORKER_URL, "") ?: "" - val apiKey = prefs.getString(KEY_API_KEY, "") ?: "" - - if (workerUrl.isEmpty() || apiKey.isEmpty()) { - Log.w(TAG, "Worker URL or API key not configured") - return - } - - val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) - val fullBody = messages.joinToString("") { it.messageBody } - val sender = messages.firstOrNull()?.originatingAddress ?: "Unknown" - - Log.d(TAG, "SMS received from: $sender") - Log.d(TAG, "Body: $fullBody") - - if (!isBkashSms(sender, fullBody)) { - Log.d(TAG, "Not a bKash SMS, skipping") - return - } - - Log.d(TAG, "bKash SMS detected, forwarding...") - - val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) - sdf.timeZone = TimeZone.getTimeZone("UTC") - val receivedAt = sdf.format(Date()) - - val pendingResult = goAsync() - - CoroutineScope(Dispatchers.IO).launch { - try { - val result = forwardSms(workerUrl, apiKey, sender, fullBody, receivedAt) - val logMsg = if (result) "✅ Forwarded: ${fullBody.take(50)}..." - else "❌ Forward failed: ${fullBody.take(50)}..." - - val count = prefs.getInt(KEY_FORWARD_COUNT, 0) - prefs.edit() - .putInt(KEY_FORWARD_COUNT, if (result) count + 1 else count) - .putString(KEY_LAST_LOG, "[${SimpleDateFormat("HH:mm:ss", Locale.US).format(Date())}] $logMsg") - .apply() - - Log.d(TAG, logMsg) - } catch (e: Exception) { - Log.e(TAG, "Error forwarding SMS: ${e.message}") - prefs.edit() - .putString(KEY_LAST_LOG, "[${SimpleDateFormat("HH:mm:ss", Locale.US).format(Date())}] ❌ Error: ${e.message}") - .apply() - } finally { - pendingResult.finish() - } - } - } - - private fun isBkashSms(sender: String, body: String): Boolean { - val lowerBody = body.lowercase() - val lowerSender = sender.lowercase() - - // bKash official sender numbers/names - val isBkashSender = lowerSender.contains("bkash") || - lowerSender == "01769-420420" || - lowerSender == "01769420420" || - lowerSender == "16247" - - // bKash SMS keywords — TrxID (not TrkID!) - val hasTrxId = lowerBody.contains("trxid") || lowerBody.contains("trx id") - val hasBkashKeyword = lowerBody.contains("bkash") || - lowerBody.contains("received tk") || - lowerBody.contains("sent tk") || - lowerBody.contains("payment") || - lowerBody.contains("cash out") - - return isBkashSender || (hasTrxId && hasBkashKeyword) - } - - private fun forwardSms( - baseUrl: String, - apiKey: String, - sender: String, - message: String, - receivedAt: String - ): Boolean { - val url = baseUrl.trimEnd('/') + "/api/sms/forward" - val jsonBody = """{"sender":"$sender","message":${escapeJson(message)},"receivedAt":"$receivedAt"}""" - - val conn = URL(url).openConnection() as HttpURLConnection - conn.requestMethod = "POST" - conn.setRequestProperty("Content-Type", "application/json") - conn.setRequestProperty("X-API-Key", apiKey) - conn.doOutput = true - conn.connectTimeout = 15000 - conn.readTimeout = 15000 - - OutputStreamWriter(conn.outputStream).use { w -> - w.write(jsonBody) - w.flush() - } - - val responseCode = conn.responseCode - Log.d(TAG, "HTTP Response: $responseCode") - return responseCode in 200..299 - } - - private fun escapeJson(text: String): String { - return "\"" + text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t") + "\"" - } -} From 9fc436597598206557008e16552e9d6444d5b3f8 Mon Sep 17 00:00:00 2001 From: Fahim Date: Mon, 25 May 2026 10:12:40 +0600 Subject: [PATCH 8/8] Add files via upload --- android/app/build.gradle.kts | 4 +- android/app/src/main/AndroidManifest.xml | 3 +- .../main/kotlin/com/paygate/f/BootReceiver.kt | 17 ++ .../main/kotlin/com/paygate/f/MainActivity.kt | 51 ++++ .../kotlin/com/paygate/f/SmsPollingService.kt | 249 ++++++++++++++++++ .../main/kotlin/com/paygate/f/SmsReceiver.kt | 144 ++++++++++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 4739 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4739 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 2898 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2898 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 6803 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6803 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 11206 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11206 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 15991 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15991 bytes lib/main.dart | 4 +- pubspec.yaml | 2 +- 18 files changed, 468 insertions(+), 6 deletions(-) create mode 100644 android/app/src/main/kotlin/com/paygate/f/BootReceiver.kt create mode 100644 android/app/src/main/kotlin/com/paygate/f/MainActivity.kt create mode 100644 android/app/src/main/kotlin/com/paygate/f/SmsPollingService.kt create mode 100644 android/app/src/main/kotlin/com/paygate/f/SmsReceiver.kt create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 113f6c0..f1c7d6c 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } android { - namespace = "com.example.paygate" + namespace = "com.paygate.f" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -16,7 +16,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.paygate" + applicationId = "com.paygate.f" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6f8268b..0a69745 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,9 +12,10 @@ + when (call.method) { + "hasSmsPermission" -> { + val granted = ActivityCompat.checkSelfPermission(this, Manifest.permission.RECEIVE_SMS) == PackageManager.PERMISSION_GRANTED + result.success(granted) + } + "requestSmsPermission" -> { + val perms = mutableListOf( + Manifest.permission.RECEIVE_SMS, + Manifest.permission.READ_SMS + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + perms.add(Manifest.permission.POST_NOTIFICATIONS) + } + ActivityCompat.requestPermissions(this, perms.toTypedArray(), 100) + result.success(null) + } + "startService" -> { + SmsPollingService.start(this) + result.success(true) + } + "stopService" -> { + SmsPollingService.stop(this) + result.success(true) + } + else -> result.notImplemented() + } + } + } + + override fun onResume() { + super.onResume() + // App foreground এ আসলে service চালু করো (যদি না থাকে) + SmsPollingService.start(this) + } +} diff --git a/android/app/src/main/kotlin/com/paygate/f/SmsPollingService.kt b/android/app/src/main/kotlin/com/paygate/f/SmsPollingService.kt new file mode 100644 index 0000000..8972b68 --- /dev/null +++ b/android/app/src/main/kotlin/com/paygate/f/SmsPollingService.kt @@ -0,0 +1,249 @@ +package com.paygate.f + +import android.app.* +import android.content.Context +import android.content.Intent +import android.database.Cursor +import android.net.Uri +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import kotlinx.coroutines.* +import java.io.OutputStreamWriter +import java.net.HttpURLConnection +import java.net.URL +import java.text.SimpleDateFormat +import java.util.* + +class SmsPollingService : Service() { + + companion object { + const val TAG = "PayGatePoller" + const val CHANNEL_ID = "paygate_foreground" + const val NOTIF_ID = 1001 + // Flutter shared_preferences uses FlutterSharedPreferences file with "flutter." prefix + const val PREFS_NAME = "FlutterSharedPreferences" + const val KEY_WORKER_URL = "flutter.worker_url" + const val KEY_API_KEY = "flutter.api_key" + const val KEY_ENABLED = "flutter.sms_forward_enabled" + const val KEY_FORWARD_COUNT = "flutter.forward_count" + const val KEY_LAST_LOG = "flutter.last_log" + // Internal tracking key — no flutter. prefix (not a Flutter pref) + const val KEY_LAST_SMS_DATE = "last_sms_date_polled" + const val PREFS_INTERNAL = "PayGateInternal" + const val POLL_INTERVAL_MS = 15_000L + + fun start(context: Context) { + val intent = Intent(context, SmsPollingService::class.java) + context.startForegroundService(intent) + } + + fun stop(context: Context) { + context.stopService(Intent(context, SmsPollingService::class.java)) + } + } + + private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var pollingJob: Job? = null + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + startForeground(NOTIF_ID, buildNotification("SMS Forwarding is running...")) + Log.d(TAG, "Service created") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startPolling() + return START_STICKY + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onDestroy() { + pollingJob?.cancel() + serviceScope.cancel() + Log.d(TAG, "Service destroyed") + super.onDestroy() + } + + private fun startPolling() { + pollingJob?.cancel() + pollingJob = serviceScope.launch { + while (isActive) { + try { + pollSmsInbox() + } catch (e: Exception) { + Log.e(TAG, "Poll error: ${e.message}") + } + delay(POLL_INTERVAL_MS) + } + } + Log.d(TAG, "Polling started (interval: ${POLL_INTERVAL_MS}ms)") + } + + private suspend fun pollSmsInbox() { + val flutterPrefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val internalPrefs = getSharedPreferences(PREFS_INTERNAL, Context.MODE_PRIVATE) + + val enabled = flutterPrefs.getBoolean(KEY_ENABLED, true) + if (!enabled) return + + val workerUrl = flutterPrefs.getString(KEY_WORKER_URL, "") ?: "" + val apiKey = flutterPrefs.getString(KEY_API_KEY, "") ?: "" + if (workerUrl.isEmpty() || apiKey.isEmpty()) return + + // Use internal prefs for polling state (not Flutter prefs) + val lastCheckedDate = internalPrefs.getLong(KEY_LAST_SMS_DATE, System.currentTimeMillis() - 60_000) + + val uri = Uri.parse("content://sms/inbox") + val cursor: Cursor? = contentResolver.query( + uri, + arrayOf("_id", "address", "body", "date"), + "date > ?", + arrayOf(lastCheckedDate.toString()), + "date ASC" // ASC so we process oldest first + ) + + var forwarded = 0 + var latestDate = lastCheckedDate + + cursor?.use { + while (it.moveToNext()) { + val body = it.getString(it.getColumnIndexOrThrow("body")) ?: continue + val sender = it.getString(it.getColumnIndexOrThrow("address")) ?: "Unknown" + val date = it.getLong(it.getColumnIndexOrThrow("date")) + + // Track latest date regardless of whether it's bKash + if (date > latestDate) latestDate = date + + if (!isBkashSms(sender, body)) continue + + Log.d(TAG, "Poller found bKash SMS from: $sender") + + val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) + sdf.timeZone = TimeZone.getTimeZone("UTC") + val receivedAt = sdf.format(Date(date)) + + val result = forwardSms(workerUrl, apiKey, sender, body, receivedAt) + if (result) { + forwarded++ + val count = flutterPrefs.getInt(KEY_FORWARD_COUNT, 0) + flutterPrefs.edit() + .putInt(KEY_FORWARD_COUNT, count + 1) + .putString(KEY_LAST_LOG, + "[${SimpleDateFormat("HH:mm:ss", Locale.US).format(Date())}] ✅ Polled & Forwarded: ${body.take(50)}...") + .apply() + } + } + } + + // Always advance the pointer to avoid re-processing same SMS + if (latestDate > lastCheckedDate) { + internalPrefs.edit().putLong(KEY_LAST_SMS_DATE, latestDate + 1).apply() + } + + val notifText = if (forwarded > 0) + "✅ $forwarded new SMS(s) forwarded successfully" + else + "SMS Forwarding is running... (checking every 15 seconds)" + + updateNotification(notifText) + Log.d(TAG, "Poll done — forwarded: $forwarded") + } + + private fun isBkashSms(sender: String, body: String): Boolean { + val lowerBody = body.lowercase() + val lowerSender = sender.lowercase() + + val isBkashSender = lowerSender.contains("bkash") || + lowerSender == "01769-420420" || + lowerSender == "01769420420" || + lowerSender == "16247" + + val hasTrxId = lowerBody.contains("trxid") || lowerBody.contains("trx id") + val hasBkashKeyword = lowerBody.contains("bkash") || + lowerBody.contains("received tk") || + lowerBody.contains("sent tk") || + lowerBody.contains("payment") || + lowerBody.contains("cash out") + + return isBkashSender || (hasTrxId && hasBkashKeyword) + } + + private fun forwardSms( + baseUrl: String, + apiKey: String, + sender: String, + message: String, + receivedAt: String + ): Boolean { + return try { + val url = baseUrl.trimEnd('/') + "/api/sms/forward" + val jsonBody = """{"sender":"$sender","message":${escapeJson(message)},"receivedAt":"$receivedAt"}""" + + val conn = URL(url).openConnection() as HttpURLConnection + conn.requestMethod = "POST" + conn.setRequestProperty("Content-Type", "application/json") + conn.setRequestProperty("X-API-Key", apiKey) + conn.doOutput = true + conn.connectTimeout = 10000 + conn.readTimeout = 10000 + + OutputStreamWriter(conn.outputStream).use { w -> + w.write(jsonBody) + w.flush() + } + + val code = conn.responseCode + Log.d(TAG, "Forward HTTP: $code") + code in 200..299 + } catch (e: Exception) { + Log.e(TAG, "Forward error: ${e.message}") + false + } + } + + private fun escapeJson(text: String): String { + return "\"" + text + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + "\"" + } + + private fun createNotificationChannel() { + val channel = NotificationChannel( + CHANNEL_ID, + "PayGate SMS Forwarder", + NotificationManager.IMPORTANCE_LOW + ).apply { + description = "bKash SMS forwarding service" + setShowBadge(false) + } + val nm = getSystemService(NotificationManager::class.java) + nm.createNotificationChannel(channel) + } + + private fun buildNotification(text: String): Notification { + val openIntent = PendingIntent.getActivity( + this, 0, + Intent(this, MainActivity::class.java), + PendingIntent.FLAG_IMMUTABLE + ) + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("PayGate SMS") + .setContentText(text) + .setSmallIcon(android.R.drawable.ic_dialog_email) + .setContentIntent(openIntent) + .setOngoing(true) + .setSilent(true) + .build() + } + + private fun updateNotification(text: String) { + val nm = getSystemService(NotificationManager::class.java) + nm.notify(NOTIF_ID, buildNotification(text)) + } +} diff --git a/android/app/src/main/kotlin/com/paygate/f/SmsReceiver.kt b/android/app/src/main/kotlin/com/paygate/f/SmsReceiver.kt new file mode 100644 index 0000000..804ba2c --- /dev/null +++ b/android/app/src/main/kotlin/com/paygate/f/SmsReceiver.kt @@ -0,0 +1,144 @@ +package com.paygate.f + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.provider.Telephony +import android.util.Log +import kotlinx.coroutines.* +import java.io.OutputStreamWriter +import java.net.HttpURLConnection +import java.net.URL +import java.text.SimpleDateFormat +import java.util.* + +class SmsReceiver : BroadcastReceiver() { + + companion object { + const val TAG = "PayGateSMS" + // Flutter shared_preferences uses FlutterSharedPreferences file with "flutter." prefix + const val PREFS_NAME = "FlutterSharedPreferences" + const val KEY_WORKER_URL = "flutter.worker_url" + const val KEY_API_KEY = "flutter.api_key" + const val KEY_ENABLED = "flutter.sms_forward_enabled" + const val KEY_FORWARD_COUNT = "flutter.forward_count" + const val KEY_LAST_LOG = "flutter.last_log" + } + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION) return + + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val enabled = prefs.getBoolean(KEY_ENABLED, true) + if (!enabled) return + + val workerUrl = prefs.getString(KEY_WORKER_URL, "") ?: "" + val apiKey = prefs.getString(KEY_API_KEY, "") ?: "" + + if (workerUrl.isEmpty() || apiKey.isEmpty()) { + Log.w(TAG, "Worker URL or API key not configured") + return + } + + val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) + val fullBody = messages.joinToString("") { it.messageBody } + val sender = messages.firstOrNull()?.originatingAddress ?: "Unknown" + + Log.d(TAG, "SMS received from: $sender") + Log.d(TAG, "Body: $fullBody") + + if (!isBkashSms(sender, fullBody)) { + Log.d(TAG, "Not a bKash SMS, skipping") + return + } + + Log.d(TAG, "bKash SMS detected, forwarding...") + + val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) + sdf.timeZone = TimeZone.getTimeZone("UTC") + val receivedAt = sdf.format(Date()) + + val pendingResult = goAsync() + + CoroutineScope(Dispatchers.IO).launch { + try { + val result = forwardSms(workerUrl, apiKey, sender, fullBody, receivedAt) + val logMsg = if (result) "✅ Forwarded: ${fullBody.take(50)}..." + else "❌ Forward failed: ${fullBody.take(50)}..." + + val count = prefs.getInt(KEY_FORWARD_COUNT, 0) + prefs.edit() + .putInt(KEY_FORWARD_COUNT, if (result) count + 1 else count) + .putString(KEY_LAST_LOG, "[${SimpleDateFormat("HH:mm:ss", Locale.US).format(Date())}] $logMsg") + .apply() + + Log.d(TAG, logMsg) + } catch (e: Exception) { + Log.e(TAG, "Error forwarding SMS: ${e.message}") + prefs.edit() + .putString(KEY_LAST_LOG, "[${SimpleDateFormat("HH:mm:ss", Locale.US).format(Date())}] ❌ Error: ${e.message}") + .apply() + } finally { + pendingResult.finish() + } + } + } + + private fun isBkashSms(sender: String, body: String): Boolean { + val lowerBody = body.lowercase() + val lowerSender = sender.lowercase() + + // bKash official sender numbers/names + val isBkashSender = lowerSender.contains("bkash") || + lowerSender == "01769-420420" || + lowerSender == "01769420420" || + lowerSender == "16247" + + // bKash SMS keywords — TrxID (not TrkID!) + val hasTrxId = lowerBody.contains("trxid") || lowerBody.contains("trx id") + val hasBkashKeyword = lowerBody.contains("bkash") || + lowerBody.contains("received tk") || + lowerBody.contains("sent tk") || + lowerBody.contains("payment") || + lowerBody.contains("cash out") + + return isBkashSender || (hasTrxId && hasBkashKeyword) + } + + private fun forwardSms( + baseUrl: String, + apiKey: String, + sender: String, + message: String, + receivedAt: String + ): Boolean { + val url = baseUrl.trimEnd('/') + "/api/sms/forward" + val jsonBody = """{"sender":"$sender","message":${escapeJson(message)},"receivedAt":"$receivedAt"}""" + + val conn = URL(url).openConnection() as HttpURLConnection + conn.requestMethod = "POST" + conn.setRequestProperty("Content-Type", "application/json") + conn.setRequestProperty("X-API-Key", apiKey) + conn.doOutput = true + conn.connectTimeout = 15000 + conn.readTimeout = 15000 + + OutputStreamWriter(conn.outputStream).use { w -> + w.write(jsonBody) + w.flush() + } + + val responseCode = conn.responseCode + Log.d(TAG, "HTTP Response: $responseCode") + return responseCode in 200..299 + } + + private fun escapeJson(text: String): String { + return "\"" + text + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + "\"" + } +} diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..93884e19afc2040f645772a693a97969b022bc52 100644 GIT binary patch literal 4739 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 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..93884e19afc2040f645772a693a97969b022bc52 GIT binary patch literal 4739 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..b670b5b2b1b6240b5290ae5e0d2e6959d692609f 100644 GIT binary patch literal 2898 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 literal 442 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391482be68e9e4a07fab769b5de337d16eb1..b22f1022941bb8770295dacbb2a1284fb4ec8dc0 100644 GIT binary patch literal 6803 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 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..b22f1022941bb8770295dacbb2a1284fb4ec8dc0 GIT binary patch literal 6803 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 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..db75b09201a9b1853dcc70b5838781ee1292d896 GIT binary patch literal 11206 zcmV;%D>>AOP)D7Dhd+$B@ zTvAn38WbuDRTv_o{+A^i3L;EZ1%Q&_{@3!YUD>C1 z_ded*^VF`~hJ8WnKI$xZupmPUfeJ*R3Ka~!5CU=O5&gdc0mQ}0PD#h-)g_OsPoLDB zI=MN%pq^v4bQ(}670X;j5T;HD1XH|0VNWjeSZn9KFZcZR<=(Yz@vQo41qcuZa0&&9 zvLQfJ2v8*&(K9@;=#dgAayXkNvc&_;}?ssdWPfuztwyOl&BB_a@1 z0jEB!53A6=YE+#vqxQnZ_2(^&&#xo_I>}E-rsNb*%mY`urnZ4Q3WPrB~l`0QYbql;44&TC+jacw(;G|6N?*x z$){}s2`gIwgWbKmuY0`xhilBs0aSXZK!!*Ka=@cZ5R`}+0Q}HPz{@1_wQpY1^zoI| zt6G4`q-}x-Q>SDIK)x;0@$Dz}T>Gck&M=3tV4YKtC_$qoP6(uxB<2~!gb)exGE|sy z{?cimU6p!u3jjC)^8}ykxD%$L0Ej8f+LQbi4{B3;>`+ zY6MQIHQsBr{*pz_pFJbBYzoT8;>2K_33EkpwfC0iTmRv={Crnl!_b5(iUD3G->w)t z$Uq#bD3(iN>PJtR{@173x&%ZOm>9cl+z1l|5eR+Hx3zxmzU*zA10duAM5#3C6acjJ z-~$}6qF{&|sx|56#;5)B8I6}74-AaXamDx$<`jk$y6YQ{wtoJ1Ua!k=79e&+3Q+%? z(z1V#D#8FI7&d-rh+OjGCA0qRoY?FdbwW&_air}?6Q-&HGL%{0(fYC5gL`-OAQUTb zj*Ehd%LJ8{t%pY$VG)$U#HlbTQ)A6vJFDTcV*x;P{1nI|N?7Sc+kW))maFfMw@Zc# zPL&iOAq4;eMJ$yrsQ=e1P0tuBgrJ7wAsOPD4;N@kLZVgkYGfoR8)8vwv_w>V#?IB+`x$VNPKvx3zQYyM7rww4>Vu zB5H^G&;QrMQap@lt!=sP+{U*Zr%s7+WJTH$AS{~x-H&d2`5iy!t$n>-o`UGm0FIEm z@92RVu)xAj|Ke+A{NL9U$Lx{235QEq#8&%lFKoTyHtG%X#)&8^xJ(iJv*Jvlj@1&< zE|vJ`ar6G;JYrVSBW0x>7GV)vyRLt{^&@wSb0IsDKPPfVhmx}4luP;-&YAyTZ#GH) zNU4s8PFRtxYyPz76AxrLE2LC0uG>n1rF59I=T2X6%f+_p$Z&Rughgz%e*F*apM0=j z!a!VU4SZ#W4T4&&atU5N?U>sx@oE!?zcwC%DVgslA@rdKcs6jHA( zxQF*_y7-rp4Ty`5xkJW56Bey235!Hm`z_CQU$eH`bLt2HzaqL06)aS+&VMQU())i^j5ZzA*AMW%C>hRe-naFm zw>!?m1yEIjMw-qmq#Y78)QruCw{70}jYnbVpoG~02vdcsy!igxymo{X8Yk#a!}wpO zZxBF4OvFTnOAjJL#B{_n?8PB(f*qfIAoJ{Q7{Ztr#{ClJlnm|s<{!=dZ8_`IsZ2^? z023KPbni-!BNKyXVgs@Y10zZjhOQ90LJ(%dFdKx~FwBQxA#?$pBT*t^W5t75Im{bD zssY5y=}RBF6QQW;7%j8WeTkwVWJmqZ zenCt`q@pS=Kp-dx)c31RZBnNDUc$F^ag*>(!VmiL;ogkva=bTFXwSO7oP%SK2hYHg zWkso!?a*MTI*iqP`LyOwpRP{I>#RmS`TY^b^umQVg}>UI;UG$P*9m5eNY&W|^*pnR zLMeuBRbkn+_l3{yA=t4yFo{$R!>NeNBP19zD_M7JOM1nuZbi) z#fXGO2^#x;`9kYkewH&qI7x96HryBX9&_#6nm@R5$Xji{S-$TiEuwdmJMGcyM74D<&*XNg#v* zQI+Dws1st=LNFG2ssuKqf#jqI)yc*SRyJR8O4X^eO}cpD8b)P%U}jKpy$69zDHB7Z z7Zr;B!ELOmSvIw1*;IV+l*gOY@p~k~1Y~H}b&vD9OrLR7N>uBYB9jnRW>P8DJ5!{y9q=TC42xbX|f`;OIP5@#CS)%=GUR3&1q!U1~XiA}AO3bdF^|4cDedN@(`#0|V<|Ey|dO<>$ z;FOSz(I{3R=6Lhx?yWg}m>czkciPoHnP^jHlAcGRmKcHW{O?E1!|sU3xo1Bj~#wuV6g z!HDNYVw8ZsFz2tDx%}P_F8R)z5{v4^2}Fr4nX&>QRdp(%h!cqlJ)`N{{~!X95=2Fv zsFM*ii^y(Ms1sGGJ#)@UcfWVhO_#}nR9~1eWR;`Jxcx#%A+7)Nkmem3-cK69;2EY+ zM%b3w{q4s<1*nu5t-LZ5LR0`#c1{WqGb0T1Mq2**oOyqDCMAtJAp)DSVJA=(FcA@% z;`9-PW?Vkg=dvLRj#S}U#7!&_GpQK)3{&! zH$Ij|LJSv`qfwY3LBWKH=X%<2dA{k2=gKjdFi|qJ=lf42_n^lBuHq)vexOjc zlnuJ=jQXYDx~TEOMG7tHU8UP3Do|#QqdUmZtn+@DcfHLo_cv?XyuQ`NEtTbGFF7ZC*v4~&M0HgREWTI$#vF4}(jylq$i zihFY!i%~F0I1Qgh0@?nR-!)#kESh8^O0qJ-RKakWVCS`KEF7eKC9I^mH42P1*`UWB z-@N>$D^trFG<0N4DOs9S1W;mTP_DIm-yQ4Q{^!NMKkdlw=mGK;@L&jr!SV93-RGpU zN4Ium*Y4pO~HoAgZJT*)-}U2g zOJ~AM%INVFb(-UR;@OUlyIyQOf5~W*%$9$FEO-2ToxdfQ;d~`!7#?uAC>xs(x?*R} zKI!K7`%@Ac3R#0l&Z&|J83Ul_v7Otme`5Eqp5{GWfU)qC#u8hUqIzzyVn-l?B|@a3 zdVq?^#?E~kJKAo3+MiNW|As}=-nF9c%vm6)6A&pABC*dzFcJpUXUtlD&j;7O?YeMn zr}a|e!qF}x>H)Lsnm;z4zhtQCSHb{cWLQNzu785afi|{foJ?KK+`;Gv)yhCv5uN6$I<4ljJeAC;rcep1$C^@X5XK zlN`DcFUKh|aQxu*-lun^Pnb4h9%J#s)CmZg$6IrMXzig;oR-QXCms76G^8M7vM%Gl zYWfMke2-~}iwpY=Pj!wBp{KC^b9X*=>NQdMbvP@ zCewo6+n-r`_I1y^`{w-SZev(vz}dk78J5t+W>z2ni>qjU6AD={kEl12DjB2#+jirV zqn#6%?g{~Dzv-E{vxdQ76dx6e$dWfq8fIy3)$u>R!Zaihh+zXT6e=MZ)A{?>C(roL zo&R!wI^+8`4UiTHF1qCqC0A@%r4U7_Dxm+dAFnAXNFYuGh}r72B)V=`^Tg?2-}94a z4YOj9Lde#rllbiFV{f@U-x$*{5OzA!(N#2uVBb%llimUh4c96e(V-26eTBB$p3kFL zG0v4RyNVcjhAAK_g;a3D58j!WTc=LQVP%$!DEIPF;E@-;&nCYK6JL`IxMqa=S zGdS$H^%;!RnSfEcAA4`zU=i;Z02RjBP>?YN&D5N`p!qLXsS_@-B_hPU?&G&_`^S4z zHU%>3hNLo#7iX-1K*4dpYUeF$p1S0}rMIArGR_F>sknuoKRdQ+cEM$f-F61UIn8U( zeb2_Qy$?JzuFp^w?{pRn| zrhy%+x`|9YQ78#C)RbSYMX%Ku2(5lOka8+ok)V zEdYjO4{)?p^ggx|n|t#V^p^}(c!Ev7C)@n7(-X@Y)Cn^w;8fVyE&uVzmVdju+NM}? zPF1MidEJlk9FawtMhK+Jt8M@JQ_p?&*UT(J{Uv2uapPxitL7&oUJ)XxRDN%^C>Ipdd3xvbyWR zmqP>32ofdaP4G#Rf)!3R zNr@Lf^Rr-EH#3P!R5}tB9r<&TQ$Mt_p!pFh#*_kt;im4)x;+@IPcR6DJ-wN=I|>No z@Jg4)2csCyIxu&XKt!AV?``_hUeCmw4DusPN-}wL#-d>BhugY0{QZL%(&m;(6*J$r zk{YYvhUH+Xs^OH_-al*sQ0^~d0DVvHj`dQ=I$G!4N(2D}2FM9B?Y-yawjVs5knIs z2P%O9WFFt?!Q;tpjygIW6ghCz7KRo#u_4bPh5$(C{ncj!C8ZYTeeHSYV z;>1$2`_4_>4{awRk=YXjDoSL!A9{J;!>w#Q8k7)NQ5T}3A;WL~^}52&9%fV45se&B z^V<2wCX1^g4Qz%%Og?Pxxi(;cWzKHd7j|a_;0lUZjtxWr@3{F%k&rnGke5=QCP~W} z1;oy&Pz1_0+b?2D^W9yz2|xlj;u z(3b1GbA1e+x_*U2`%|ha3j`um6|=*#6(WMD z2sFE+M@6CpQvjUb+7$(sCjJ`6hEWV>QUH4&*j&sd9Htdd$hjQ^RMkfkn|re_w4pp@ ziU>e*=~P%lg~8KC5ki9Y-k>LkQjnPot=(FAmv;atz=QyOkM1Ox==>EBDhA3~D=${A zU?vX2?zMXWKudla0)WK48m}=CU`RtyauD4YNJkzejydS)bEt5Iez>bm36yW`%x&6f zVA-h0(vJF+O<1(vRYmiOSTL=3(E z(fXZvd9Uov_$6;PmHdj)qXMBI0_QgF0U%t=Nuwx}z=Jod3UEU__sZ~Kr7MU285Z_r z1xJ+^5k&!(-?YzZVTdMN)DbI^FbFYBW><#&c^EhDj!@ z@C9)=BB7PtvJX(<(tOflb!g@GdQoL?zOApYufWCJuIP0Dh&NYZSVn*X1PU?*^v|$E zYoIf^h{jrOdpnn!s-slYO_f<}GXQek&h@yWnuGxxB-xNOqs$yFD}g=4y6#CvvNFZVd(zoO3}wQX&Q|-~!c=3%qn- z2i&7!h8+N0iGHa4p%@7tj!{;11x&*4f6AP&6(3a@Za4#g=M6OJI9g_yDvV(p^MNw2 zrviYA8}4!{$~fPkc?^t@&saeiMuiFhPJ1#S2#yXAAWX0Z{pVd;6S=S0$7{$p z1Y*-=Gt5-S7<_cO6Qd*|TNUq*d{wM>V;D4yv#auefsq#J>|t+?!9&VqMKnNQs(qBK zPxczpJiM*J(SUeMt)jezNX1_RkwX0XxL4-`19K}n?@VXUfHS!Cc~T7srY?#8CDBev z9+h+f-s~CyaTHgt>@~d~0*TkB{8X%bNsG?Bu&Z-OOi}>@Ldr9ZNuVgUI5xAIL8Fzv zR4NESdR~)Zs}dEv9t|K;XmVx~F{za44NL%ZM~1cwNEf;>@l$cC#KZcm zU7|r~0#zMwgeW;kd`Tn9)7-c=?$1qoz=)M)m4OjPQk0}#wYtU3s|_S5#cnGXOdKFr zyJ|KiSY@DGC5p9nJif(1ga-Km)Jc|u_~J%j;AJMF^s!UCik@L3fgmD&wO@Ds;yenr z;#Wk7g1m+cmH}m_s$gc94SW8u)xbiy%<}%&P|mW+VehpVXq@qrmVS3Kd}X zr6+PMA!4+0t8YL|PJ-Am)9O~w4iWa(u&62lnKe7zrcOitpf(Xi7B;@L(bUGv?P&&( zc-4$Bo)TCE711R63g#jcN)cJGLp4ti*14BScBmh(MCsr4zKvJouB#{$GHL2!zp8~*A zsb{x>3~OHY+`XP*hjil;(SU-g)h*Ex`d?>t!oID3^=zMPMb`9%#~v#{p% z3v$vsmcH*34`OywXt?a8#G*zOM;z5$tBOFe@Aq3XPi<%R%G*0A!3r0uC4F);FsQvs z(X_64?L37Z=Jt%^g9GH$s1g_p|Nadw?m;V0zh|fjYSOd+@--N6Pbf+|QSP|u3503~ zPZeg0!K*%brq`5G8I*?35qni<&go4%h*Vgi&gdvDAjBk2YF15e{=}*t*X@(3q(P@( z>XV-4kFQQHs#WK}#bk07C8EsBU2Q*mCP{G_lDkGQpxW~m0>c`sfC;McIkl;irX`~8 zsuhP3iy;PHjJhby``dF^56)jkB=U(+Q*MEQ+9yT7}}r-Tgd56(<6#Qo({6UR0c75_3YN*p$x zzoa)7B@0)|2ptho`QrB5p5A%K^N1R^LWxP8RG&P3@iiA`H3JT4a{T~A%$#+(C0}|= z?W?9sC}anAv{F?NZT|8DvM*CAZYz(fP)u$8g^S6sx}mL_8333d8b78!wR&cPeVED< z-NWD&X!93-gS?>F1d_>c=z?jNAAihOE^>iWCdY(f-4VwhN>Eto2|HV^KDYVfr>GMu zOLS5fb>7^%s*}*^=iuR0#e1K5 zx%JvV5)WjXTQ=&#nV(uc^NZ*8g`J*;WG31;BO+@!SLmJbp4AJ!@>&%~TvqL`0Fm~( z7q0$=EkL-qZ^1xCtl>EMsnsp1g^jwOp1;u>^%pD(rpLU{jM&GoqM#x%ioNuO-(=SA zBg;{ja}W_5bu#DgPM`m^w`SqgIb(RjMj#m|v^(sY_Q`V=Uw@G!CE}=}2mn;Y1;l3K z-`$t_(-!N+qvo&Wrzh&5nEsvWZ8sm?FzMF+yl=}jkB~9W^)DjC##1NBWeum?_2D^R zc~fqR%>>;dO5PD`Ma4vHnFRT;yEl<*{>*Dv{`P})XV21543kpi#ASe82r^!7bJw$% z+(_+NvDQe`dYf2_HB(A3A>~q-@`0103mo}J%b!I4$Cgj64veT&PUnsh&b>5lq0nkm zx8ak&+I7=fV;N;@L$q}QteN|%vsOO(iK!o2Rj3NGL616RW-@%R>twM2jfo61vq)G7 zGC`bcE6(h4}@l*FF@z)j-mjJKa4 zPLw%nHeFl-MUr!Cmwo%9%tue(@!d5$ezw+a@9+VFXW1BH04}PO9=>2k!aVBA29*#Z z;!seComn^K!c&^BT$w&)8US%Z9POkUwe2EO66e^O>`PrwzwJ7GavyuO8V3CrMNm=* zL5WzPTzay`n|t?zVDIk+16el$4~SCT8E_y8oIn(z zLIebpj?b;FUDaIw7su3|vmnu!1fZe>a8VsC&O=dE6=O};?{_@+&L4%FJK|oI7Pnd~ zYK8;d%otTJ=3lWizNkr^V)R`VhY}V^vYON8#x9uS-?qKapd|hFvkBRxp}+7{onwxr z3bg41KP_zQp8LhKLFAkmGhnw6NJZf^-jtki3_T}HZv{as1%Vv&;$+CIq6VY{Ql(PRuGpJ`;RzR-P4lN-1s%Z`H3Xx0 zG>D*R`NCP*>G0j;Zc8 zmL#R9RLKxqHq2Dyytr!)L?F>NI#B{HS;O#{Eb)-caM7Xz+AuM9KD_nmH+*m7JAQ-> zT``*uK*bmjO#qJ?xIfv07Ln-Ie8Pl4sSPs##RGg10 z!^n_2X?)j-yMOjv;?8YZQ$Sc*t73HZS{Ae@ANS5dNj+PIec{gk_wcUo|EcL6$418OfA+kpW2czJ2-{fqZ-`Ea@`;N~M&?-b zpA>@3hP|D?+r01Ym%9GAJ=odq11UD=)HH~Fc`w06ZjF{jw zKBvC<-(R=;gLgR=9c3@TamGlaq)diIB4N~fq)LG>v9Sp_DLlM&`@>tdvhfzx*PJph zeaf`DB~#-w>uh6!(>}$@IO_lLK7_q}h4x(U_V(<$&Ym@!Gtac<*LO)@)`ERl-%F{I z6Nxqu8B5fj7*#=x&YC{`ldo1MW6uaZP}>JXa60w9E4qI365qVB#|l+it0MwM8rSFo zTS*i`jzF+B<`Jora9u~ox?LSNVmmBX$9QT&>eHsyBoj%SjHzLl$+>Jko5`DQk+!a& zvk+!-K7;^9JVqY*RSJP~aB8&Vd>+H?5rVHA)W;TIe*rO}ytwZ`IB=hiNEu!5oi{!A zv+vBbYtMgkm?%R0G`o8h3@xfM@MJW)t5OC4f z>I3cAv?Trx$QN0v3S%|@=j17GKTgAgV+&(!T||cAwB}W_reAwrjEv8f)j#J5j2~5~ zYP6TBld^|u&S7$Hw+)(vlLxL~n{LiVsy2oLGYOc3>_ zrWf9EIiFOYETm}SbdV8Hu~SW1t+4g|mLI*j@si^YpDi4EM^Ywr;!jB*bLXYzHT5ay zk+l!)YN)hSiDRkXn4l73QMZX;?Bmah2gZ z@V63IL`!jJO=EJ-?H4z^`3S6p{Wz?nLoc!jJ@c2B(A0RsS+YhBRGvJ>!yhe%GZ|RrxR~^Zk3Hs&%VRUD z)g58BaKzf6N0D`>jc;7E><{m=XH82xOCV>)-Kwqx!&!;QqAK)b)ye5MoIm%QZ(z@; z=y-HBIHE^1q@l#;H7vR7-PNBzDPa%~Ju-*CVpX=`Y=~6TVVq8#vEZRAr@r$9IDzV+ zd7ci4BRmAiGDO)kEnhfy-aYU1mN%qajETq&Z9jZ4s8GXM2{05Fk3FyE^RHg?;1$W^ zr>IjLeAfQhF%E@$Oi)!P)o0FI`p}iNe|56wd5O>?rg7SbV&bQo62XwCoOZGJs_9F9 zd*#e8p2?np93Tz!@G(xekRmJ1R>f!j>+2T&{@t;&r^g7s5ZS1US1E&?sC?(*gZq)m zGMo)rq3SSZno><)KWpKGS602cMWbBX@i#-qu{4$pL!nO6D`qUZ=iSq9c#~b!nADiB zhAc(N?uV{NPPpM5%_;yX7c)L3KeBA;;~$v%iC3{LO{@v=^Ne%sn2HV&GCbwd6B{pB zw&%M~?*7^%vF-W176PjZB6ql2-gsyID}k8UNmZE4a4vE2f_Z;+TI$5<0N_;l5F=ZM zhKUIFN7}7A<&;0|FJIMg>9V#Pp5Al)pW>Uc0bHILr0R4O-QmE7^Z2!xQU(!OM+xWU zY#4v*f>|G1nLe!rQNBJi%tV)hPh55e8G-_$-jwu=&zv^pLo4>(vbOEJYp`ao4Y*GW zY7|6HHL4is|K9!$=kb_Z!Y~YRNQiHV@{L~l;-zyxa8l}oX+W{Kf3iBqO>8<10VUdO zRlMnaE1TYPO6LQcI&OR>cgIWdy#budpdcQTl8mkYp&luvgGfo4Nt}9WQ=#dj(5{%; z@Xn?6Z(kOhQ4N&(FidJMtI5cJDx(dCYR_I!d-j6-&hE}%Z)m@Dtvs^1I%l2Jyh07J zWsKN8`bZegN<^eYqyRa@)$_#U5ZL)u={GE%b@9^VsWYQZ4b*9=U}=&uc?oMp+EfKZ zh)u7adew@lSFPyV)Y1Lm%iVXb4<6a-w+A+eC2blQ*h(R4m!(iCwUeT;$S@*>g22Qm zl`|WGud%oq2V}gge_Va?b@OJPyD-uXNRFy;x*eV;a0RzY* zbZMe_b>4~@4Xb9QPisyrZX8g}ohmAestA)!SP8{qR$!o>wI)$>)`FU|7NC?59epwi zSUAtQ1X=M+2ohpq5&&A>f}@y?3*^b=$hj(Gr=;wR>iB|&CaN@0CDxipP(&%WBif{r4|BV^WKTBO+ZT4^!tO$$C#%7rOnaO1Z93-HBy4@c zG^KcI!mmvjf9M#EoD{^QLkm|b5fx0>I6;dY86w9R(Lq#>R057-!iw|nK$k#73ZxPZZSFxc4GSZ=Z`+mfxi>e^jxn|AMW&ilumnQgO0(yp{K($8yYHFM|A zEx&u7(+{aCDhUb|fdT|FJn$XoJJ*@cZSBhL=+3qEWVapg+q#2&si3{j@5;HJjQ07O z4u)I~0#VdZm1mNkR~NGlN!yh0W>iI6Ya^|-(b+YT)>_jPH!*L}$3#^WM8pJ|^pie{ zpvoYSib6pIaR2R+bdcWC(evt_+`5j;x;@!#9e!Jn-;qIDJXiu)NudH2s22VlzHSErk>d3shiA99~RRBaIUkW7(|4`&V z1po?GX0QPpGNgtyNJUcKjGDyCma21GYtCp*oZMokCHnW{lq!o!Wl5m?GtEHZHl#Q1 z?|yVk*CU(zp4yRny*r7hBMIOFb;=apcvV1^C-j6hJOENARsbp_WWiBb15P8SEwSX8 zvuiGxU-O3f@fD56nEWT5Vz9g{QV3T zQb4r0RAsju*#C=H+V6Uy@5x>9OqC-SAT0)fsuCnzB!mh?84gkqAd`RFl)y|-kdQ}+ z3U}I|YUQl@YnL{@by;G?^!#Q{L7*}eNK>6a`PIr00Ll5?4{dG#(es@Ty%ud(fu!LR zGXYKmQo>YzkcpJU5R20oA|6a%ELZb}g-zG5s(te!TN?wQP6o=?vcS|OkctxWT0*Wp z)&8TGI=}Zq_PLG-TrcOufR$8~Le zPghR6>GZ18W&mYFqbWupg^Gj@z|5=rcYp1V?LS-_@1~6Ae5;~R6(}>5R6!8r=`+0S)h5+VsEAsyTHwLhsQ4pT(5Ilpv70 zywu6=>)rF^C-;2osaS`XlZ-Q}qNEH!2?7OaXjoMqfdB}F@M@8jFrKK+RQ=pG+IU;-78YJSNA2~j~&mf~R_ ztz$l-AOgcUN@$c$tZVs)^Bb-?0aHvA9VG%49D_k;X2<7$zvn;x5D7R%&M`#-$-9Xm zAQXIlD2ShsLtJ{dl?Wvm;<$@DWRuq~o&ArO#^=;c9Z_@?2vih2oeyu=_SX;7OMShR zA|n)pSr{NCgwd<;5c^lI;xES#fe?ycVue(xmzx=zbITi>-ggqFhA2AX1ab;PK~Jvj z?;qa(l_wdoj0segR7mr&DN+K_00mixX+Osnfr=nz>udyG{MtoxzVwDjYt2zN(2gL1 z!hkqa*B`fT{i}z)r}yvEtF zkv$qXQUnqWovORP^2fHne~5b}YX!bixw?^$8uB3<1M7?uT0V95tj}G<5nEIYjxdf0 zfm~i+-uCeaQ$KvE4>>g&`UXdemLJEMPzEX0Xw1H0R_m>Q5})4?Ugslx7?VyQ4TLPG z*YDl@XLq}2_xG6~2k{&or_SL+iO3)k#AQx&-gmF4yJV3%0n?F8cbr55sVYRtaOW>J zYOv&Fp`@wB@EVini;K!K4wWpfd0N#?x&d`!xmX z8x_eXc@6RHOh#tlu>&DdRu+?#Sp6qYZT;uVa0J!r$sv$Jp+vOpbHD4j<*_UULLh-q z=;j#|y3*qWIy?vo0423RRIB5Ews`S3ujYs;8ZMKHi6>CVeRcBs&G)5meYuY_s>hY( zBLD@Ite0>RbLs5Gw_oGcCysz=Ho*kSvnS_Xzww^z9UFU1RzycBmpP7!hqZ|yYR{gw z=;!ZR1QrX zfkeS1xnRR}cLjHE>a$r5WWd`~JLA2wpb)iO9mL#76c-v6`E zK{v4+Iy?e7g`wTwe7f^@cVruU%}NS(k? z=Yy~9`1pf4&iN83L?Hw@?)aHfAOOy#OwjZ1&+Po>A7Pj#&cZoV0*R90%!c-@H{OGs zbA#6`I_`R&VjxvPsb^ZgAe0|fb4#qnjUEI>}v*7qK7-};GAp+4Ll9wdQ8 zVW{WXw(Xz%Rhs>*`kHSzd3?<(8)%>@lRfa|HT&;;>F|}IgCURtfJ^zCZn`hpBZ^Ga zIim=Bf(hj_dyXgfOno>NA=&=1UuCx*I80^eAPA(QVA}Hc58;{4v~i*0JZJ+tUSLrk zAVI+5?ebs$@csGT#)qm5jh8^`gba23X5+rEJQGmZDdRX}MnV3bP}pXgh?t0p43lAE zCd176Unu;A!hY0+&y$8flXZYj`%E@?pl#2$pCdzuEQH3F&Jl)7dvY(G{cZPhSJntS z7ZRvwZB4lbE2FQZlXkRWSvHdsh7fx`lZu1nAPOZwAPgu-Ko&$$2WG&AB{@XIM#(zL zIRwTTo;0062_AV=pIGt4jj_ek4lRv(Tm%ZUNw$3QfBL?-rq86qQc?w!5mt>(#bh5H z?ZPW2KGBVsh=@Uc5cqJ4j6=>)lB*K7E+Ml$8;?b+JZ?&|<#;lYVU@0w-_e^p;7oT; z_H}3XXSFXUnghtdMi5~(V5o|gZlZtKFkJ1dxoXa`d)@=lL+F4$4g!gSsqdLxFJJIo z&A8&J6qDg;N_Lo&7zgo@mRK@#cwP?Wy}WS{pxPBbJ~r5GVpa2TDb4Y)NCwdGnJOw>F+KGcj`-5~jGZ z=2PxeC11_0`RNKwlyCS*Lpmhnkloka{`~gtN4Io6*_L^3r}m^RY=oj@t%IPDY<`%L zQDsEcVs3KP(;qbTaiW8h&326UMPa!8`)lRVof+fEm3X8luj~6EL!qYHi#8`l+JNHb z(3b=PGpo~d(5nclSIn+|;|WcdEv`MM)z(D|2O#Pk5O%E3w=350pG4tYj_{x1)~pEC z0u2d4N)Z#CRWozeikVleKo02__jWw``p#c(=y`mT^tLAv^SrnysT{5=kOVp7l3Oy{ z|Lw8)|L^4oD}+WRkSa0z2hwd{_zg%htPxk5CR43L0s$o9Xapf1d^uD>q)rc32~q+e zhCK~jT6&#lYc5?j=lV06E?r7Z5dZ=>Cmd>og?`~7MT%K20}ioY-t<58l(*V|@Pev} zswjILUp;-+>glsSa(3^U9oz4GuIsLK-CH`M@FFHAq$*NuyKo3l(Ub{#zwuo2^`|D5 zH6Bz!*=Pg`yBF^I>J#R*Ov;E2NJ$D*>Ax#e)0hv(KWXvi49XTbDDnsZAyP{mNIK~8 zYs~a3SG9ih!um5?!!HUal;v&m!uKDpmLAg25HXPNovpA`b=IuqXU%f|=hC)2pWpGF zC(~38C(enJg0`YSpsFI8f5$nI{5mxbJ~4p}L!nMlXXbwVoU9(RKnjpfu-tAKKLQ$d<4;eLNp5O`y;~*!{JqqCId7QWd9n8_Np;RZ}h zAPSXY6&bY(C$HF%AyTN5L~Gs3Z(n=zkN;8^)%Lkg!|Z8oZ7(SKD#Q6Q)vT@wgeZZSH3)jGKkIMKU-bXHiL0!0fnkG@)Pisl z9s|Uo>R`3qF=` z@mUR~DK?}Ma`eBgJR>MmR`o-VK@2&4*P+WVht;+@pTFfJ_e>VLmd*%SKu*m648=cYwZ{MQ?% z-*|$OLe!Yy7fJzAQ3ti0pAenf*46Xe&V5g8?_0Ak{rVoat3z@*g;*F9Za-koW$5(T z3w3Y~Fo<|@W1Aad^QT8wH#D9(tM1g}kHpk~;IMl#uyG#1$1U;abgg$u?byoO#E6#li+i$G?u2JBYx> z^yT_=&a~6M|L)q0=1Aa}38jR&iYlN?7L?tSKJd_M`|n-f`NyrYwFiK}c`#8}OCB?8 zSW6Q+lZtna08k|V%}^1fAzM1Sx9r9vyoZUKek)-bJLZL*+?k|@Ca~{;dJk>H}tIAUA?kpOml45gQY}F`)+^9 z+m*{02Uor&UI%yB3bpK|{Z6cIJoASiNG@;CoN&Yx_fuR!3SHuvfSwoj@A%$xJMVo4 zo7y8Vgs8QiVJfJ@Dx&HHgb{Axps{B}AhJ>8k*cc7E8zjL6j@lOPAD?Sfh>S3 zj5snDK+l?;+rIq!o_k-(c4uPnJX0gAs>r_8?YRIamdqJSDw(BRT>)-c58T>V9@tM9z&`5V4%Hl~b? z2XJFXaKb6YS(Wsi>vNyEFw$H#dYZ=|W~hLOdVaet``q4)Iu+7<_irgby=UCUhk{^Y znIIE6x$(3+Z#E0-vf{`T-Ymd~8d5Sgv$^}lkKVK9lCSN*?J4fbCA^3+Mig+MaMt30 z5`!?u_n-oW{hw8ZsE7mx@ZvQl>eg)A_~APrzw|5HfA~BUWS9aSo{5zwzF`;Ss?VHt z((Uh0w;C7Z)Ub?WJt2k9F1kC|f7g19wE_(yP`I$R-}YQgj0^o7Qz}M!P#F8jiIe~( zHaXWVi;^e*jH#lx@=U%$hwf-+0^8SAD1Vk9*0ol8OwnYzmGRGA1HUYF5uW7zu1^TmRx>ZS7o->AVpZo#*UmzPvcX*_nz3& zMi&Iege!t6oU)1pWNdb0_xgAIVB?4Gj&JkpY=eT-ITA8~g@g-_gpG}zl7ot>glQY1 z5M{;ipYdGNP#_6GiA14+H6~HdHMw7HT66iAwtn%q>`0kZP{Keauwf0P=KPiw-+o(X zEC~C%2jq{QEUQk^9)&XZi!N@@Qb_5^e?hpV{h3kuT zo$}QiYtNk{ff#F*L!|=9DpZH027EzP1SvGljbCe^4q%ZWY~e0Ax5==h^bFg3qN^TVs(Rv^e5n{hPhit`K3?ZxAR+1 zCApa-;yHvV^rfDrqj!Z0I*&MscoAyPJbV53x*pj?%<6Q20Lp_6iKOc;pEvu17xlSb zA|CxJscH_vfk!sEo-DX@dpt%cod}stojp&tWhteuUqc^Q`Bh2qGpXdMt#fWZS4D|f z3!W|^6>&_w@gE-8`ITQ+do@Ngq%NRhDZ!BEOOIlxM-jR%nU%>$m5IdLvG&HF^uDwg zh62gw^$f70i4jqiC7-&;EN$>zZgf9033BAz>)pLi@5BhJSeP#f06mYl#k$~g#q<{& zAyWqckc;q=Prn%n8YsvW5K%T>+qa&3{R_WJdbP#{tfKii=#+MfDM6fgHmYsiPyhM% z+@36l!$Jpb1dxG{uEK8N}5)@X+fhCTkA{qu@X6dSufGal`_FS_OoN{|vEOkWW|lnBA@%nKjAJKj!A#s;bkL0Gp{?EN@JuTRlr4#clEH9Noe z)b87!HO7!==z1<}L;yhq3C#On7o;OZqgHc>XbygQ?Y{K3E>MY8EG$k&di~yDb7zhM z9Cn@2RKds>g_zhd3i^wNYJw_()_?Zr?u8vmPC9Y@4Qq-Y)Tui-rZ_}PKmbQ!Hh%tr zU~`%{AD|l;Zy`2LrQx#qjTbJ?NqUrWW>Qr{(KPk^aXWzFiD?{~d|glPNJ^B<@I90* z4n|>76*Am@-`c(3f3lhz0u4s0ou9&5P#$5#!`f(eWA|(S^hoLE;sl6rgj+svW{QP% zj2mx3eUEL)e{eAJIVC;6-zpPZbzKg~x6xMsB>VjhpM5xyivtq&u5l0t<$NCo4$~xL zto)dx4hj$0NfjsCzW;dlquUMh@P&oRLbc_JrB$cR^(6;3BRz4V@Kw5>-XTuN@Q_qS zCd0@{`i1=gWWxL4A61N3Sf~myZTqh$`(D@)F>#gt^4l_XqK58Y!<@*<#;-mE2cn_p zLIhRexS9EmlXJ+9@?emvqg?v6{n?$}z);aFR7Y$HMvX~4+)TqQ=;#|4XO2=M7D&MD+(NRR_tw+Ae_`5)@+>xV|m>a!?f$MpY*9oTe0%!7=Ugci;6A!&43j$^!zX zN;LItu2DcrM%bx~z_$P51?lsL7e+FZs>bG4H>{rL5U?>c0R7u3d^mq=mut_2`|jUE z(3#G)bqfWRWrAT$@u4C_w&xFR`(N1Nu}2d$d}JztNExb@O!rG|z0d36j@3 zn{5kSK?5XEuB|(wb`l#}A1#c!f~hKjdSBS(Z|sDLQ0cn2ijY766dvN~^}B;DLl+Iw!wORXdmrB{z9S}5sSL+YNsv&K z;`2#=A?m$K`Idd3}GDbcnRREdSx+LX$M7EHu zSNvFU{8^2Jz1CX0TeG7OHa;0 z%G_&T3L~8mic*U!Is?0^YC@3wXWE{d6OxLYW_E6|=-kRb_NdRq!%Av*@&bOnIKjHDm- z+Z!o6QB?e3;4779d=)WLWEl+Btor-97(){@D;o+(kM{p#(0d9XR+9*OF^sqaRSgD7 z?XjlylrctPh7u^~%h{B#U@1I96{GHq2vq`cDvNwI;NW)&0F3oU>P1JYN@SG%o-7Kk zd>oSm0sxl@a%m+PgidJW%U&v&MM|)MmkCfRMxzP>Y%)pk4r&Rn+$C&eF$^Hc27Xou z{U(@}dkXY8pn#vvj<$g%H)dMT;P)$60v+UpCNy9zt$A| z_3Tv;5waTW?eRuVa4{fhj-ZeN8^T6gYbWoZk19%E1udAIH49rTjEIc%%vBVK08LJ- zjW*ROfT0=8svzyoaA%glrLP77fSp@~!H*_KT5BAn64(FIq##AuN-4RthH4^C97AY? zx}M5F7^U$MV1Bi!u_e?)1p&GJnas|tfJa)47*sW@4uids9GzY3BE*V=8Sh&T7$qZ- zNP)!s>2_wlgI{SUJ`AKVAcwx_vRarz;B){&I^=eCX(|AXFcw1*K;m=ihrG;QYmKdq zF-)aBfTPG%fu$j zPHniyBG^t(*jY(~V@I0=5l9q-1JMRR2~LD;UG0kb07`bs1dv46zU z`O0n?xCaBKYA>%b53EczF)uQ|!NOEpgQ-yo5r+So&s%_~r>+7Oz(F7c@OeQ^d~qX6 znP4DBfZlbx2-az?&?|gEkcVJlqI2rJrsR-4Fo49;rqRZ#mMcaTs6>D!RyFg9(^+$s z>;VD*^^r=hZsz8wmQa9{fS^5On@SGE3t^5XgsXI%g?(XdgR8zO`z{`dz7=ofZrQs~KWggq*2b z*$iOFEGY(%Skag>J_}V;On$6WQDOyVU9~Ech^iARm~!x^ov|>sq|Vn8J_{m(L-+5u#Sj^?aD{&<5i9vI#8)&8csUPNQBxc9 zPHKvfWy6)O4fKOZp+XUwdBchQ{OU|ikx_r3;hGRn(pm26f`|nrhCKLvvQnM zS3%crtmT*#3{nwph_$@`?2M*)9G@y9D3LN}kezpj>In6fTa7J`dt%?wbRd5CNAn&=@@$0_dbG!2P<jPHiT2VmwoS z>7uw8Qz22+kr@zXa1NP!#jK)BK5<#?1#>v zDjwO6pS{xc`;AeK$q0jTnK?pU^*M7RvucKC5#_fjel38Sw=C;s6`_h)#YSkd1-N#< z{kfeW8YqW|grKP56Tk5`e|{`*IkBpFnO`;_fQ;?X)WXlbvF@B!@dXyFhWgJ!^C|JY z*z(mUW8xLt8V}nQsaS#5UAY(-v4oEM|5mL#byjR?V+5uW8rP5V2spg(r3ZptX`o0m z1pp>>k~pF9#IL+370HSCVsm9wd`JKrPWjz)uRCYKU!4<(5L*yv(Bed@j)?Z$zP9VB z*F2NZy!tcrjw>M|l&d#&Z(fX%ZIV2JLQ90BrvBRH9y~JTFM&q3!c5ZhpW3zg*2joQ zMGB392p}-~F4uJB;${E#rvZY9ixB0X*(sZG7CO)!sai@0>Et;lEdTdw)G8BTbV&45 zH2|6HzSOJ#bYEN}1SOOv5Hn)L)t4-Y&8mTnnD5gsxWX+OuRS3h_0>T*l<5@+g(l<4 zonL+|`${JnD&!{+!ptdVasJE?pS9?gx27bmpuh^$PFVtl*@K?tjDNr`n|{jo-$~OA z1WcS~Pyd0dYCuF=KL49wQwJHV5(r7y)_4erYS}E!Abf%EtsnVnuV}oY_&e zWwrE|#AtP4?3tjmcf;p@1)oR;nOsDMiACJ(PoF#ImdkskBkIJMEHD*~)37GVdN!9! zdrM}n{?QHIqD1I6#IQ@CPMKq!zj%Gie?Ag7Nr98Rt}=`ujEGn_H{Nu~Vj>!a41?ZL zQDSPo@f0maJ*MnaH#SDI!o;}*#rOUA*`428OO^`#{R@s-W+tQJ=KuW#i~jxEUgN|u z8-uAel#c*)r0`6i-|fzrbNW3Wh@RT$oR+Y+RaG6B-1f}+|9y8PZFKYjUqn&Zrnj#& z)e&_vicvM_o5EGB{>r66Ydj_~!-i451;3-;YZ8^HjlKHWhcYjB!KjD;u8@2eb{S_; zH}7NTulVkbsTR!y=?L3l7V%Mo0^*4U_xK0mmn}Z$&i6)^)jB5y{Z$7{JyS&>8W7Ri zPdt!)Wmm!^;AF6CaR7)&kcmsv^{X&Ehk7suQc6SJilyI+LJ<|)fJ(km2a&FaDwHr+=t6)glhy@Du?IZOYGxtCxJ^{KTpj=+Gp_AsA-; z69Jg>m#3zx`b5SW1U;28N-Ru4Py^xsaU1J?c*FY7+;5mg3HkX>dB-h*VT=ZT_35+D zec;1QH=osQ_XjS`hLs6m($I^0Bv7BgtTP&F%8e7;;j$H{lS&z(4}E9eTGuf~Ew zB$IuK!pvnyU?yfw2VL#l)AE6H&wA)X)8D>SMMw}f=qVfuhvN)X$vGwCbv?Rs-A#AI zvha;6;E4%CryOQsk`{+Pu;FTndZ#qjUb+yfV>)z7sMn!?cIHRU&P7F~)Ckt{z-SVv z@JIg7My_4|se5+czTQ})GTfvE8CHRcHeG$vxxf9`+|OU(x0qD0Kah;`NGCIkA?yZ0 zhND8h%l5c*;*zCj-uG84zV(*qf}}bjD)#3YqRPNIXN~QBX7>x%e?753rCt_ZqYZCg4josqVCg>5Dnf=^K61{+`!;$`bNP{e<%Q7*B!hHERn2+q z+0NjZ_kMr%cmKTU%BA8&nL+)5$b$aCP*suG(z@kex-$L2^LO9AZrdGex?kO$2-L#B zc!ns{jY!pv*Amfu#DR#3gp3N*IbX7hK&RI7Ow);b9+cz|g}$X7BAT4Y#h#>dYwiHJH0vsFjO2#v;>RJ8UD zb7NP|(R+90tdFuwmmXsU8+2a@AgAQn+I`t|H+*-+*RP*`-6|CkD98-lEc}ZMLqvf1 z(%Si-J9pM!pVsl4*Z1GEzWeu^-0NLgI0i*w35J=&<_-BLu&8mX|C*`=CQCzaZXP&76EhpQAyT|8s;Z()Ol(5p59E3sf`;6$ zO>2Hd5m6$WL}J(1AMakj^TaQ`BY8@bQ>TuIOZ6iS^>7pF9#Byt1rg6kRLz)MecC}E zw~C6>FfKq${gPm$6VklS%EawIe|^K>-k*JCOO35@s-#3jtN>(SGx?&(Dk{_<70AL{ z-pJOd8{2v>Vl<+KLMXAcspUgwR3TDs4xv%4u+WelkYuL=%%6aq&zXALko|4jlUIGC z?HkV;-;iMy8PJ@dloO6XhKQIn)MUjur!G)&P9zX@I!KDEsEDY`Xy8jASy3k-B_@W! zsNRA6dZbR2K!($8T`zs;o@d|n9rw!qDpMUe35b}$ih`}K0~b6k3>FTlnskxG6?5wT zWP!>-um@wO+Yv!AFIBZ=sa?1m5j zP6}9#8*3Vz+l~>i>SXNkx7yE?}Qy5|lBx-DpNdynPxaLh?d*ze& zXSVl{F<_%gkbXO6_&x`kTjM#_Vqg<_y+&#HUKJ2k5hY^7l+wLFUi;K#|FQ97cSbko zlBPxwHgE||bP63j0~HS~$e#W~WF(z8{Uc{2S2nAYgOw5Em>0%?Xwy4Zb>6lPzucNK zzDhZGg+{f)^1lpTg-D0p?oh3PdS23Y`n$jQKl|=@an_Az&V2uAvDSfh^-$H8`ops0 z!tf3ISJ#4iuc|O%Q*?3%ay##QasPik)$_!515Vmn5rz2FU)_~gu8ShJB$N?zo?D!p z^{Mkz#%sC{#MlJN7cdZ7|MjvLpZcyI2!ynpM9~PKLfQrjX<5L~{!0KPeA5ZYCb-UR z&20VrZ`y8seEN;2&Ux3ViIp=D$#0+lC_gD+XiQ=FmyZc0KtrvIJVv-zAF1be?Yj5H zUH|=p|LXo2Op@ac5GRHG7TR7HvI}(Z7t+AThKPxkygC2=CQ}=O9LmhS@l7mbQYY~x zjjgv_uUSGVPq#&^ z#v~Oap||tUz(}}A;{7Y@{$%ku{m#QN-jSDK7^vlgXLj7RA^w{!DKqsu;*Dm7VVHiQ z;~Tq2LgYBg3C|LGa((x_^2$9go7Se<(`M9Nw4na{IW>z~xS9v9KN@~p-Vr^hl8D2I zR7f%!%(WacyE;2x+}Zp4Z5>Z+&OEg14{tRBSB z-B1Wc7uL7_%Vj%myiYhZSf?t7Mz_L-DJda`#&c7Y^4o@rtpYF<XQ(IZ=dwLreca33nab_y40<%^%|dc9 z7zQU0Wv)^~0#OarQkrJUA9o29@^p+;^3w>lVLb*de2FH zk8kU}bzQH?Avol(o@9(33)3;TI;ii0x-hLv6BJ`u$G&Qy=>S`Dxh=f`h@ktRc{Mxy zL}A?`zycQRfxwom=P3!`lLqP+Z0F9HI#5(aAD)j&z%p^!z^S!__5ziP|W+% zo7S$`7F*MuHd!h!2lX(N$R8UAN<;(m(Iyr6qDrA#hhYhpD9pkp;-Xz$_}!Cpeq*W$|u$*55E@dE3b%+l|?#l)f;g7(HifXuVA;Am%KNAKa7_ww|?yYZ4 zES#42U>zO~>E1J`)2fv%bH01ETJi`}vKhmuR@j8B2jvE4B%GOk%lQp&U8;d#A_~|K zQFxFcIBmLm#q?V)NQg1)O+B~J*s-t>tfiZCRT#)pwTpQ-E}i}9^T(g=GbRpW^W(CmV)h&g!z-ML)Tg6OrNbRt^V;r~eW`!Jb`t+q?Wz8$3kGaySIJ zj0&Pl_NJ&9C~L$SX-+Ks>9vtrHHYIfEQ!N5pqL

tZL|eJ!nuMM<#*w$e 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 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4435b8e1ae358439722cde5b5f4a86a5b87acccf GIT binary patch literal 15991 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

tZL|eJ!nuMM<#*w$e zrW};viKV*e!n@v4EVlZ%=b-g_-ANW@)CJRI$NbWB@e%J77Zr3F8FYuy_cY9KT@ zokb>0pphXBT=InGh4;Uk=fJu~&vXkZPoyh%? zHR_~l`Sb<9cqf*|;sL7}UWRg{aLig^hT`I(Dl-3PZ=d$o6_d&yOn47lGOSLjmNzf| z^^JT+eOy^RkBFzTT~<+6n4y@nc6xI0{nr&%uE`BFP5j~bf z79Yo+6=o^nJYHNg|5w-5UNH9vum_XWMLXnBq@ilre_xk;=aOU)rE%ukOeBsWD@=*V zq9%xXXEiVR^?RyLZh_Myz#dFq8}_g$(ZnP3zxTG}-<=i(MV!+kYV8~=R+tzlQzD2& z-o9YT1MiN_t5-KMfzt6{GD#Zt0VT})hc`C=;0jlpj5>S7PV88*!VDQP5wW$OIeXDh z-(hOwkcsyej)F=4rf{)vGUMH+F8rhggJZ0lHa{| z`t_^A-YF9s=Ni|69GR~n!*Jp?Bp3hqy5=ukOto6&oUw-TO|mBCJ4bw{+^sM}o>(if zst=#E{K5BBpU{l3pT^N}lpMiqRKsE7=FE?rxAcGBXHRdclBg;st;)3mtuSRMbJ9h< z+10audUfl6TxOCHh#oo3au|+iI&&ys)k)3jIZJ={{;I!Q%`p=bYb=2$JPD+-tS}y{_qmH#Ja|L&ycyM+utF0YE3P0b zj0{1wI*3MEYv%swszpBuGkbJkU)ZCCqf<~3b`pkY^?7rbKm6X>e?HGN#p7ZWqAY}o z@LZR_6-EFtgAsF{HJrTZ#N|)^MdNj=RPD5OYhX=rQ5%klJ&-ACg)zV!a~u~NxuA8)gE!3o`W2CusxYPS$Xi<@;ou7kClsM5 zQqkzbhQ&X4chg^N+WFbX^bfl^2m(@N(4mj(7{dyivQPpsS;sM0EKM|h`uyqdIVG$s zC*~vRBpeTpGJybueFekjG<6p)sQc~w_B&tN@x>=1&$K%c--KP`3vF%=f=*>x zRm3ttBIE0&e`KvNrTjEFavF7>b)2$6 zvWluIS4A4%vb^yv%d=a$+8=yn|NZObsa>%yAzyPMJ}Dbwm@rqQG@Xuw6-I3GU7f9P z%vlinBF>)NRCmSVd2e1?b=u7QhGAouGNew3av)Fzl>44J?48r1AtPfwuh|>!=_(OEZ9?HzyLDvrP1p0rsV2b zEf>$LK4YeBuIk^H3&#u4k;z$}Y*4umZc^3!JU1GYP9?r$&j^G|NuV{Sf5WZW5uuo< zbATh&d>7h$hp@q4k#DIP_7G zgbXoB2^Cu)0(A^BObJixBV3>KTjGg1$*TEH@wxTUdG*mbwWcW%erf;L6|w}UPUvdF zQT7t(aG)9A_2q)zoI8*Wdb57o_j}R+26-V>P0X{&h)G7g>X=s( { const SizedBox(height: 24), const Center( - child: Text('PayGate SMS Forwarder v1.0.0', + child: Text('PayGate SMS Forwarder v1.0.1', style: TextStyle(fontSize: 11, color: kMuted)), ), ], diff --git a/pubspec.yaml b/pubspec.yaml index c533685..77f64cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: paygate description: "PayGate SMS Forwarder - bKash SMS auto-forward to Cloudflare Worker" publish_to: 'none' -version: 1.0.0+1 +version: 1.0.1+2 environment: sdk: ^3.12.0