I had a similar problem while trying to implement a download feature that downloads and saves a CSV from the backend to the local app location.
There are a few reasons why url_launcher wouldn't work for my case some of them are
- The downloads here will be authenticated (using JWT in the backend)
which requires a header value of access-token which cannot be set in url_launcher since the headers will already be set when the user launches the URL in the launch view.
- The return type is not a web view file rather it is a byte-stream data from the backend. If this is your logic then url_launcher will just show a white page and crash right away since the byte stream cannot be opened in web view.
Hence I would like to recommend this strategy of writing files as bytes into the device using dart:io functions
the dependencies we would need are
- permission_handler: ^10.2.0
- api_cache_manager: ^1.0.1 (only if you are using the local sqlite storage)
Backend logic:
const functionToGetCSV = async (req, res) => {
    try {
        const data = await inventory.findAll({raw: true});
        sendCSV(res, "Backend Data" + dateForFilename(), data);
        return;
    } catch (error) {
        logger.error("server error", { error });
        return res.status(500).send({ message: "Server Error. Try again." });
    }
}
Frontend logic:
I had the following export option in the three-dot button which will initiate the download process
- Export button This gets executed as soon as the export is clicked This gets executed as soon as the export is clicked
 -  SelectedItem(BuildContext context, int item) {
 if (item == 2) {
   _launchUrl();
 } else if (item == 3) {
   _logout();
 }}
 
- The launch URL logic -  Future<void> _launchUrl() async {
 String baseUrl = 'http://${Global.endpoint}';
 var url = Uri.parse('$baseUrl/api/csv/get-inventory');
 var token = await APICacheManager().getCacheData("login_details");
 var token1 = json.decode(token.syncData);
 var client = http.Client();
 final response = await client.get(url, headers: <String, String>{
   'Access-Token': token1["data"]["token"],
 });
 if (await _requestPermission(Permission.storage)) {
   try {
     // ignore: await_only_futures
     DateTime datetime = await DateTime.now();
     // ignore: unnecessary_new
     File file = await new File(
             'storage/emulated/0/ERP/${datetime.hour.toString()}${datetime.minute.toString()}${datetime.second.toString()}get-inventory.csv')
         .create(recursive: true);
     await file.writeAsBytes(response.bodyBytes,
         flush: true, mode: FileMode.write);
     final snackBar = SnackBar(
         backgroundColor: const Color.fromARGB(255, 36, 198, 30),
         content: Text(
             'File Saved! ${file.path}'));
     ScaffoldMessenger.of(context).showSnackBar(snackBar);
   } catch (err) {
     final snackBar = SnackBar(
         backgroundColor: const Color.fromARGB(255, 198, 30, 30),
         content: Text("Error saving file! ${err.toString()}"));
     ScaffoldMessenger.of(context).showSnackBar(snackBar);
   }
 } else {
   _launchUrl();
 }}
 
Make sure:
- To keep the await for DateTime.Now() as I found in some emulators there might be a delay in calling the DateTime.Now() (ps: yes it was weird as the bug vanished after I gave await for this function) 
- To keep the new File and .create(recursive:true); since this would create com.package_name folder in case you don't have one by default 
- To add this request permission function -  Future<bool> _requestPermission(Permission permission) async {
 var res = await permission.request();
 if (res == PermissionStatus.granted) {
   return true;
 } else {
   return false;
 }}
 
- you will be able to see your downloads saved here
Internal storage has your app folder
Inside that you can find your saved files 
- Do let me know if this works for you as this will be very easy for downloaded authenticated files.