Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
android:name="android.hardware.touchscreen"
android:required="false" />


<!-- Samsung-specific permissions for better compatibility -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
Expand All @@ -38,7 +38,7 @@
android:name="com.dexterous.flutterlocalnotifications.ForegroundService"
android:exported="false"
android:stopWithTask="false"
android:foregroundServiceType="systemExempted"/>
android:foregroundServiceType="systemExempted" />

<!-- Register the VPN service with Samsung compatibility -->
<service
Expand All @@ -63,13 +63,22 @@
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="file"
android:pathPattern=".*\\.dfx"
android:mimeType="*/*" />
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent filter only matches android:scheme="file", but on modern Android most file managers/DocumentProvider flows deliver ACTION_VIEW URIs as content://.... With the current filter, tapping a .dfx file is unlikely to resolve to the app.

Consider adding a content scheme variant (and handling persisted URI permissions as needed), or registering by MIME/type/extension in a way that matches how files are actually opened (and ensure the Flutter side can read the content URI).

Suggested change
android:mimeType="*/*" />
android:mimeType="*/*" />
<data
android:scheme="content"
android:pathPattern=".*\\.dfx"
android:mimeType="*/*" />

Copilot uses AI. Check for mistakes.
</intent-filter>
</activity>
<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-0000000000000000~0000000000"/>
Expand All @@ -86,4 +95,4 @@
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
</manifest>
77 changes: 48 additions & 29 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
Expand All @@ -27,17 +27,19 @@
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-6029472941300558~5225768298</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<true />
<key>NSDocumentsFolderUsageDescription</key>
<string>WARP needs access to store its configuration files</string>
<key>NSVPNConfigurationUsageDescription</key>
<string>Defyx needs access to VPN configurations to secure your connection.</string>
<key>NSUserTrackingUsageDescription</key>
<string>We need permission to show you personalized ads. This helps support our free VPN service. Declining means you'll see generic ads instead.</string>
<string>We need permission to show you personalized ads. This helps support our free VPN
service. Declining means you'll see generic ads instead.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Defyx needs access to your photo library to let you select and share images, such as profile pictures or VPN connection QR codes.</string>
<string>Defyx needs access to your photo library to let you select and share images, such as
profile pictures or VPN connection QR codes.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<true />
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
Expand All @@ -64,36 +66,53 @@
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<true />
</dict>
<key>UIStatusBarHidden</key>
<false/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.content</string>
</array>
<key>UTTypeDescription</key>
<string>DefyX Configuration File</string>
<key>UTTypeIdentifier</key>
<string>de.unboundtech.defyxvpn.dfx</string>
<key>UTTypeTagSpecification</key>
<false />
<key>LSSupportsOpeningDocumentsInPlace</key>
<true />
<key>UIFileSharingEnabled</key>
<true />
<key>UISupportsDocumentBrowser</key>
<true />
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.content</string>
</array>
<key>UTTypeDescription</key>
<string>DefyX Configuration File</string>
<key>UTTypeIdentifier</key>
<string>de.unboundtech.defyxvpn.dfx</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>dfx</string>
</array>
</dict>
</dict>
</array>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>public.filename-extension</key>
<key>CFBundleTypeName</key>
<string>DFX File</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>dfx</string>
</array>
<key>LSItemContentTypes</key>
<array>
<string>public.data</string>
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LSItemContentTypes is set to public.data, which makes the app claim it can open any data type rather than only the custom .dfx type. This can cause the app to appear as an option for many unrelated files and may lead to unexpected opens.

Set LSItemContentTypes to the UTI you declared in UTImportedTypeDeclarations (e.g., de.unboundtech.defyxvpn.dfx) so only .dfx files are associated.

Suggested change
<string>public.data</string>
<string>de.unboundtech.defyxvpn.dfx</string>

Copilot uses AI. Check for mistakes.
</array>
</dict>
</dict>
</array>
</array>
</dict>
</plist>
83 changes: 44 additions & 39 deletions lib/app/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import 'package:defyx_vpn/app/advertise_director.dart';
import 'package:defyx_vpn/app/router/app_router.dart';
import 'package:defyx_vpn/core/theme/app_theme.dart';
import 'package:defyx_vpn/modules/core/file_listener.dart';
import 'package:defyx_vpn/modules/core/vpn.dart';
import 'package:defyx_vpn/modules/core/desktop_platform_handler.dart';
import 'package:defyx_vpn/modules/main/presentation/widgets/ump_service.dart';
Expand Down Expand Up @@ -35,6 +36,7 @@ class App extends ConsumerWidget {
}

Future<bool> _initializeApp(WidgetRef ref) async {
FileListener().init(ProviderScope.containerOf(ref.context));
await VPN(ProviderScope.containerOf(ref.context)).getVPNStatus();
await AlertService().init();
Comment on lines 38 to 41
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FileListener().init(...) is called inside _initializeApp, but _initializeApp(ref) is invoked directly in the FutureBuilder (a new Future is created on every rebuild). This can register multiple ReceiveSharingIntent listeners and repeat initialization side-effects. Also, receive_sharing_intent has no desktop implementation; calling this unconditionally can trigger a MissingPluginException on Windows/Linux.

Consider initializing FileListener exactly once (e.g., move init to a StatefulWidget's initState, a Riverpod provider, or cache the Future), and guard the initialization by platform (Android/iOS only, or whichever platforms the plugin supports).

Copilot uses AI. Check for mistakes.
await AnimationService().init();
Expand All @@ -57,20 +59,22 @@ class App extends ConsumerWidget {
if (Platform.isAndroid || Platform.isIOS) {
// Request App Tracking Transparency (iOS only)
if (Platform.isIOS) {
final status = await AppTrackingTransparency.trackingAuthorizationStatus;
final status =
await AppTrackingTransparency.trackingAuthorizationStatus;
if (status == TrackingStatus.notDetermined) {
// Small delay to ensure UI is ready
await Future.delayed(const Duration(milliseconds: 500));
final result = await AppTrackingTransparency.requestTrackingAuthorization();
final result =
await AppTrackingTransparency.requestTrackingAuthorization();
debugPrint('📱 ATT Authorization: $result');
} else {
debugPrint('📱 ATT Status: $status');
}
}

// Get UMP service with cache integration
final umpService = ref.read(umpServiceProvider);

// Request UMP consent (checks cache first)
await umpService.requestConsent(
onDone: () async {
Expand All @@ -91,41 +95,42 @@ class App extends ConsumerWidget {
final designSize = _getDesignSize(context);

return ToastificationWrapper(
config: ToastificationConfig(
maxToastLimit: 1,
blockBackgroundInteraction: false,
applyMediaQueryViewInsets: true,
),
child: ScreenUtilInit(
designSize: designSize,
minTextAdapt: true,
splitScreenMode: true,
builder: (_, __) {
return MaterialApp.router(
title: 'Defyx',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.light,
routerConfig: router,
builder: _appBuilder,
debugShowCheckedModeBanner: false,
// Force English locale (comment out to enable device language detection)
locale: const Locale('en'),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('fa'),
Locale('zh'),
Locale('ru'),
],
);
},
));
config: ToastificationConfig(
maxToastLimit: 1,
blockBackgroundInteraction: false,
applyMediaQueryViewInsets: true,
),
child: ScreenUtilInit(
designSize: designSize,
minTextAdapt: true,
splitScreenMode: true,
builder: (_, __) {
return MaterialApp.router(
title: 'Defyx',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.light,
routerConfig: router,
builder: _appBuilder,
debugShowCheckedModeBanner: false,
// Force English locale (comment out to enable device language detection)
locale: const Locale('en'),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('fa'),
Locale('zh'),
Locale('ru'),
],
);
},
),
);
}

Size _getDesignSize(BuildContext context) {
Expand Down
4 changes: 2 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'app/app.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load();

// Initialize cache directory for VPN core
try {
final String vpnCacheDir = await VpnBridge().getSharedDirectory();
Expand All @@ -20,7 +20,7 @@ void main() async {
} catch (e) {
debugPrint('Failed to set cache directory: $e');
}

// Initialize Firebase only on supported platforms (not Windows)
if (!Platform.isWindows && !Platform.isLinux) {
await Firebase.initializeApp(
Expand Down
56 changes: 56 additions & 0 deletions lib/modules/core/file_listener.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'dart:io';
import 'package:defyx_vpn/core/data/local/remote/api/flowline_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';

class FileListener {
ProviderContainer? _container;

void _startListening(void Function(String dfxPath) onDfxFile) {
ReceiveSharingIntent.instance.getMediaStream().listen(
(List<SharedMediaFile> files) async {
if (files.isEmpty) return;
final file = files.first;
if (file.path.endsWith('.dfx')) {
final content = await _readFileAsString(file.path);
onDfxFile(content);
Comment on lines +15 to +17
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_readFileAsString(file.path) can throw (file missing/permission/encoding), but the async callback passed to .listen/.then doesn't catch exceptions. That can terminate the stream listener or surface as unhandled async errors.

Wrap the per-file handling in a try/catch (and optionally validate file size / encoding) so a bad share/open action doesn't break future intent handling.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +17
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback signature/name is misleading: _startListening(void Function(String dfxPath) onDfxFile) suggests the callback receives a file path, but the code passes the file content (onDfxFile(content)). This makes the API easy to misuse.

Rename the parameter/type to reflect what is passed (e.g., onDfxContent / String content), or change the call to pass the path and let the handler decide how to read it.

Copilot uses AI. Check for mistakes.
}
Comment on lines +11 to +18
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stream subscription returned by getMediaStream().listen(...) is not stored or cancelled, and there is no dispose/close method on FileListener. If init() is ever called more than once (which can happen given the current call site), this will accumulate listeners and can process the same intent multiple times.

Store the StreamSubscription(s) and provide a teardown method to cancel them (and ensure init is idempotent).

Copilot uses AI. Check for mistakes.
},
onError: (err) {
debugPrint('Error receiving shared media: $err');
},
);

ReceiveSharingIntent.instance
.getInitialMedia()
.then((List<SharedMediaFile> files) async {
if (files.isEmpty) return;
final file = files.first;
if (file.path.endsWith('.dfx')) {
final content = await _readFileAsString(file.path);
onDfxFile(content);
}
})
.catchError((err) {
debugPrint('Error getting initial shared media: $err');
});
}

Future<String> _readFileAsString(String path) async {
final file = File(path);
return await file.readAsString();
}

Future<void> _handleFile(String content) async {
_container ??= ProviderContainer();
await _container
?.read(flowlineServiceProvider)
Comment on lines +46 to +48
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_container ??= ProviderContainer(); creates a new, unmanaged Riverpod container when init hasn't been called, and it is never disposed. This can lead to duplicated state, missing overrides, and memory leaks.

Prefer making the container a required dependency (e.g., assert _container != null in _handleFile, or pass WidgetRef/ProviderContainer into the handler) and avoid creating a separate container implicitly.

Suggested change
_container ??= ProviderContainer();
await _container
?.read(flowlineServiceProvider)
final container = _container;
assert(
container != null,
'FileListener.init must be called with a ProviderContainer before handling files.',
);
if (container == null) {
debugPrint(
'FileListener used before init; ignoring received file content.',
);
return;
}
await container
.read(flowlineServiceProvider)

Copilot uses AI. Check for mistakes.
.saveFlowline(offlineMode: true, flowLine: content);
}

void init(ProviderContainer container) {
_container = container;
_startListening(_handleFile);
}
}
2 changes: 1 addition & 1 deletion lib/shared/layout/navbar/defyx_navbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class DefyxNavBar extends ConsumerWidget {
}

void _showShareDialog(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);
ref.read(currentScreenProvider.notifier).state = AppScreen.share;
showGeneralDialog(
context: context,
Expand Down
2 changes: 1 addition & 1 deletion lib/shared/layout/navbar/widgets/introduction_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class IntroductionDialog extends StatelessWidget {

@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final l10n = AppLocalizations.of(context);

return Dialog(
insetPadding: EdgeInsets.symmetric(horizontal: 24.w),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ class _OfflineFlowlineWidgetState extends ConsumerState<OfflineFlowlineWidget> {
backgroundColor: const Color(0xFFF2F2F2),
padding: EdgeInsets.all(10.h),
),
child: Text(AppLocalizations.of(context).offlineFlowlineUndo),
child: Text(
AppLocalizations.of(context).offlineFlowlineUndo,
style: TextStyle(fontSize: 13.sp, fontWeight: FontWeight.w500),
),
),
],
),
Expand Down
Loading
Loading