diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1610106fa..e2268a967 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -68,7 +68,7 @@ diff --git a/blue_modules/fs.js b/blue_modules/fs.ts similarity index 53% rename from blue_modules/fs.js rename to blue_modules/fs.ts index 08b2f362f..340d975a9 100644 --- a/blue_modules/fs.js +++ b/blue_modules/fs.ts @@ -3,75 +3,79 @@ import RNFS from 'react-native-fs'; import Share from 'react-native-share'; import loc from '../loc'; import DocumentPicker from 'react-native-document-picker'; -import { launchCamera, launchImageLibrary } from 'react-native-image-picker'; -import { presentCameraNotAuthorizedAlert } from '../class/camera'; -import { isDesktop } from '../blue_modules/environment'; +import { launchImageLibrary } from 'react-native-image-picker'; +import { isDesktop } from './environment'; import alert from '../components/Alert'; import { readFile } from './react-native-bw-file-access'; + const LocalQRCode = require('@remobile/react-native-qrcode-local-image'); -const writeFileAndExportToAndroidDestionation = async ({ filename, contents, destinationLocalizedString, destination }) => { - const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, { - title: loc.send.permission_storage_title, - message: loc.send.permission_storage_message, - buttonNeutral: loc.send.permission_storage_later, - buttonNegative: loc._.cancel, - buttonPositive: loc._.ok, - }); - if (granted === PermissionsAndroid.RESULTS.GRANTED || Platform.Version >= 33) { - const filePath = destination + `/${filename}`; - try { - await RNFS.writeFile(filePath, contents); - alert(loc.formatString(loc._.file_saved, { filePath: filename, destination: destinationLocalizedString })); - } catch (e) { - console.log(e); - alert(e.message); - } - } else { - console.log('Storage Permission: Denied'); - Alert.alert(loc.send.permission_storage_title, loc.send.permission_storage_denied_message, [ - { - text: loc.send.open_settings, - onPress: () => { - Linking.openSettings(); - }, - style: 'default', - }, - { text: loc._.cancel, onPress: () => {}, style: 'cancel' }, - ]); - } +const _shareOpen = async (filePath: string) => { + return await Share.open({ + url: 'file://' + filePath, + saveToFiles: isDesktop, + }) + .catch(error => { + alert(error.message); + console.log(error); + }) + .finally(() => { + RNFS.unlink(filePath); + }); }; -const writeFileAndExport = async function (filename, contents) { +/** + * Writes a file to fs, and triggers an OS sharing dialog, so user can decide where to put this file (share to cloud + * or perhabs messaging app). Provided filename should be just a file name, NOT a path + */ +const writeFileAndExport = async function (filename: string, contents: string) { if (Platform.OS === 'ios') { const filePath = RNFS.TemporaryDirectoryPath + `/${filename}`; await RNFS.writeFile(filePath, contents); - await Share.open({ - url: 'file://' + filePath, - saveToFiles: isDesktop, - }) - .catch(error => { - console.log(error); - }) - .finally(() => { - RNFS.unlink(filePath); - }); + await _shareOpen(filePath); } else if (Platform.OS === 'android') { - await writeFileAndExportToAndroidDestionation({ - filename, - contents, - destinationLocalizedString: loc._.downloads_folder, - destination: RNFS.DownloadDirectoryPath, + const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, { + title: loc.send.permission_storage_title, + message: loc.send.permission_storage_message, + buttonNeutral: loc.send.permission_storage_later, + buttonNegative: loc._.cancel, + buttonPositive: loc._.ok, }); + + // In Android 13 no WRITE_EXTERNAL_STORAGE permission is needed + // @see https://stackoverflow.com/questions/76311685/permissionandroid-request-always-returns-never-ask-again-without-any-prompt-r + if (granted === PermissionsAndroid.RESULTS.GRANTED || Platform.Version >= 33) { + const filePath = RNFS.DocumentDirectoryPath + `/${filename}`; + try { + await RNFS.writeFile(filePath, contents); + console.log(`file saved to ${filePath}`); + await _shareOpen(filePath); + } catch (e: any) { + console.log(e); + alert(e.message); + } + } else { + console.log('Storage Permission: Denied'); + Alert.alert(loc.send.permission_storage_title, loc.send.permission_storage_denied_message, [ + { + text: loc.send.open_settings, + onPress: () => { + Linking.openSettings(); + }, + style: 'default', + }, + { text: loc._.cancel, onPress: () => {}, style: 'cancel' }, + ]); + } + } else { + alert('Not implemented for this platform'); } }; /** * Opens & reads *.psbt files, and returns base64 psbt. FALSE if something went wrong (wont throw). - * - * @returns {Promise} Base64 PSBT */ -const openSignedTransaction = async function () { +const openSignedTransaction = async function (): Promise { try { const res = await DocumentPicker.pickSingle({ type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles], @@ -87,7 +91,7 @@ const openSignedTransaction = async function () { return false; }; -const _readPsbtFileIntoBase64 = async function (uri) { +const _readPsbtFileIntoBase64 = async function (uri: string): Promise { const base64 = await RNFS.readFile(uri, 'base64'); const stringData = Buffer.from(base64, 'base64').toString(); // decode from base64 if (stringData.startsWith('psbt')) { @@ -106,19 +110,17 @@ const showImagePickerAndReadImage = () => { return new Promise((resolve, reject) => launchImageLibrary( { - title: null, mediaType: 'photo', - takePhotoButtonTitle: null, maxHeight: 800, maxWidth: 600, selectionLimit: 1, }, response => { if (!response.didCancel) { - const asset = response.assets[0]; + const asset = response.assets?.[0] ?? {}; if (asset.uri) { const uri = asset.uri.toString().replace('file://', ''); - LocalQRCode.decode(uri, (error, result) => { + LocalQRCode.decode(uri, (error: any, result: string) => { if (!error) { resolve(result); } else { @@ -132,33 +134,7 @@ const showImagePickerAndReadImage = () => { ); }; -const takePhotoWithImagePickerAndReadPhoto = () => { - return new Promise((resolve, reject) => - launchCamera( - { - title: null, - mediaType: 'photo', - takePhotoButtonTitle: null, - }, - response => { - if (response.uri) { - const uri = response.uri.toString().replace('file://', ''); - LocalQRCode.decode(uri, (error, result) => { - if (!error) { - resolve(result); - } else { - reject(new Error(loc.send.qr_error_no_qrcode)); - } - }); - } else if (response.error) { - presentCameraNotAuthorizedAlert(response.error); - } - }, - ), - ); -}; - -const showFilePickerAndReadFile = async function () { +const showFilePickerAndReadFile = async function (): Promise<{ data: string | false; uri: string | false }> { try { const res = await DocumentPicker.pickSingle({ copyTo: 'cachesDirectory', @@ -175,21 +151,32 @@ const showFilePickerAndReadFile = async function () { : [DocumentPicker.types.allFiles], }); + if (!res.fileCopyUri) { + alert('Picking and caching a file failed'); + return { data: false, uri: false }; + } + const fileCopyUri = decodeURI(res.fileCopyUri); - let file = false; + let file; if (res.fileCopyUri.toLowerCase().endsWith('.psbt')) { // this is either binary file from ElectrumDesktop OR string file with base64 string in there file = await _readPsbtFileIntoBase64(fileCopyUri); return { data: file, uri: decodeURI(res.fileCopyUri) }; } - if (res?.type === DocumentPicker.types.images || res?.type?.startsWith('image/')) { + if (res.type === DocumentPicker.types.images || res.type?.startsWith('image/')) { return new Promise(resolve => { - const uri2 = res.fileCopyUri.toString().replace('file://', ''); - LocalQRCode.decode(decodeURI(uri2), (error, result) => { + if (!res.fileCopyUri) { + // to make ts happy, should not need this check here + alert('Picking and caching a file failed'); + resolve({ data: false, uri: false }); + return; + } + const uri2 = res.fileCopyUri.replace('file://', ''); + LocalQRCode.decode(decodeURI(uri2), (error: any, result: string) => { if (!error) { - resolve({ data: result, fileCopyUri }); + resolve({ data: result, uri: fileCopyUri }); } else { resolve({ data: false, uri: false }); } @@ -198,8 +185,8 @@ const showFilePickerAndReadFile = async function () { } file = await RNFS.readFile(fileCopyUri); - return { data: file, fileCopyUri }; - } catch (err) { + return { data: file, uri: fileCopyUri }; + } catch (err: any) { if (!DocumentPicker.isCancel(err)) { alert(err.message); } @@ -207,12 +194,13 @@ const showFilePickerAndReadFile = async function () { } }; -// todo expand with other platforms if necessary -const readFileOutsideSandbox = filePath => { +const readFileOutsideSandbox = (filePath: string) => { if (Platform.OS === 'ios') { return readFile(filePath); - } else { + } else if (Platform.OS === 'android') { return RNFS.readFile(filePath); + } else { + alert('Not implemented for this platform'); } }; @@ -220,5 +208,4 @@ module.exports.writeFileAndExport = writeFileAndExport; module.exports.openSignedTransaction = openSignedTransaction; module.exports.showFilePickerAndReadFile = showFilePickerAndReadFile; module.exports.showImagePickerAndReadImage = showImagePickerAndReadImage; -module.exports.takePhotoWithImagePickerAndReadPhoto = takePhotoWithImagePickerAndReadPhoto; module.exports.readFileOutsideSandbox = readFileOutsideSandbox; diff --git a/loc/en.json b/loc/en.json index 510faabd4..621477ff7 100644 --- a/loc/en.json +++ b/loc/en.json @@ -16,8 +16,6 @@ "success": "Success", "wallet_key": "Wallet key", "invalid_animated_qr_code_fragment": "Invalid animated QR Code fragment. Please try again.", - "file_saved": "File {filePath} has been saved in your {destination}.", - "downloads_folder": "Downloads Folder", "close": "Close", "change_input_currency": "Change input currency", "refresh": "Refresh", diff --git a/screen/selftest.js b/screen/selftest.js index a628bf51b..791073b00 100644 --- a/screen/selftest.js +++ b/screen/selftest.js @@ -42,7 +42,7 @@ export default class Selftest extends Component { } onPressSaveToStorage = () => { - fs.writeFileAndExport('bluewallet-storagesave-test.txt', 'Success'); + fs.writeFileAndExport('bluewallet-storagesave-test.txt', 'Success on ' + new Date().toUTCString()); }; onPressImportDocument = async () => { @@ -318,7 +318,7 @@ export default class Selftest extends Component {