diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 6ceef46..b093c41 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -71,7 +71,7 @@ jobs: steps: - name: 📚 Git Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2.16.0 @@ -84,13 +84,19 @@ jobs: working-directory: example run: flutter build apk --release + - name: 📤 Upload APK + uses: actions/upload-artifact@v6 + with: + name: app-demo + path: example/build/app/outputs/flutter-apk/app-release.apk + build-ios: runs-on: macos-latest needs: test steps: - name: 📚 Git Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2.16.0 diff --git a/.gitignore b/.gitignore index 5d21bbc..8cc6c4c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ build/ example/pubspec.lock # FVM Version Cache -.fvm/ \ No newline at end of file +.fvm/ + +# AI stuff +.gemini/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 27bf515..405e30e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [7.3.1] - 2025-10-20 +- Android SDK version: 17.0.0 +- iOS SDK version: 6.13.0 + +### Flutter + +#### Changed +- Updated example application + ## [7.3.0] - 2025-10-20 - Android SDK version: 17.0.0 - iOS SDK version: 6.13.0 diff --git a/analysis_options.yaml b/analysis_options.yaml index 1fd1684..390280d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,7 +1,11 @@ include: package:very_good_analysis/analysis_options.yaml +linter: + rules: + public_member_api_docs: false + analyzer: errors: document_ignores: false exclude: - - '**/*.g.dart' \ No newline at end of file + - '**/*.g.dart' diff --git a/example/README.md b/example/README.md index 4c71df7..954431c 100644 --- a/example/README.md +++ b/example/README.md @@ -1,16 +1,42 @@ -# freerasp_example +# freeRASP Example App -Demonstrates how to use the freerasp plugin. +This example application demonstrates how to use the freeRASP plugin to protect your Flutter +application from various security threats. ## Getting Started -This project is a starting point for a Flutter application. +### Prerequisites -A few resources to get you started if this is your first Flutter project: +- Flutter SDK (>=3.0.0) +- Dart SDK (>=3.0.0) -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) +### Setup -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +1. Clone the repository and navigate to the example directory: + ```bash + cd example + ``` + +2. Install dependencies: + ```bash + flutter pub get + ``` + +3. Run the app: + ```bash + flutter run + ``` + +## Architecture + +The app uses: + +- **freeRASP** for security monitoring +- **Riverpod** for state management +- **Material Design 3** with dynamic color theming + +## Resources + +- [freeRASP Documentation](https://github.com/talsec/freerasp) +- [Flutter Documentation](https://flutter.dev/docs) +- [Riverpod Documentation](https://riverpod.dev) diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 7b81027..82b3ad3 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,5 +1,11 @@ include: package:very_good_analysis/analysis_options.yaml +linter: + rules: + public_member_api_docs: false + analyzer: + exclude: + - '**/*.g.dart' errors: document_ignores: false \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 3363e13..f1c04f2 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -23,7 +23,7 @@ if (flutterVersionName == null) { } android { - namespace 'com.aheaditec.freerasp_example' + namespace 'app.talsec.demo.freerasp' compileSdkVersion 35 ndkVersion = "27.1.12297006" @@ -42,9 +42,9 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.aheaditec.freerasp_example" + applicationId "app.talsec.demo.freerasp" // Talsec library needs higher version than default (16) - minSdkVersion 23 + minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index 5070f0f..0f30652 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="app.talsec.demo.freerasp"> diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index f700a23..d986779 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="app.talsec.demo.freerasp"> diff --git a/example/android/app/src/main/kotlin/com/aheaditec/freerasp_example/MainActivity.kt b/example/android/app/src/main/kotlin/app/talsec/demo/freerasp/MainActivity.kt similarity index 71% rename from example/android/app/src/main/kotlin/com/aheaditec/freerasp_example/MainActivity.kt rename to example/android/app/src/main/kotlin/app/talsec/demo/freerasp/MainActivity.kt index 8108737..7fef11f 100644 --- a/example/android/app/src/main/kotlin/com/aheaditec/freerasp_example/MainActivity.kt +++ b/example/android/app/src/main/kotlin/app/talsec/demo/freerasp/MainActivity.kt @@ -1,4 +1,4 @@ -package com.aheaditec.freerasp_example +package app.talsec.demo.freerasp import io.flutter.embedding.android.FlutterActivity diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml index 5070f0f..0f30652 100644 --- a/example/android/app/src/profile/AndroidManifest.xml +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="app.talsec.demo.freerasp"> diff --git a/example/lib/app/app.dart b/example/lib/app/app.dart new file mode 100644 index 0000000..3b7d56a --- /dev/null +++ b/example/lib/app/app.dart @@ -0,0 +1,31 @@ +// ignore_for_file: public_member_api_docs +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freerasp_example/screens/security_screen.dart'; +import 'package:freerasp_example/theme/app_theme.dart'; + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + child: DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { + final lightScheme = + AppTheme.getScheme(lightDynamic, Brightness.light); + final darkScheme = AppTheme.getScheme(darkDynamic, Brightness.dark); + + return MaterialApp( + restorationScopeId: 'root', + title: 'My Secure App', + theme: AppTheme.create(lightScheme), + darkTheme: AppTheme.create(darkScheme), + home: const SecurityScreen(), + ); + }, + ), + ); + } +} diff --git a/example/lib/config/talsec_config.dart b/example/lib/config/talsec_config.dart new file mode 100644 index 0000000..cd0e1dc --- /dev/null +++ b/example/lib/config/talsec_config.dart @@ -0,0 +1,27 @@ +import 'package:freerasp/freerasp.dart'; + +/// Creates and returns the Talsec configuration for the example app. +/// +/// This configuration is used to initialize Talsec with Android and iOS +/// settings. +TalsecConfig createTalsecConfig() { + return TalsecConfig( + androidConfig: AndroidConfig( + packageName: 'app.talsec.freerasp.app', + signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + supportedStores: ['com.sec.android.app.samsungapps'], + malwareConfig: MalwareConfig( + blacklistedPackageNames: ['com.google.android.youtube'], + suspiciousPermissions: [ + ['android.permission.CAMERA'], + ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'], + ], + ), + ), + iosConfig: IOSConfig( + bundleIds: ['com.aheaditec.freeraspExample'], + teamId: 'M8AK35...', + ), + watcherMail: 'your_mail@example.com', + ); +} diff --git a/example/lib/extensions.dart b/example/lib/extensions.dart deleted file mode 100644 index 0636a49..0000000 --- a/example/lib/extensions.dart +++ /dev/null @@ -1,17 +0,0 @@ -/// Extensions for the `String` class. -extension StringX on String { - /// Converts the first character of the string to uppercase. - /// - /// If the string is empty, returns an empty string. - /// - /// If the string has only one character, returns the uppercase version of - /// the character. - /// - /// Otherwise, returns the string with the first character converted to - /// uppercase. - String capitalize() { - if (isEmpty) return ''; - if (length == 1) return toUpperCase(); - return this[0].toUpperCase() + substring(1); - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart index ab9ec32..ea8ce05 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,157 +1,15 @@ -// ignore_for_file: public_member_api_docs, avoid_redundant_argument_values - import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freerasp/freerasp.dart'; -import 'package:freerasp_example/screen_capture_notifier.dart'; -import 'package:freerasp_example/threat_notifier.dart'; -import 'package:freerasp_example/threat_state.dart'; -import 'package:freerasp_example/widgets/widgets.dart'; +import 'package:freerasp_example/app/app.dart'; +import 'package:freerasp_example/config/talsec_config.dart'; import 'package:permission_handler/permission_handler.dart'; -/// Represents current state of the threats detectable by freeRASP -final threatProvider = - NotifierProvider.autoDispose(() { - return ThreatNotifier(); -}); - -final screenCaptureProvider = - AsyncNotifierProvider.autoDispose(() { - return ScreenCaptureNotifier(); -}); - Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await Permission.locationWhenInUse.request(); - /// Initialize Talsec config - await _initializeTalsec(); - - runApp(const ProviderScope(child: App())); -} - -/// Initialize Talsec configuration for Android and iOS -Future _initializeTalsec() async { - final config = TalsecConfig( - androidConfig: AndroidConfig( - packageName: 'com.aheaditec.freeraspExample', - signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], - supportedStores: ['com.sec.android.app.samsungapps'], - malwareConfig: MalwareConfig( - blacklistedPackageNames: ['com.aheaditec.freeraspExample'], - suspiciousPermissions: [ - ['android.permission.CAMERA'], - ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'], - ], - ), - ), - iosConfig: IOSConfig( - bundleIds: ['com.aheaditec.freeraspExample'], - teamId: 'M8AK35...', - ), - watcherMail: 'your_mail@example.com', - isProd: true, - ); - + final config = createTalsecConfig(); await Talsec.instance.start(config); -} - -/// Example of how to use [Talsec.storeExternalId]. -Future testStoreExternalId(String data) async { - await Talsec.instance.storeExternalId(data); -} - -/// The root widget of the application -class App extends StatelessWidget { - const App({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData(primarySwatch: Colors.blue), - home: const HomePage(), - ); - } -} - -/// The home page that displays the threats and results -class HomePage extends ConsumerWidget { - const HomePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final threatState = ref.watch(threatProvider); - - // Listen for changes in the threatProvider and show the malware modal - ref.listen(threatProvider, (prev, next) { - if (prev?.detectedMalware != next.detectedMalware) { - _showMalwareBottomSheet(context, next.detectedMalware); - } - }); - - return Scaffold( - appBar: AppBar(title: const Text('freeRASP Demo')), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - Text( - 'Threat Status', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - ListTile( - title: const Text('Store External ID'), - trailing: IconButton( - icon: const Icon(Icons.refresh), - onPressed: () { - testStoreExternalId('testData'); - }, - ), - ), - ListTile( - title: const Text('Change Screen Capture'), - leading: SafetyIcon( - isDetected: !(ref.watch(screenCaptureProvider).value ?? true), - ), - trailing: IconButton( - icon: const Icon(Icons.refresh), - onPressed: () { - ref.read(screenCaptureProvider.notifier).toggle(); - }, - ), - ), - ListTile( - title: const Text('Check Rounds Completed'), - trailing: SafetyIcon(isDetected: !threatState.allChecksPassed), - ), - Expanded( - child: ThreatListView(threats: threatState.detectedThreats), - ), - ], - ), - ), - ), - ); - } -} -/// Extension method to show the malware bottom sheet -void _showMalwareBottomSheet( - BuildContext context, - List suspiciousApps, -) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showModalBottomSheet( - context: context, - isDismissible: false, - enableDrag: false, - builder: (BuildContext context) => MalwareBottomSheet( - suspiciousApps: suspiciousApps, - ), - ); - }); + runApp(const MyApp()); } diff --git a/example/lib/models/security_check.dart b/example/lib/models/security_check.dart new file mode 100644 index 0000000..5247e03 --- /dev/null +++ b/example/lib/models/security_check.dart @@ -0,0 +1,60 @@ +import 'package:freerasp/freerasp.dart'; + +/// Categories of threats. +enum ThreatCategory { + /// Threats related to the application's integrity and source. + appIntegrity, + + /// Threats related to the device's environment and security settings. + deviceSecurity, + + /// Threats related to user actions or runtime events. + runtimeStatus, +} + +/// Represents a security check with metadata. +class SecurityCheck { + /// Creates a [SecurityCheck]. + SecurityCheck({ + required this.threat, + required this.name, + required this.secureDescription, + required this.insecureDescription, + required this.category, + this.isSecure = true, + }); + + /// The specific threat being checked. + final Threat threat; + + /// Human-readable name of the threat. + final String name; + + /// Description shown when the check is secure. + final String secureDescription; + + /// Description shown when the check is insecure. + final String insecureDescription; + + /// The category this threat belongs to. + final ThreatCategory category; + + /// Whether the check is currently passing (secure). + bool isSecure; + + /// Returns the appropriate description based on security status. + String get description => isSecure ? secureDescription : insecureDescription; + + SecurityCheck copyWith({ + bool? isSecure, + }) { + return SecurityCheck( + threat: threat, + name: name, + secureDescription: secureDescription, + insecureDescription: insecureDescription, + category: category, + isSecure: isSecure ?? this.isSecure, + ); + } +} diff --git a/example/lib/models/security_state.dart b/example/lib/models/security_state.dart new file mode 100644 index 0000000..2a7f5b3 --- /dev/null +++ b/example/lib/models/security_state.dart @@ -0,0 +1,52 @@ +import 'package:freerasp/freerasp.dart'; +import 'package:freerasp_example/models/security_check.dart'; + +/// State class for security checks managed by Riverpod. +class SecurityState { + /// Creates a [SecurityState]. + SecurityState({ + required this.checks, + this.detectedMalware = const [], + }); + + /// Creates an initial state with all checks initialized. + factory SecurityState.initial(List checks) { + return SecurityState(checks: checks); + } + + /// List of all security checks. + final List checks; + + /// List of detected suspicious apps. + final List detectedMalware; + + /// Creates a copy of this state with updated checks. + SecurityState copyWith({ + List? checks, + List? detectedMalware, + }) { + return SecurityState( + checks: checks ?? this.checks, + detectedMalware: detectedMalware ?? this.detectedMalware, + ); + } + + /// Groups checks by category. + Map> get checksByCategory { + final map = >{}; + for (final check in checks) { + map.putIfAbsent(check.category, () => []).add(check); + } + return map; + } + + /// Returns true if all checks are secure. + bool get isAllSecure { + return checks.every((check) => check.isSecure); + } + + /// Returns true if any malware is detected. + bool get hasMalware { + return detectedMalware.isNotEmpty; + } +} diff --git a/example/lib/providers/external_id_provider.dart b/example/lib/providers/external_id_provider.dart new file mode 100644 index 0000000..74ffdcd --- /dev/null +++ b/example/lib/providers/external_id_provider.dart @@ -0,0 +1,23 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freerasp/freerasp.dart'; + +class ExternalIdNotifier extends StateNotifier { + ExternalIdNotifier() : super(null); + + Future setExternalId(String id) async { + try { + await Talsec.instance.storeExternalId(id); + state = id; + } catch (e) { + rethrow; + } + } + + void clearExternalId() { + state = null; + } +} + +final externalIdProvider = StateNotifierProvider( + (ref) => ExternalIdNotifier(), +); diff --git a/example/lib/screen_capture_notifier.dart b/example/lib/providers/screen_capture_provider.dart similarity index 76% rename from example/lib/screen_capture_notifier.dart rename to example/lib/providers/screen_capture_provider.dart index ba0cc0b..0fa1f77 100644 --- a/example/lib/screen_capture_notifier.dart +++ b/example/lib/providers/screen_capture_provider.dart @@ -1,6 +1,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freerasp/freerasp.dart'; +/// Provider for screen capture blocking functionality +final screenCaptureProvider = + AsyncNotifierProvider.autoDispose(() { + return ScreenCaptureNotifier(); +}); + /// Class responsible for triggering screen capture blocking class ScreenCaptureNotifier extends AutoDisposeAsyncNotifier { @override diff --git a/example/lib/providers/security_provider.dart b/example/lib/providers/security_provider.dart new file mode 100644 index 0000000..b7b12a7 --- /dev/null +++ b/example/lib/providers/security_provider.dart @@ -0,0 +1,215 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freerasp/freerasp.dart'; +import 'package:freerasp_example/models/security_check.dart'; +import 'package:freerasp_example/models/security_state.dart'; + +/// Provider for the security controller that manages all security checks. +final securityControllerProvider = + NotifierProvider.autoDispose( + SecurityController.new, +); + +class SecurityController extends AutoDisposeNotifier { + /// Initializes the security controller and starts monitoring. + @override + SecurityState build() { + final checks = _initChecks(); + Future.microtask(_startListening); + + ref.onDispose(() { + Talsec.instance.detachListener(); + }); + + return SecurityState.initial(checks); + } + + List _initChecks() { + return [ + // App Integrity + SecurityCheck( + threat: Threat.appIntegrity, + name: 'App Integrity', + secureDescription: 'Application signature is verified and intact.', + insecureDescription: 'Application signature verification failed.', + category: ThreatCategory.appIntegrity, + ), + SecurityCheck( + threat: Threat.obfuscationIssues, + name: 'Obfuscation', + secureDescription: 'Application code is properly obfuscated.', + insecureDescription: 'Application code is not obfuscated.', + category: ThreatCategory.appIntegrity, + ), + SecurityCheck( + threat: Threat.unofficialStore, + name: 'Unofficial Store', + secureDescription: 'Application installed from an official store.', + insecureDescription: 'Application installed from an unknown source.', + category: ThreatCategory.appIntegrity, + ), + SecurityCheck( + threat: Threat.simulator, + name: 'Simulator', + secureDescription: 'Running on a real device.', + insecureDescription: 'Running on a simulator or emulator.', + category: ThreatCategory.appIntegrity, + ), + SecurityCheck( + threat: Threat.deviceBinding, + name: 'Device Binding', + secureDescription: 'Application properly bound to the device.', + insecureDescription: 'Device binding compromised.', + category: ThreatCategory.appIntegrity, + ), + SecurityCheck( + threat: Threat.multiInstance, + name: 'Multi Instance', + secureDescription: 'Single application instance running.', + insecureDescription: 'Multiple application instances detected.', + category: ThreatCategory.appIntegrity, + ), + + // Device Security + SecurityCheck( + threat: Threat.privilegedAccess, + name: 'Root / Jailbreak', + secureDescription: 'System is running securely (standard environment).', + insecureDescription: 'Privileged access (Root/Jailbreak) detected.', + category: ThreatCategory.deviceSecurity, + ), + SecurityCheck( + threat: Threat.hooks, + name: 'Hooks', + secureDescription: 'No hooking frameworks detected.', + insecureDescription: 'Hooking framework detected.', + category: ThreatCategory.deviceSecurity, + ), + SecurityCheck( + threat: Threat.secureHardwareNotAvailable, + name: 'Secure Hardware', + secureDescription: 'Secure hardware available.', + insecureDescription: 'Secure hardware unavailable.', + category: ThreatCategory.deviceSecurity, + ), + SecurityCheck( + threat: Threat.devMode, + name: 'Developer Mode', + secureDescription: 'Developer options are disabled.', + insecureDescription: 'Developer options are enabled.', + category: ThreatCategory.deviceSecurity, + ), + SecurityCheck( + threat: Threat.debug, + name: 'Debugger', + secureDescription: 'No debugger attached.', + insecureDescription: 'Debugger is attached.', + category: ThreatCategory.deviceSecurity, + ), + SecurityCheck( + threat: Threat.passcode, + name: 'Passcode', + secureDescription: 'Device is protected with a passcode.', + insecureDescription: 'Device is not protected with a passcode.', + category: ThreatCategory.deviceSecurity, + ), + SecurityCheck( + threat: Threat.adbEnabled, + name: 'ADB Enabled', + secureDescription: 'USB debugging (ADB) is disabled.', + insecureDescription: 'USB debugging (ADB) is enabled.', + category: ThreatCategory.deviceSecurity, + ), + + SecurityCheck( + threat: Threat.systemVPN, + name: 'System VPN', + secureDescription: 'No VPN active.', + insecureDescription: 'Network traffic is routed via VPN.', + category: ThreatCategory.deviceSecurity, + ), + SecurityCheck( + threat: Threat.screenshot, + name: 'Screenshot', + secureDescription: 'No screenshots detected.', + insecureDescription: 'Screenshot detected.', + category: ThreatCategory.runtimeStatus, + ), + SecurityCheck( + threat: Threat.screenRecording, + name: 'Screen Recording', + secureDescription: 'No screen recording detected.', + insecureDescription: 'Screen recording detected.', + category: ThreatCategory.runtimeStatus, + ), + SecurityCheck( + threat: Threat.locationSpoofing, + name: 'Location Spoofing', + secureDescription: 'Device location is valid.', + insecureDescription: 'Device location is being manipulated.', + category: ThreatCategory.runtimeStatus, + ), + SecurityCheck( + threat: Threat.timeSpoofing, + name: 'Time Spoofing', + secureDescription: 'Device time is correct.', + insecureDescription: 'Device time is out of sync.', + category: ThreatCategory.runtimeStatus, + ), + SecurityCheck( + threat: Threat.unsecureWiFi, + name: 'Unsecure Wi-Fi', + secureDescription: 'Connected to a secure Wi-Fi network.', + insecureDescription: 'Connected to an unsecure Wi-Fi network.', + category: ThreatCategory.runtimeStatus, + ), + ]; + } + + Future _startListening() async { + final threatCallback = ThreatCallback( + onAppIntegrity: () => _handleThreat(Threat.appIntegrity), + onObfuscationIssues: () => _handleThreat(Threat.obfuscationIssues), + onUnofficialStore: () => _handleThreat(Threat.unofficialStore), + onPrivilegedAccess: () => _handleThreat(Threat.privilegedAccess), + onDeviceBinding: () => _handleThreat(Threat.deviceBinding), + onSimulator: () => _handleThreat(Threat.simulator), + onHooks: () => _handleThreat(Threat.hooks), + onDebug: () => _handleThreat(Threat.debug), + onScreenRecording: () => _handleThreat(Threat.screenRecording), + onScreenshot: () => _handleThreat(Threat.screenshot), + onSecureHardwareNotAvailable: () => + _handleThreat(Threat.secureHardwareNotAvailable), + onSystemVPN: () => _handleThreat(Threat.systemVPN), + onDeviceID: () => _handleThreat(Threat.deviceId), + onPasscode: () => _handleThreat(Threat.passcode), + onADBEnabled: () => _handleThreat(Threat.adbEnabled), + onDevMode: () => _handleThreat(Threat.devMode), + onMultiInstance: () => _handleThreat(Threat.multiInstance), + onUnsecureWiFi: () => _handleThreat(Threat.unsecureWiFi), + onTimeSpoofing: () => _handleThreat(Threat.timeSpoofing), + onLocationSpoofing: () => _handleThreat(Threat.locationSpoofing), + onMalware: _handleMalware, + ); + + await Talsec.instance.attachListener(threatCallback); + } + + void _handleThreat(Threat type) { + final currentState = state; + final checks = List.from(currentState.checks); + final index = checks.indexWhere((c) => c.threat == type); + + if (index != -1 && checks[index].isSecure) { + checks[index] = checks[index].copyWith(isSecure: false); + state = currentState.copyWith(checks: checks); + } + } + + void _handleMalware(List malware) { + final currentState = state; + final malwareList = malware.whereType().toList(); + state = currentState.copyWith(detectedMalware: malwareList); + } +} diff --git a/example/lib/screens/security_screen.dart b/example/lib/screens/security_screen.dart new file mode 100644 index 0000000..0217a6e --- /dev/null +++ b/example/lib/screens/security_screen.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freerasp_example/models/security_check.dart'; +import 'package:freerasp_example/providers/security_provider.dart'; +import 'package:freerasp_example/widgets/common/titled_section.dart'; +import 'package:freerasp_example/widgets/security/malware_alert_card.dart'; +import 'package:freerasp_example/widgets/security/security_check_list.dart'; +import 'package:freerasp_example/widgets/security/security_status_card.dart'; +import 'package:freerasp_example/widgets/settings/settings_group.dart'; +import 'package:freerasp_example/widgets/settings/tiles/external_id_tile.dart'; +import 'package:freerasp_example/widgets/settings/tiles/screen_capture_tile.dart'; + +class SecurityScreen extends ConsumerWidget { + const SecurityScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final securityState = ref.watch(securityControllerProvider); + final checksByCategory = securityState.checksByCategory; + + return Scaffold( + appBar: AppBar(), + body: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + spacing: 12, + children: [ + const SecurityStatusCard(), + const MalwareAlertCard(), + TitledSection( + title: 'App Integrity', + child: SecurityCheckList( + checks: checksByCategory[ThreatCategory.appIntegrity] ?? [], + ), + ), + TitledSection( + title: 'Device Security', + child: SecurityCheckList( + checks: checksByCategory[ThreatCategory.deviceSecurity] ?? [], + ), + ), + TitledSection( + title: 'Runtime Status', + child: SecurityCheckList( + checks: checksByCategory[ThreatCategory.runtimeStatus] ?? [], + ), + ), + const TitledSection( + title: 'Other Settings', + child: SettingsGroup( + items: [ + ScreenCaptureTile(), + ExternalIdTile(), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/screens/suspicious_apps_screen.dart b/example/lib/screens/suspicious_apps_screen.dart new file mode 100644 index 0000000..6b2b6b5 --- /dev/null +++ b/example/lib/screens/suspicious_apps_screen.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freerasp/freerasp.dart'; +import 'package:freerasp_example/providers/security_provider.dart'; + +class SuspiciousAppsScreen extends ConsumerWidget { + const SuspiciousAppsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final securityState = ref.watch(securityControllerProvider); + final suspiciousApps = securityState.detectedMalware; + final textTheme = Theme.of(context).textTheme; + const radius = 16.0; + + return Scaffold( + appBar: AppBar( + title: const Text('Suspicious Apps'), + ), + body: suspiciousApps.isEmpty + ? Center( + child: Text( + 'No suspicious apps detected', + style: textTheme.bodyLarge, + ), + ) + : Padding( + padding: const EdgeInsets.all(16), + child: ListView.separated( + itemCount: suspiciousApps.length, + itemBuilder: (context, index) { + final app = suspiciousApps[index]; + final isFirst = index == 0; + final isLast = index == suspiciousApps.length - 1; + + return ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: isFirst + ? const Radius.circular(radius) + : Radius.zero, + bottom: isLast + ? const Radius.circular(radius) + : Radius.zero, + ), + ), + leading: Padding( + padding: const EdgeInsets.all(8), + child: FutureBuilder( + future: Talsec.instance.getAppIcon( + app.packageInfo.packageName, + ), + builder: (context, snapshot) { + Widget appIcon; + if (snapshot.hasData && snapshot.data != null) { + try { + appIcon = ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.memory( + base64.decode(snapshot.data!), + width: 32, + height: 32, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.android, + size: 32, + ); + }, + ), + ); + } catch (e) { + appIcon = const Icon( + Icons.android, + size: 32, + ); + } + } else { + appIcon = const Icon( + Icons.android, + size: 32, + ); + } + + return appIcon; + }, + ), + ), + title: Text(app.packageInfo.packageName), + subtitle: Text('Reason: ${app.reason}'), + ); + }, + separatorBuilder: (ctx, i) => const SizedBox(height: 2), + ), + ), + ); + } +} diff --git a/example/lib/theme/app_theme.dart b/example/lib/theme/app_theme.dart new file mode 100644 index 0000000..541167c --- /dev/null +++ b/example/lib/theme/app_theme.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppTheme { + // Default to Talsec company colours + static const _fallbackSeed = Color(0xFF8df6a2); + static const _fallbackSurface = Color(0xFF191B24); + + static ColorScheme getScheme( + ColorScheme? dynamicScheme, + Brightness brightness, + ) { + if (dynamicScheme != null) { + return ColorScheme.fromSeed( + seedColor: dynamicScheme.primary, + brightness: brightness, + ); + } + + return ColorScheme.fromSeed( + seedColor: _fallbackSeed, + brightness: brightness, + surface: _fallbackSurface, + ); + } + + static ThemeData create(ColorScheme scheme) { + return ThemeData( + useMaterial3: true, + colorScheme: scheme, + scaffoldBackgroundColor: scheme.surface, + + // Font setup + textTheme: GoogleFonts.openSansTextTheme(), + + // Component Styles + listTileTheme: ListTileThemeData( + tileColor: scheme.surfaceContainerHigh, + titleTextStyle: GoogleFonts.openSans( + fontSize: 16, + fontWeight: FontWeight.w600, + color: scheme.onSurface, + ), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + ), + cardTheme: CardThemeData( + color: scheme.surfaceContainerHigh, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ); + } +} diff --git a/example/lib/threat_notifier.dart b/example/lib/threat_notifier.dart deleted file mode 100644 index 927dc6d..0000000 --- a/example/lib/threat_notifier.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freerasp/freerasp.dart'; -import 'package:freerasp_example/threat_state.dart'; - -/// Class responsible for setting up listeners to detected threats -class ThreatNotifier extends AutoDisposeNotifier { - @override - ThreatState build() { - _init(); - return ThreatState.initial(); - } - - void _init() { - final threatCallback = ThreatCallback( - onMalware: _updateMalware, - onHooks: () => _updateThreat(Threat.hooks), - onDebug: () => _updateThreat(Threat.debug), - onPasscode: () => _updateThreat(Threat.passcode), - onDeviceID: () => _updateThreat(Threat.deviceId), - onSimulator: () => _updateThreat(Threat.simulator), - onAppIntegrity: () => _updateThreat(Threat.appIntegrity), - onObfuscationIssues: () => _updateThreat(Threat.obfuscationIssues), - onDeviceBinding: () => _updateThreat(Threat.deviceBinding), - onUnofficialStore: () => _updateThreat(Threat.unofficialStore), - onPrivilegedAccess: () => _updateThreat(Threat.privilegedAccess), - onSecureHardwareNotAvailable: () => - _updateThreat(Threat.secureHardwareNotAvailable), - onSystemVPN: () => _updateThreat(Threat.systemVPN), - onDevMode: () => _updateThreat(Threat.devMode), - onADBEnabled: () => _updateThreat(Threat.adbEnabled), - onScreenshot: () => _updateThreat(Threat.screenshot), - onScreenRecording: () => _updateThreat(Threat.screenRecording), - onMultiInstance: () => _updateThreat(Threat.multiInstance), - onUnsecureWiFi: () => _updateThreat(Threat.unsecureWiFi), - onTimeSpoofing: () => _updateThreat(Threat.timeSpoofing), - onLocationSpoofing: () => _updateThreat(Threat.locationSpoofing), - ); - - final raspExecutionStateCallback = - RaspExecutionStateCallback(onAllChecksDone: _updateChecksStatus); - - Talsec.instance.attachListener(threatCallback); - Talsec.instance.attachExecutionStateListener(raspExecutionStateCallback); - } - - void _updateThreat(Threat threat) { - state = state.copyWith(detectedThreats: {...state.detectedThreats, threat}); - } - - void _updateMalware(List malware) { - state = state.copyWith(detectedMalware: malware.nonNulls.toList()); - } - - void _updateChecksStatus() { - state = state.copyWith(allChecksPassed: true); - } -} diff --git a/example/lib/threat_state.dart b/example/lib/threat_state.dart deleted file mode 100644 index 85efba0..0000000 --- a/example/lib/threat_state.dart +++ /dev/null @@ -1,34 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:freerasp/freerasp.dart'; - -class ThreatState { - factory ThreatState.initial() => const ThreatState._( - detectedThreats: {}, - detectedMalware: [], - allChecksPassed: false, - ); - - const ThreatState._({ - required this.detectedThreats, - required this.detectedMalware, - required this.allChecksPassed, - }); - - final Set detectedThreats; - final List detectedMalware; - final bool allChecksPassed; - - ThreatState copyWith({ - Set? detectedThreats, - List? detectedMalware, - bool? allChecksPassed, - }) { - return ThreatState._( - detectedThreats: detectedThreats ?? this.detectedThreats, - detectedMalware: - detectedMalware?.nonNulls.toList() ?? this.detectedMalware, - allChecksPassed: allChecksPassed ?? this.allChecksPassed, - ); - } -} diff --git a/example/lib/widgets/common/grouped_list_item.dart b/example/lib/widgets/common/grouped_list_item.dart new file mode 100644 index 0000000..dbf6839 --- /dev/null +++ b/example/lib/widgets/common/grouped_list_item.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +class GroupedListItem extends StatelessWidget { + const GroupedListItem({ + required this.title, + required this.subtitle, + this.leading, + this.trailing, + this.onTap, + super.key, + }); + + final String title; + final String subtitle; + final Widget? leading; + final Widget? trailing; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return ListTile( + shape: const RoundedRectangleBorder(), + leading: leading != null + ? Padding( + padding: const EdgeInsets.all(8), + child: leading, + ) + : null, + title: Text(title), + subtitle: Text(subtitle), + trailing: trailing, + onTap: onTap, + ); + } +} diff --git a/example/lib/widgets/common/titled_section.dart b/example/lib/widgets/common/titled_section.dart new file mode 100644 index 0000000..2fd5570 --- /dev/null +++ b/example/lib/widgets/common/titled_section.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class TitledSection extends StatelessWidget { + const TitledSection({ + required this.title, + required this.child, + super.key, + }); + + final String title; + final Widget child; + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8), + child: Text( + title, + style: textTheme.labelLarge?.copyWith(color: colors.primary), + ), + ), + child, + ], + ); + } +} diff --git a/example/lib/widgets/malware_bottom_sheet.dart b/example/lib/widgets/malware_bottom_sheet.dart deleted file mode 100644 index 04abf12..0000000 --- a/example/lib/widgets/malware_bottom_sheet.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:freerasp/freerasp.dart'; - -/// Bottom sheet widget that displays malware information -class MalwareBottomSheet extends StatelessWidget { - /// Represents malware information in the example app - const MalwareBottomSheet({ - required this.suspiciousApps, - super.key, - }); - - /// List of suspicious apps - final List suspiciousApps; - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - - return Container( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Suspicious Apps', style: textTheme.titleMedium), - const SizedBox(height: 8), - SizedBox( - height: 240, - child: ListView.builder( - itemCount: suspiciousApps.length, - itemBuilder: (_, index) => - MalwareListTile(malware: suspiciousApps[index]), - ), - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: () => Navigator.pop(context), - child: const Text('Dismiss'), - ), - ), - ], - ), - ); - } -} - -/// List tile widget that displays malware information -class MalwareListTile extends StatelessWidget { - /// Represents malware information in the example app - const MalwareListTile({ - required this.malware, - super.key, - }); - - /// Malware information - final SuspiciousAppInfo malware; - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: Talsec.instance.getAppIcon(malware.packageInfo.packageName), - builder: (context, snapshot) { - Widget appIcon; - if (snapshot.data != null) { - appIcon = Image.memory(base64.decode(snapshot.data!)); - } else { - appIcon = const Icon( - Icons.error, - color: Colors.red, - ); - } - - return ListTile( - title: Text(malware.packageInfo.packageName), - subtitle: Text('Reason: ${malware.reason}'), - leading: appIcon, - ); - }, - ); - } -} diff --git a/example/lib/widgets/safety_icon.dart b/example/lib/widgets/safety_icon.dart deleted file mode 100644 index 2e00baf..0000000 --- a/example/lib/widgets/safety_icon.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Class responsible for changing threat icon and style in the example app -class SafetyIcon extends StatelessWidget { - /// Represents security state icon in the example app - const SafetyIcon({required this.isDetected, super.key}); - - /// Determines whether given threat was detected - final bool isDetected; - - @override - Widget build(BuildContext context) { - return isDetected - ? const Icon(Icons.gpp_bad_outlined, color: Colors.red, size: 32) - : const Icon(Icons.gpp_good_outlined, color: Colors.green, size: 32); - } -} diff --git a/example/lib/widgets/security/malware_alert_card.dart b/example/lib/widgets/security/malware_alert_card.dart new file mode 100644 index 0000000..4ec3e7a --- /dev/null +++ b/example/lib/widgets/security/malware_alert_card.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freerasp_example/providers/security_provider.dart'; +import 'package:freerasp_example/screens/suspicious_apps_screen.dart'; + +class MalwareAlertCard extends ConsumerStatefulWidget { + const MalwareAlertCard({super.key}); + + @override + ConsumerState createState() => _MalwareAlertCardState(); +} + +class _MalwareAlertCardState extends ConsumerState { + @override + Widget build(BuildContext context) { + final securityState = ref.watch(securityControllerProvider); + final hasMalware = securityState.hasMalware; + final malwareCount = securityState.detectedMalware.length; + final colors = Theme.of(context).colorScheme; + + if (!hasMalware) { + return const SizedBox.shrink(); + } + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Malware security', + style: TextStyle( + fontSize: 14, + color: colors.onSurfaceVariant, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + 'You have security recommendations', + style: TextStyle( + fontSize: 22, + height: 1.2, + color: colors.onSurface, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 8), + Text( + malwareCount == 1 + ? '$malwareCount suspicious app detected' + : '$malwareCount suspicious apps detected', + style: TextStyle( + color: colors.onSurfaceVariant, + fontSize: 14, + ), + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + height: 50, + child: FilledButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const SuspiciousAppsScreen(), + ), + ); + }, + child: const Text('See suspicious apps'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/widgets/security/security_check_list.dart b/example/lib/widgets/security/security_check_list.dart new file mode 100644 index 0000000..2b2d5bd --- /dev/null +++ b/example/lib/widgets/security/security_check_list.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:freerasp_example/models/security_check.dart'; +import 'package:freerasp_example/widgets/common/grouped_list_item.dart'; + +class SecurityCheckList extends StatelessWidget { + const SecurityCheckList({ + required this.checks, + super.key, + }); + + final List checks; + + @override + Widget build(BuildContext context) { + if (checks.isEmpty) { + return const SizedBox.shrink(); + } + + return Material( + type: MaterialType.transparency, + borderRadius: BorderRadius.circular(16), + clipBehavior: Clip.antiAlias, + child: Column( + spacing: 2, + children: checks.map((item) { + return GroupedListItem( + title: item.name, + subtitle: item.description, + leading: Icon( + item.isSecure ? Icons.check_circle : Icons.error, + color: item.isSecure ? Colors.green : Colors.amber, + ), + ); + }).toList(), + ), + ); + } +} diff --git a/example/lib/widgets/security/security_status_card.dart b/example/lib/widgets/security/security_status_card.dart new file mode 100644 index 0000000..ffc5e19 --- /dev/null +++ b/example/lib/widgets/security/security_status_card.dart @@ -0,0 +1,43 @@ +import 'package:avatar_glow/avatar_glow.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freerasp_example/providers/security_provider.dart'; + +class SecurityStatusCard extends ConsumerWidget { + const SecurityStatusCard({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final securityState = ref.watch(securityControllerProvider); + final isSecure = securityState.isAllSecure; + + return Padding( + padding: const EdgeInsets.all(4), + child: ListTile( + contentPadding: const EdgeInsets.all(20), + leading: AvatarGlow( + glowColor: isSecure ? Colors.green : Colors.orange, + glowRadiusFactor: 0.4, + duration: const Duration(seconds: 1), + glowCount: 1, + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + Icons.security, + color: isSecure ? Colors.green : Colors.orange, + size: 32, + ), + ), + ), + title: Text( + isSecure ? 'Device is Secure' : 'Device may be compromised', + ), + subtitle: Text( + isSecure + ? 'Checking security status...' + : 'See alerts for more details', + ), + ), + ); + } +} diff --git a/example/lib/widgets/settings/settings_group.dart b/example/lib/widgets/settings/settings_group.dart new file mode 100644 index 0000000..44cef6d --- /dev/null +++ b/example/lib/widgets/settings/settings_group.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class SettingsGroup extends StatelessWidget { + const SettingsGroup({ + required this.items, + super.key, + }); + + final List items; + + @override + Widget build(BuildContext context) { + if (items.isEmpty) { + return const SizedBox.shrink(); + } + + return Material( + type: MaterialType.transparency, + borderRadius: BorderRadius.circular(16), + clipBehavior: Clip.antiAlias, + child: Column( + spacing: 2, + children: items, + ), + ); + } +} diff --git a/example/lib/widgets/settings/tiles/external_id_tile.dart b/example/lib/widgets/settings/tiles/external_id_tile.dart new file mode 100644 index 0000000..e2c8231 --- /dev/null +++ b/example/lib/widgets/settings/tiles/external_id_tile.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freerasp_example/providers/external_id_provider.dart'; +import 'package:freerasp_example/widgets/common/grouped_list_item.dart'; + +class ExternalIdTile extends ConsumerWidget { + const ExternalIdTile({ + this.title = 'External ID', + super.key, + }); + + final String title; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final externalId = ref.watch(externalIdProvider); + final subtitle = externalId ?? 'Not set'; + + return GroupedListItem( + title: title, + subtitle: subtitle, + trailing: const Icon(Icons.chevron_right), + onTap: () => _showExternalIdDialog(context, ref), + ); + } + + void _showExternalIdDialog( + BuildContext context, + WidgetRef ref, + ) { + final currentId = ref.read(externalIdProvider); + final controller = TextEditingController(text: currentId ?? ''); + + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(title), + content: TextField( + controller: controller, + decoration: const InputDecoration( + labelText: 'External ID', + hintText: 'Enter device identifier', + border: OutlineInputBorder(), + ), + autofocus: true, + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () async { + final newId = controller.text.trim(); + if (newId.isNotEmpty) { + try { + await ref + .read(externalIdProvider.notifier) + .setExternalId(newId); + if (dialogContext.mounted) { + Navigator.of(dialogContext).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('External ID saved successfully'), + duration: Duration(seconds: 2), + ), + ); + } + } catch (e) { + if (dialogContext.mounted) { + ScaffoldMessenger.of(dialogContext).showSnackBar( + SnackBar( + content: Text('Error saving external ID: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } else { + // Clear the external ID if empty + ref.read(externalIdProvider.notifier).clearExternalId(); + if (dialogContext.mounted) { + Navigator.of(dialogContext).pop(); + } + } + }, + child: const Text('Save'), + ), + ], + ), + ); + } +} diff --git a/example/lib/widgets/settings/tiles/screen_capture_tile.dart b/example/lib/widgets/settings/tiles/screen_capture_tile.dart new file mode 100644 index 0000000..8371def --- /dev/null +++ b/example/lib/widgets/settings/tiles/screen_capture_tile.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freerasp_example/providers/screen_capture_provider.dart'; +import 'package:freerasp_example/widgets/common/grouped_list_item.dart'; + +class ScreenCaptureTile extends ConsumerWidget { + const ScreenCaptureTile({ + this.title = 'Block Screen Capture', + this.subtitle = 'Prevent screenshots and screen recording', + super.key, + }); + + final String title; + final String subtitle; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final screenCaptureState = ref.watch(screenCaptureProvider); + + final trailing = screenCaptureState.when( + data: (isBlocked) => Switch( + value: isBlocked, + onChanged: (_) => ref.read(screenCaptureProvider.notifier).toggle(), + ), + loading: () => const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), + error: (_, __) => const Icon(Icons.error), + ); + + return GroupedListItem( + title: title, + subtitle: subtitle, + trailing: trailing, + ); + } +} diff --git a/example/lib/widgets/threat_listview.dart b/example/lib/widgets/threat_listview.dart deleted file mode 100644 index 2342436..0000000 --- a/example/lib/widgets/threat_listview.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:freerasp/freerasp.dart'; -import 'package:freerasp_example/extensions.dart'; -import 'package:freerasp_example/widgets/widgets.dart'; - -/// ListView displaying all detected threats -class ThreatListView extends StatelessWidget { - /// Represents a list of detected threats - const ThreatListView({ - required this.threats, - super.key, - }); - - /// Set of detected threats - final Set threats; - - @override - Widget build(BuildContext context) { - return ListView.separated( - padding: const EdgeInsets.all(8), - itemCount: Threat.values.length, - itemBuilder: (context, index) { - final currentThreat = Threat.values[index]; - final isDetected = threats.contains(currentThreat); - - return ListTile( - title: Text(currentThreat.name.capitalize()), - subtitle: Text(isDetected ? 'Danger' : 'Safe'), - trailing: SafetyIcon(isDetected: isDetected), - ); - }, - separatorBuilder: (_, __) => const Divider(height: 1), - ); - } -} diff --git a/example/lib/widgets/widgets.dart b/example/lib/widgets/widgets.dart deleted file mode 100644 index 2342ae2..0000000 --- a/example/lib/widgets/widgets.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'malware_bottom_sheet.dart'; -export 'safety_icon.dart'; -export 'threat_listview.dart'; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 65d4295..e243cd1 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -7,6 +7,8 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: + avatar_glow: ^3.0.1 + dynamic_color: ^1.8.1 flutter: sdk: flutter flutter_riverpod: ^2.3.2 @@ -17,6 +19,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + google_fonts: ^6.3.3 permission_handler: ^12.0.1 diff --git a/ios/freerasp/.index-build/arm64-apple-macosx/debug/freerasp.build/EventProcessor.swiftdeps~ b/ios/freerasp/.index-build/arm64-apple-macosx/debug/freerasp.build/EventProcessor.swiftdeps~ new file mode 100644 index 0000000..f167f8f Binary files /dev/null and b/ios/freerasp/.index-build/arm64-apple-macosx/debug/freerasp.build/EventProcessor.swiftdeps~ differ diff --git a/ios/freerasp/.index-build/arm64-apple-macosx/debug/freerasp.build/SwiftFreeraspPlugin.swiftdeps~ b/ios/freerasp/.index-build/arm64-apple-macosx/debug/freerasp.build/SwiftFreeraspPlugin.swiftdeps~ new file mode 100644 index 0000000..8d36fd3 Binary files /dev/null and b/ios/freerasp/.index-build/arm64-apple-macosx/debug/freerasp.build/SwiftFreeraspPlugin.swiftdeps~ differ diff --git a/ios/freerasp/.index-build/arm64-apple-macosx/debug/freerasp.build/TalsecHandlers.swiftdeps~ b/ios/freerasp/.index-build/arm64-apple-macosx/debug/freerasp.build/TalsecHandlers.swiftdeps~ new file mode 100644 index 0000000..c98219a Binary files /dev/null and b/ios/freerasp/.index-build/arm64-apple-macosx/debug/freerasp.build/TalsecHandlers.swiftdeps~ differ diff --git a/pubspec.yaml b/pubspec.yaml index c3d8a2f..5aaadce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: freerasp description: Flutter library for improving app security and threat monitoring on Android and iOS mobile devices. Learn more about provided features on the freeRASP's homepage first. -version: 7.3.0 +version: 7.3.1 homepage: https://www.talsec.app/freerasp-in-app-protection-security-talsec repository: https://github.com/talsec/Free-RASP-Flutter