Conversation
There was a problem hiding this comment.
Pull request overview
This PR aims to add support for opening/importing .dfx files into the app (via OS “open with” / sharing flows), and wires the imported content into the existing flowline saving path.
Changes:
- Add a
FileListenerthat listens for incoming shared/opened files and saves.dfxcontent viaflowlineServiceProvider. - Register
.dfxas a supported document type on iOS and add an Android intent-filter for viewing.dfxfiles. - Update Flutter dependencies/lockfile (including adding
receive_sharing_intent).
Reviewed changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| pubspec.yaml | Adds dependencies needed for receiving shared/opened files and webview platform impls. |
| pubspec.lock | Updates resolved dependency graph to include new packages and version changes. |
| lib/modules/core/file_listener.dart | New listener to ingest .dfx files and persist flowline data. |
| lib/app/app.dart | Initializes the new file listener during app initialization. |
| android/app/src/main/AndroidManifest.xml | Adds an ACTION_VIEW intent-filter targeting .dfx files. |
| ios/Runner/Info.plist | Declares .dfx UTI/document types and enables document opening/sharing. |
| lib/shared/layout/navbar/** | Minor localization null-assertion cleanup and a small styling tweak. |
| lib/main.dart | Whitespace-only formatting change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <data | ||
| android:scheme="file" | ||
| android:pathPattern=".*\\.dfx" | ||
| android:mimeType="*/*" /> |
There was a problem hiding this comment.
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).
| android:mimeType="*/*" /> | |
| android:mimeType="*/*" /> | |
| <data | |
| android:scheme="content" | |
| android:pathPattern=".*\\.dfx" | |
| android:mimeType="*/*" /> |
| Future<bool> _initializeApp(WidgetRef ref) async { | ||
| FileListener().init(ProviderScope.containerOf(ref.context)); | ||
| await VPN(ProviderScope.containerOf(ref.context)).getVPNStatus(); | ||
| await AlertService().init(); |
There was a problem hiding this comment.
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).
| 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); | ||
| } |
There was a problem hiding this comment.
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).
| if (file.path.endsWith('.dfx')) { | ||
| final content = await _readFileAsString(file.path); | ||
| onDfxFile(content); |
There was a problem hiding this comment.
_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.
| _container ??= ProviderContainer(); | ||
| await _container | ||
| ?.read(flowlineServiceProvider) |
There was a problem hiding this comment.
_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.
| _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) |
| 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); |
There was a problem hiding this comment.
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.
| </array> | ||
| <key>LSItemContentTypes</key> | ||
| <array> | ||
| <string>public.data</string> |
There was a problem hiding this comment.
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.
| <string>public.data</string> | |
| <string>de.unboundtech.defyxvpn.dfx</string> |
Change Description
Briefly describe what this PR does and why. Keep it short and clear.
Related Platforms
Verification Checklist
Optional (for bigger changes)
Related Links
Closes #ID.