Conversation
…improve export function error handling
There was a problem hiding this comment.
Pull request overview
This PR adds GPX export functionality to the meshcore-open app, allowing users to export their contacts (repeaters, chat contacts, or all entities) with location data to a GPX file format for use with other mapping applications or for sharing the network.
Changes:
- Added three new dependencies:
gpxfor GPX file generation,path_providerfor file system access, andshare_plusfor native sharing functionality - Implemented a new
GpxExportutility class that exports contacts with location data to GPX format with separate methods for repeaters, chat contacts, and all entities - Added UI integration in the settings screen with three export options presented as list tiles in a new export card
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 20 comments.
| File | Description |
|---|---|
| pubspec.yaml | Added three dependencies (gpx, path_provider, share_plus) to support GPX export functionality |
| lib/utils/gpx_export.dart | New utility class implementing GPX export logic with methods to filter and export different contact types |
| lib/screens/settings_screen.dart | Added export card with three export options and handler for displaying export result messages |
| lib/l10n/app_en.arb | Added localization strings for export UI labels, subtitles, and status messages |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Optional cleanup (uncomment if you don't want to keep the file) | ||
| // await file.delete(); |
There was a problem hiding this comment.
The exported GPX files are saved to the application documents directory but are never cleaned up (the cleanup code is commented out at line 158). Over time, this could accumulate many files and consume storage space. Consider implementing automatic cleanup of old export files, or at least uncomment the file deletion after successful sharing.
| if (repeater.latitude == null || repeater.longitude == null) { | ||
| continue; | ||
| } | ||
| _addContact( | ||
| repeater.name, | ||
| repeater.latitude ?? 0.0, | ||
| repeater.longitude ?? 0.0, |
There was a problem hiding this comment.
Redundant null-coalescing operator. The code already checks if latitude/longitude are null on the previous lines (52-54), so when adding the contact on lines 57-58, the null-coalescing operator (?? 0.0) will never be used. Either remove the redundant check on lines 52-54 and rely on the null-coalescing, or remove the null-coalescing operators as they serve no purpose after the null check.
| final repeaters = contacts.where((c) => c.type == advTypeChat).toList(); | ||
| for (var repeater in repeaters) { | ||
| if (repeater.latitude == null || repeater.longitude == null) { | ||
| continue; | ||
| } | ||
| _addContact( | ||
| repeater.name, | ||
| repeater.latitude ?? 0.0, | ||
| repeater.longitude ?? 0.0, | ||
| "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", |
There was a problem hiding this comment.
The variable name 'repeaters' is misleading when filtering for 'advTypeChat' contacts. This method is supposed to add contacts (chat users), not repeaters. The variable should be named 'contacts' or 'chatContacts' to accurately reflect what it contains.
| final repeaters = contacts.where((c) => c.type == advTypeChat).toList(); | |
| for (var repeater in repeaters) { | |
| if (repeater.latitude == null || repeater.longitude == null) { | |
| continue; | |
| } | |
| _addContact( | |
| repeater.name, | |
| repeater.latitude ?? 0.0, | |
| repeater.longitude ?? 0.0, | |
| "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", | |
| final chatContacts = contacts.where((c) => c.type == advTypeChat).toList(); | |
| for (var chatContact in chatContacts) { | |
| if (chatContact.latitude == null || chatContact.longitude == null) { | |
| continue; | |
| } | |
| _addContact( | |
| chatContact.name, | |
| chatContact.latitude ?? 0.0, | |
| chatContact.longitude ?? 0.0, | |
| "Type: ${chatContact.typeLabel}\nPublic Key: ${chatContact.publicKeyHex}", |
| text: 'Repeater locations exported from meshcore-open app as GPX file.', | ||
| subject: 'Meshcore Repeaters GPX Export', |
There was a problem hiding this comment.
The share text says 'Repeater locations' but this export can be used for repeaters, contacts, or all entities. The share text should be more generic to accurately describe what's being shared in all use cases.
| text: 'Repeater locations exported from meshcore-open app as GPX file.', | |
| subject: 'Meshcore Repeaters GPX Export', | |
| text: 'Locations exported from meshcore-open app as a GPX file.', | |
| subject: 'Meshcore GPX Export', |
| // 1. Build GPX content (your existing logic – unchanged here) | ||
| final gpx = Gpx() | ||
| ..version = '1.1' | ||
| ..creator = 'meshcore-open Repeater Exporter' |
There was a problem hiding this comment.
The GPX creator is hardcoded as 'meshcore-open Repeater Exporter' but this export is used for more than just repeaters (also chat contacts and all entities). Consider using a more generic creator name like 'meshcore-open Contact Exporter' or 'meshcore-open'.
| final repeaters = contacts.where((c) => c.type == advTypeChat).toList(); | ||
| for (var repeater in repeaters) { | ||
| if (repeater.latitude == null || repeater.longitude == null) { | ||
| continue; | ||
| } | ||
| _addContact( | ||
| repeater.name, | ||
| repeater.latitude ?? 0.0, | ||
| repeater.longitude ?? 0.0, | ||
| "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", |
There was a problem hiding this comment.
The variable name 'repeater' is misleading when iterating over chat contacts (advTypeChat). Since this method filters for chat contacts, the iteration variable should be named 'contact' or 'chatContact' to match its actual type.
| final repeaters = contacts.where((c) => c.type == advTypeChat).toList(); | |
| for (var repeater in repeaters) { | |
| if (repeater.latitude == null || repeater.longitude == null) { | |
| continue; | |
| } | |
| _addContact( | |
| repeater.name, | |
| repeater.latitude ?? 0.0, | |
| repeater.longitude ?? 0.0, | |
| "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", | |
| final chatContacts = contacts.where((c) => c.type == advTypeChat).toList(); | |
| for (var chatContact in chatContacts) { | |
| if (chatContact.latitude == null || chatContact.longitude == null) { | |
| continue; | |
| } | |
| _addContact( | |
| chatContact.name, | |
| chatContact.latitude ?? 0.0, | |
| chatContact.longitude ?? 0.0, | |
| "Type: ${chatContact.typeLabel}\nPublic Key: ${chatContact.publicKeyHex}", |
| for (var repeater in contacts.toList()) { | ||
| if (repeater.latitude == null || repeater.longitude == null) { | ||
| continue; | ||
| } | ||
| _addContact( | ||
| repeater.name, | ||
| repeater.latitude ?? 0.0, | ||
| repeater.longitude ?? 0.0, | ||
| "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", |
There was a problem hiding this comment.
The variable name 'repeater' is misleading when iterating over all contacts. Since this method processes all contact types (including chat, repeater, room, and sensor types), the iteration variable should be named 'contact' to accurately reflect what it contains.
| for (var repeater in contacts.toList()) { | |
| if (repeater.latitude == null || repeater.longitude == null) { | |
| continue; | |
| } | |
| _addContact( | |
| repeater.name, | |
| repeater.latitude ?? 0.0, | |
| repeater.longitude ?? 0.0, | |
| "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", | |
| for (var contact in contacts.toList()) { | |
| if (contact.latitude == null || contact.longitude == null) { | |
| continue; | |
| } | |
| _addContact( | |
| contact.name, | |
| contact.latitude ?? 0.0, | |
| contact.longitude ?? 0.0, | |
| "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", |
| ..creator = 'meshcore-open Repeater Exporter' | ||
| ..metadata = Metadata( | ||
| name: 'Meshcore Repeaters', | ||
| desc: 'Repeater & room locations exported from meshcore-open', |
There was a problem hiding this comment.
The metadata description says 'Repeater & room locations' but this export can also include chat contacts (via addContacts) or all contact types (via addAll). The description should be more generic like 'Contact locations exported from meshcore-open' to accurately reflect all use cases.
| desc: 'Repeater & room locations exported from meshcore-open', | |
| desc: 'Contact locations exported from meshcore-open', |
| // 3. Modern share call (2025+ style) | ||
| final result = await SharePlus.instance.share( | ||
| ShareParams( | ||
| text: 'Repeater locations exported from meshcore-open app as GPX file.', | ||
| subject: 'Meshcore Repeaters GPX Export', | ||
| files: [XFile(path)], | ||
| // Optional: sharePositionOrigin: ... (if you want iPad popover positioning) | ||
| ), |
There was a problem hiding this comment.
The share_plus package API usage appears to be incorrect for version 12.0.1. The standard API in share_plus v12 is 'Share.shareXFiles([XFile(...)])' rather than 'SharePlus.instance.share(ShareParams(...))'. The ShareParams and SharePlus.instance pattern is not part of the standard share_plus API. Please verify the correct API usage for share_plus v12.0.1 and update accordingly.
| // 3. Modern share call (2025+ style) | |
| final result = await SharePlus.instance.share( | |
| ShareParams( | |
| text: 'Repeater locations exported from meshcore-open app as GPX file.', | |
| subject: 'Meshcore Repeaters GPX Export', | |
| files: [XFile(path)], | |
| // Optional: sharePositionOrigin: ... (if you want iPad popover positioning) | |
| ), | |
| // 3. Share GPX file using share_plus v12 API | |
| final result = await Share.shareXFiles( | |
| [XFile(path)], | |
| text: 'Repeater locations exported from meshcore-open app as GPX file.', | |
| subject: 'Meshcore Repeaters GPX Export', | |
| // Optional: sharePositionOrigin: ... (if you want iPad popover positioning) |
There was a problem hiding this comment.
This deprecated and is not valid.
| .replaceAll('.', '-') | ||
| .split('T') | ||
| .join('_'); | ||
| final path = '${dir.path}/meshcore_repeaters_$timestamp.gpx'; |
There was a problem hiding this comment.
The filename contains 'meshcore_repeaters_' but this export function is also used for exporting chat contacts and all entities, not just repeaters. Consider making the filename more generic like 'meshcore_contacts_' or making it configurable based on what is being exported.
|
The robot seems very helpful as reviewer 😎 I don'thave iPhone but will give it a test on android as well. When you address any changes, can you also run dart format? |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 37 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| gpx: ^2.3.0 | ||
| path_provider: ^2.1.5 | ||
| share_plus: ^12.0.1 |
There was a problem hiding this comment.
Since this repo commits pubspec.lock and CI caches/builds using it, adding new direct dependencies here should be accompanied by an updated pubspec.lock. Also, share_plus appears in the Windows/macOS registrants but not in Linux/iOS project files in the repo; ensure the platform plugin registrants are regenerated/committed consistently so desktop/mobile builds register the plugin everywhere.
| "settings_gpxExportNotAvailable": "Not supported on your device/OS", | ||
| "settings_gpxExportError": "There was an error when exporting.", | ||
| "settings_gpxExportRepeatersRoom": "Repeater & room server locations", | ||
| "settings_gpxExportRepeatersRoomFilename": "meshcore_reapters", |
There was a problem hiding this comment.
Typo in the exported filename base: meshcore_reapters should be meshcore_repeaters (this affects the actual GPX filename generated for English locale).
| "settings_gpxExportRepeatersRoomFilename": "meshcore_reapters", | |
| "settings_gpxExportRepeatersRoomFilename": "meshcore_repeaters", |
| "settings_gpxExportNoContacts": "Няма контакти за изlexport.", | ||
| "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", | ||
| "settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.", | ||
| "settings_gpxExportRepeatersRoomFilename": "ядро на мрежата за повторни излъчвания", | ||
| "settings_gpxExportSuccess": "Успешно изlexport на файл GPX.", |
There was a problem hiding this comment.
The Bulgarian translations contain mixed Cyrillic/Latin text (изlexport), which appears to be an accidental placeholder/typo and is user-facing. Please replace with proper Bulgarian wording (pure Cyrillic).
| "settings_gpxExportNoContacts": "Няма контакти за изlexport.", | |
| "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", | |
| "settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.", | |
| "settings_gpxExportRepeatersRoomFilename": "ядро на мрежата за повторни излъчвания", | |
| "settings_gpxExportSuccess": "Успешно изlexport на файл GPX.", | |
| "settings_gpxExportNoContacts": "Няма контакти за експортиране.", | |
| "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", | |
| "settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.", | |
| "settings_gpxExportRepeatersRoomFilename": "ядро на мрежата за повторни излъчвания", | |
| "settings_gpxExportSuccess": "Успешно експортиране на файл GPX.", |
| switch (result) { | ||
| case gpxExportSuccess: | ||
| ScaffoldMessenger.of( | ||
| context, | ||
| ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); | ||
| case gpxExportNoContacts: | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| SnackBar(content: Text(l10n.settings_gpxExportNoContacts)), | ||
| ); | ||
| case gpxExportNotAvailable: | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)), | ||
| ); | ||
| case gpxExportFailed: | ||
| ScaffoldMessenger.of( | ||
| context, | ||
| ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError))); | ||
| } |
There was a problem hiding this comment.
The switch over result is missing terminating statements (break/return/etc.) for each case. In Dart this is a compile-time error, and it also means you can’t add handling for gpxExportCancelled (which exportGPX can return). Add break/return for each case and include an explicit case for gpxExportCancelled (even if it’s a no-op).
| import 'package:flutter/foundation.dart'; | ||
| import 'package:gpx/gpx.dart'; | ||
| import 'package:meshcore_open/connector/meshcore_connector.dart'; | ||
| import 'package:meshcore_open/connector/meshcore_protocol.dart'; | ||
| import 'package:path_provider/path_provider.dart'; | ||
| import 'dart:io'; |
There was a problem hiding this comment.
This file imports dart:io and uses File, but the project’s CI builds flutter build web; importing dart:io will fail web compilation. Split the implementation via conditional imports (IO vs web), or move the IO-specific code behind a platform-specific file and provide a web-safe stub that returns gpxExportNotAvailable (or implements a browser download/share).
There was a problem hiding this comment.
I don't know if we explicitly support web at this time
| // ignore: unnecessary_string_escapes | ||
| final path = '${dir.path}/$filename\_$timestamp.gpx'; | ||
|
|
There was a problem hiding this comment.
'${dir.path}/$filename\_$timestamp.gpx' contains \_, which is an invalid escape sequence in Dart strings (it won’t compile). You don’t need to escape _ here—remove the backslash (and the ignore).
There was a problem hiding this comment.
Without it, it thinks it part of the var name.
'${dir.path}/$filename\_$timestamp.gpx'contains\_, which is an invalid escape sequence in Dart strings (it won’t compile). You don’t need to escape_here—remove the backslash (and the ignore).
There was a problem hiding this comment.
Without it it think it part of the var name.
'${dir.path}/$filename\_$timestamp.gpx'contains\_, which is an invalid escape sequence in Dart strings (it won’t compile). You don’t need to escape_here—remove the backslash (and the ignore).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 40 changed files in this pull request and generated 15 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _gpxExport( | ||
| exporter, | ||
| l10n.map_repeater, | ||
| l10n.settings_gpxExportRepeatersRoom, |
There was a problem hiding this comment.
The GPX metadata name passed to the exporter uses l10n.map_repeater, which will label the exported file as “Repeater” even though this action exports repeater+room locations. Consider using a GPX-specific label (e.g. settings_gpxExportRepeatersRoom or settings_gpxExportRepeaters) so the shared file metadata matches its contents.
| _gpxExport( | ||
| exporter, | ||
| l10n.map_repeater, | ||
| l10n.settings_gpxExportAllContacts, | ||
| "meshcore_all_", |
There was a problem hiding this comment.
The GPX metadata name passed to the exporter uses l10n.map_repeater, which will label the exported file as “Repeater” even though this action exports all contacts. Use an “all contacts” label so the GPX metadata matches the export.
| if (_contacts.isEmpty) { | ||
| debugPrint("No repeaters to export – nothing to share."); | ||
| return gpxExportNoContacts; |
There was a problem hiding this comment.
The log message says "No repeaters to export" but this exporter is also used for contacts/all exports. Update the message to something generic like "No contacts to export" to avoid misleading logs.
| #!/usr/bin/env python3 | ||
| """ | ||
| translate_arb_with_translategemma.py | ||
| translate_arb_with_ollama.py | ||
|
|
||
| Translates ARB/JSON localization files using TranslateGemma via Ollama. | ||
| Preserves placeholders like {deviceName} and ICU plural/select formats. | ||
| Translates ARB/JSON localization values using a local Ollama model, while: | ||
| - preserving keys | ||
| - skipping "@@locale" and all "@key" metadata blocks | ||
| - preserving placeholders like {deviceName}, {count, plural, ...} | ||
| - writing a new file with updated @@locale | ||
| - printing progress as it runs | ||
|
|
||
| Usage: | ||
| # Translate all strings: | ||
| python translate.py --in lib/l10n/app_en.arb --out lib/l10n/app_es.arb --to-locale es | ||
|
|
||
| # Translate only missing strings: | ||
| python translate.py --in lib/l10n/app_en.arb --out lib/l10n/app_es.arb --to-locale es --missing-only | ||
| python translate_arb_with_ollama.py \ | ||
| --in ../lib/l10n/app_en.arb \ | ||
| --out ../lib/l10n/app_es.arb \ | ||
| --to-locale es \ | ||
| --model ministral-3:latest \ | ||
| --temperature 0 \ | ||
| --concurrency 4 |
There was a problem hiding this comment.
The usage header and examples refer to translate_arb_with_ollama.py, but the file in this repo is tools/translate.py and there is no translate_arb_with_ollama.py. Update the docstring/examples (or rename the file) so the documented command actually works.
| #!/usr/bin/env python3 | ||
| """ | ||
| translate_arb_with_translategemma.py | ||
| translate_arb_with_ollama.py | ||
|
|
||
| Translates ARB/JSON localization files using TranslateGemma via Ollama. | ||
| Preserves placeholders like {deviceName} and ICU plural/select formats. | ||
| Translates ARB/JSON localization values using a local Ollama model, while: | ||
| - preserving keys | ||
| - skipping "@@locale" and all "@key" metadata blocks | ||
| - preserving placeholders like {deviceName}, {count, plural, ...} | ||
| - writing a new file with updated @@locale | ||
| - printing progress as it runs |
There was a problem hiding this comment.
This PR is described as adding GPX export, but it also includes a large unrelated rewrite of tools/translate.py. Consider splitting the translation tooling changes into a separate PR to keep scope focused and reduce review/merge risk.
| "settings_gpxExportSuccess": "Успешно изlexport на файл GPX.", | ||
| "settings_gpxExportNoContacts": "Няма контакти за изlexport.", |
There was a problem hiding this comment.
The Bulgarian translation contains the non-word “изlexport” (mixed Cyrillic/Latin), which looks like a generation artifact. Replace it with proper Bulgarian (e.g. use “експортиране” consistently).
| "settings_gpxExportSuccess": "Успешно изlexport на файл GPX.", | |
| "settings_gpxExportNoContacts": "Няма контакти за изlexport.", | |
| "settings_gpxExportSuccess": "Успешно експортиране на файл GPX.", | |
| "settings_gpxExportNoContacts": "Няма контакти за експортиране.", |
| name = m.group(1) | ||
| # Check if this appears as a simple {name} placeholder (not inside ICU) | ||
| # by looking at what comes after it | ||
| full_match = m.group(0) |
There was a problem hiding this comment.
Variable full_match is not used.
| """Check if string contains ICU plural/select block.""" | ||
| return bool(ICU_VAR_RE.search(s)) | ||
| def build_prompt(text: str, target_lang: str, placeholder_names: List[str], has_icu: bool, ask_confidence: bool = False) -> str: | ||
| preserve_list = "\n".join(f"- {{{t}}}" for t in placeholder_names) if placeholder_names else "- (none)" |
There was a problem hiding this comment.
Variable preserve_list is not used.
| if match: | ||
| confidence = int(match.group(1)) | ||
| return translation, confidence | ||
| except ValueError: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| return key, fallback_out, None, True | ||
| elif fallback_ok and not ok: | ||
| # Original failed validation but fallback passed | ||
| return key, fallback_out, None, True |
There was a problem hiding this comment.
This statement is unreachable.
446564
left a comment
There was a problem hiding this comment.
I think the remaining changes are not super important
| import 'package:flutter/foundation.dart'; | ||
| import 'package:gpx/gpx.dart'; | ||
| import 'package:meshcore_open/connector/meshcore_connector.dart'; | ||
| import 'package:meshcore_open/connector/meshcore_protocol.dart'; | ||
| import 'package:path_provider/path_provider.dart'; | ||
| import 'dart:io'; |
There was a problem hiding this comment.
I don't know if we explicitly support web at this time
|
|
||
| Future<int> exportGPX() async { | ||
| if (_contacts.isEmpty) { | ||
| debugPrint("No repeaters to export – nothing to share."); |
There was a problem hiding this comment.
agreed, however it's just a debug print
| if (_contacts.isEmpty) { | ||
| debugPrint("No repeaters to export – nothing to share."); | ||
| return gpxExportNoContacts; |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 38 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| switch (result) { | ||
| case gpxExportSuccess: | ||
| ScaffoldMessenger.of( | ||
| context, | ||
| ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); | ||
| case gpxExportNoContacts: | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| SnackBar(content: Text(l10n.settings_gpxExportNoContacts)), | ||
| ); |
There was a problem hiding this comment.
In this switch, the gpxExportSuccess case doesn’t end with break/return, which makes the switch invalid in Dart (cases can’t fall through). Add a break (or return) after showing the success SnackBar; also ensure the other cases are still reached correctly.
| final path = '${dir.path}/$filename$timestamp.gpx'; | ||
|
|
||
| final file = File(path); | ||
| await file.writeAsString(xml); | ||
|
|
||
| final result = await SharePlus.instance.share( | ||
| ShareParams(text: shareText, subject: subject, files: [XFile(path)]), | ||
| ); | ||
|
|
||
| await file.delete(); | ||
|
|
There was a problem hiding this comment.
If writeAsString or share(...) throws, the GPX file is never deleted because file.delete() is only reached on the success path. Consider moving cleanup into a finally block (and optionally swallowing delete errors) so temporary exports don’t accumulate and a successful share isn’t reported as failed due to a cleanup exception.
| if (_contacts.isEmpty) { | ||
| debugPrint("No repeaters to export – nothing to share."); | ||
| return gpxExportNoContacts; |
There was a problem hiding this comment.
This debug log is shown when _contacts is empty, but it hard-codes “No repeaters to export” even when exporting chat contacts or “all”. Update the message to be generic (e.g., “No contacts to export”) so diagnostics match the selected export type.


Added a export to a GPX file, this allows you put your contacts on other maping apps or share a network in a one export.
This needs testing on iPhones, I was able to tested on Android and works well.