BlueWallet/blue_modules/fs.ts

209 lines
7.1 KiB
TypeScript
Raw Normal View History

import { Alert, Linking, PermissionsAndroid, Platform } from 'react-native';
2020-10-05 23:25:14 +02:00
import RNFS from 'react-native-fs';
import Share from 'react-native-share';
import loc from '../loc';
import DocumentPicker from 'react-native-document-picker';
2024-01-16 21:34:47 +01:00
import { launchImageLibrary } from 'react-native-image-picker';
import { isDesktop } from './environment';
import presentAlert from '../components/Alert';
2023-11-12 15:41:51 +01:00
import { readFile } from './react-native-bw-file-access';
2024-01-16 21:34:47 +01:00
2020-11-10 19:42:15 +01:00
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
2020-10-05 23:25:14 +02:00
2024-01-16 21:34:47 +01:00
const _shareOpen = async (filePath: string) => {
return await Share.open({
url: 'file://' + filePath,
saveToFiles: isDesktop,
})
.catch(error => {
console.log(error);
})
.finally(() => {
RNFS.unlink(filePath);
});
};
2024-01-16 21:34:47 +01:00
/**
* 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
*/
2024-03-22 01:54:40 +01:00
export const writeFileAndExport = async function (filename: string, contents: string, showShareDialog: boolean = true) {
2020-10-05 23:25:14 +02:00
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${filename}`;
await RNFS.writeFile(filePath, contents);
2024-01-16 21:34:47 +01:00
await _shareOpen(filePath);
2020-10-05 23:25:14 +02:00
} else if (Platform.OS === 'android') {
2024-01-16 21:34:47 +01:00
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,
2021-09-11 19:46:01 +02:00
});
2024-01-16 21:34:47 +01:00
// 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
2024-03-22 01:54:40 +01:00
if (granted === PermissionsAndroid.RESULTS.GRANTED || Platform.Version >= 30) {
const filePath = RNFS.DownloadDirectoryPath + `/${filename}`;
2024-01-16 21:34:47 +01:00
try {
await RNFS.writeFile(filePath, contents);
console.log(`file saved to ${filePath}`);
2024-03-22 01:54:40 +01:00
if (showShareDialog) {
await _shareOpen(filePath);
} else {
presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { filePath }) });
}
2024-01-16 21:34:47 +01:00
} catch (e: any) {
console.log(e);
}
} 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 {
presentAlert({ message: 'Not implemented for this platform' });
2020-10-05 23:25:14 +02:00
}
};
/**
* Opens & reads *.psbt files, and returns base64 psbt. FALSE if something went wrong (wont throw).
*/
2024-02-18 11:21:43 +01:00
export const openSignedTransaction = async function (): Promise<string | boolean> {
2020-10-05 23:25:14 +02:00
try {
2021-12-27 22:17:02 +01:00
const res = await DocumentPicker.pickSingle({
2021-08-13 22:50:19 +02:00
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles],
2020-10-05 23:25:14 +02:00
});
return await _readPsbtFileIntoBase64(res.uri);
2020-10-05 23:25:14 +02:00
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
presentAlert({ message: loc.send.details_no_signed_tx });
2020-10-05 23:25:14 +02:00
}
}
return false;
};
2024-01-16 21:34:47 +01:00
const _readPsbtFileIntoBase64 = async function (uri: string): Promise<string> {
const base64 = await RNFS.readFile(uri, 'base64');
const stringData = Buffer.from(base64, 'base64').toString(); // decode from base64
if (stringData.startsWith('psbt')) {
// file was binary, but outer code expects base64 psbt, so we return base64 we got from rn-fs;
// most likely produced by Electrum-desktop
return base64;
} else {
// file was a text file, having base64 psbt in there. so we basically have double base64encoded string
// thats why we are returning string that was decoded once;
// most likely produced by Coldcard
return stringData;
}
};
2024-02-18 11:21:43 +01:00
export const showImagePickerAndReadImage = () => {
2020-12-11 04:28:54 +01:00
return new Promise((resolve, reject) =>
2020-12-12 19:07:00 +01:00
launchImageLibrary(
2020-12-11 04:28:54 +01:00
{
mediaType: 'photo',
maxHeight: 800,
maxWidth: 600,
selectionLimit: 1,
2020-12-11 04:28:54 +01:00
},
response => {
2021-09-28 23:58:53 +02:00
if (!response.didCancel) {
2024-01-17 00:30:18 +01:00
const asset = response.assets?.[0] ?? {};
2021-09-28 23:58:53 +02:00
if (asset.uri) {
const uri = asset.uri.toString().replace('file://', '');
2024-01-16 21:34:47 +01:00
LocalQRCode.decode(uri, (error: any, result: string) => {
2021-09-28 23:58:53 +02:00
if (!error) {
resolve(result);
} else {
reject(new Error(loc.send.qr_error_no_qrcode));
}
});
}
2020-12-11 04:28:54 +01:00
}
},
),
);
};
2024-02-18 11:21:43 +01:00
export const showFilePickerAndReadFile = async function (): Promise<{ data: string | false; uri: string | false }> {
try {
2021-12-27 22:17:02 +01:00
const res = await DocumentPicker.pickSingle({
copyTo: 'cachesDirectory',
type:
Platform.OS === 'ios'
2020-11-10 19:42:15 +01:00
? [
'io.bluewallet.psbt',
'io.bluewallet.psbt.txn',
'io.bluewallet.backup',
DocumentPicker.types.plainText,
'public.json',
DocumentPicker.types.images,
]
: [DocumentPicker.types.allFiles],
});
2024-01-16 21:34:47 +01:00
if (!res.fileCopyUri) {
presentAlert({ message: 'Picking and caching a file failed' });
2024-01-16 21:34:47 +01:00
return { data: false, uri: false };
}
const fileCopyUri = decodeURI(res.fileCopyUri);
2020-11-04 16:38:49 +01:00
2024-01-16 21:34:47 +01:00
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) };
}
2024-01-17 00:30:18 +01:00
if (res.type === DocumentPicker.types.images || res.type?.startsWith('image/')) {
return new Promise(resolve => {
2024-01-16 21:34:47 +01:00
if (!res.fileCopyUri) {
// to make ts happy, should not need this check here
presentAlert({ message: 'Picking and caching a file failed' });
2024-01-16 21:34:47 +01:00
resolve({ data: false, uri: false });
return;
}
const uri2 = res.fileCopyUri.replace('file://', '');
LocalQRCode.decode(decodeURI(uri2), (error: any, result: string) => {
if (!error) {
2024-01-16 21:34:47 +01:00
resolve({ data: result, uri: fileCopyUri });
} else {
resolve({ data: false, uri: false });
}
2020-11-10 19:42:15 +01:00
});
});
}
file = await RNFS.readFile(fileCopyUri);
2024-01-16 21:34:47 +01:00
return { data: file, uri: fileCopyUri };
} catch (err: any) {
if (!DocumentPicker.isCancel(err)) {
presentAlert({ message: err.message });
}
2020-10-13 18:58:06 +02:00
return { data: false, uri: false };
}
};
2024-02-18 11:21:43 +01:00
export const readFileOutsideSandbox = (filePath: string) => {
2023-11-12 15:41:51 +01:00
if (Platform.OS === 'ios') {
return readFile(filePath);
2024-01-16 21:34:47 +01:00
} else if (Platform.OS === 'android') {
2023-11-12 15:41:51 +01:00
return RNFS.readFile(filePath);
2024-01-16 21:34:47 +01:00
} else {
presentAlert({ message: 'Not implemented for this platform' });
2024-02-18 11:21:43 +01:00
throw new Error('Not implemented for this platform');
2023-11-12 15:41:51 +01:00
}
};