ADD: Save File Button

This commit is contained in:
Marcos Rodriguez Velez 2024-03-21 20:54:40 -04:00
parent e22bf7d7d0
commit 945a255c7b
No known key found for this signature in database
GPG Key ID: 6030B2F48CCE86D7
13 changed files with 152 additions and 44 deletions

View File

@ -28,7 +28,7 @@ const _shareOpen = async (filePath: string) => {
* 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
*/
export const writeFileAndExport = async function (filename: string, contents: string) {
export const writeFileAndExport = async function (filename: string, contents: string, showShareDialog: boolean = true) {
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${filename}`;
await RNFS.writeFile(filePath, contents);
@ -44,15 +44,18 @@ export const writeFileAndExport = async function (filename: string, contents: st
// 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}`;
if (granted === PermissionsAndroid.RESULTS.GRANTED || Platform.Version >= 30) {
const filePath = RNFS.DownloadDirectoryPath + `/${filename}`;
try {
await RNFS.writeFile(filePath, contents);
console.log(`file saved to ${filePath}`);
await _shareOpen(filePath);
if (showShareDialog) {
await _shareOpen(filePath);
} else {
presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { filePath }) });
}
} catch (e: any) {
console.log(e);
presentAlert({ message: e.message });
}
} else {
console.log('Storage Permission: Denied');

View File

@ -40,19 +40,19 @@ export const Button: React.FC<ButtonProps> = props => {
color: fontColor,
};
return (
<TouchableOpacity
testID={props.testID}
style={[buttonStyle, props.style]}
accessibilityRole="button"
onPress={props.onPress}
disabled={props.disabled}
>
<View style={styles.content}>
{props.icon && <Icon name={props.icon.name} type={props.icon.type} color={props.icon.color} />}
{props.title && <Text style={textStyle}>{props.title}</Text>}
</View>
const buttonView = (
<View style={[buttonStyle, props.style, styles.content]}>
{props.icon && <Icon name={props.icon.name} type={props.icon.type} color={props.icon.color} />}
{props.title && <Text style={textStyle}>{props.title}</Text>}
</View>
);
return props.onPress ? (
<TouchableOpacity testID={props.testID} accessibilityRole="button" onPress={props.onPress} disabled={props.disabled}>
{buttonView}
</TouchableOpacity>
) : (
buttonView
);
};

View File

@ -0,0 +1,41 @@
import React, { ReactNode } from 'react';
import { Platform, StyleProp, TouchableOpacity, ViewStyle } from 'react-native';
import ToolTipMenu from './TooltipMenu';
import loc from '../loc';
const fs = require('../blue_modules/fs');
interface SaveFileButtonProps {
fileName: string;
fileContent: string;
children: ReactNode;
onPress: () => void;
style?: StyleProp<ViewStyle>;
}
const SaveFileButton: React.FC<SaveFileButtonProps> = ({ fileName, fileContent, children, onPress, style }) => {
const actions = [
{ id: 'save', text: loc._.save_to_downloads },
{ id: 'share', text: loc.receive.details_share },
];
const handlePressMenuItem = (actionId: string) => {
const action = actions.find(a => a.id === actionId);
if (action?.id === 'save') {
fs.writeFileAndExport(fileName, fileContent, false);
} else if (action?.id === 'share') {
fs.writeFileAndExport(fileName, fileContent, true);
}
};
return Platform.OS === 'android' ? (
<ToolTipMenu isMenuPrimaryAction actions={actions} onPressMenuItem={handlePressMenuItem} buttonStyle={style}>
{children}
</ToolTipMenu>
) : (
<TouchableOpacity onPress={onPress} style={style}>
{children}
</TouchableOpacity>
);
};
export default SaveFileButton;

View File

@ -14,6 +14,7 @@ type SecondButtonProps = {
disabled?: boolean;
icon?: IconProps;
title?: string;
onPress?: () => void;
};
export const SecondButton = forwardRef<TouchableOpacity, SecondButtonProps>((props, ref) => {
@ -25,13 +26,19 @@ export const SecondButton = forwardRef<TouchableOpacity, SecondButtonProps>((pro
fontColor = colors.buttonDisabledTextColor;
}
return (
const buttonView = (
<View style={styles.view}>
{props.icon && <Icon name={props.icon.name} type={props.icon.type} color={props.icon.color} />}
{props.title && <Text style={[styles.text, { color: fontColor }]}>{props.title}</Text>}
</View>
);
return props.onPress ? (
<TouchableOpacity accessibilityRole="button" style={[styles.button, { backgroundColor }]} {...props} ref={ref}>
<View style={styles.view}>
{props.icon && <Icon name={props.icon.name} type={props.icon.type} color={props.icon.color} />}
{props.title && <Text style={[styles.text, { color: fontColor }]}>{props.title}</Text>}
</View>
{buttonView}
</TouchableOpacity>
) : (
<View style={[styles.button, { backgroundColor }]}>{buttonView}</View>
);
});

View File

@ -4,7 +4,7 @@ import { useTheme } from './themes';
interface SquareButtonProps {
title: string;
onPress: () => void;
onPress?: () => void;
style: StyleProp<ViewStyle>;
testID?: string;
}
@ -19,12 +19,18 @@ export const SquareButton = forwardRef<TouchableOpacity, SquareButtonProps>((pro
},
});
return (
const buttonView = (
<View style={styles.contentContainer}>
<Text style={[styles.text, hookStyles.text]}>{title}</Text>
</View>
);
return onPress ? (
<TouchableOpacity ref={ref} style={style} onPress={onPress} testID={testID} accessibilityRole="button">
<View style={styles.contentContainer}>
<Text style={[styles.text, hookStyles.text]}>{title}</Text>
</View>
{buttonView}
</TouchableOpacity>
) : (
<View style={style}>{buttonView}</View>
);
});

View File

@ -14,6 +14,7 @@
"yes": "Yes",
"no": "No",
"save": "Save",
"save_to_downloads": "Save to Downloads",
"seed": "Seed",
"success": "Success",
"wallet_key": "Wallet key",
@ -210,6 +211,7 @@
"reset_amount_confirm": "Would you like to reset the amount?",
"success_done": "Done",
"txSaved": "The transaction file ({filePath}) has been saved in your Downloads folder.",
"file_saved_at_path": "The file has been saved in your Downloads folder ({filePath}).",
"problem_with_psbt": "Problem with PSBT"
},
"settings": {

View File

@ -20,6 +20,7 @@ import ecc from '../blue_modules/noble_ecc';
import Button from '../components/Button';
import SafeArea from '../components/SafeArea';
import presentAlert from '../components/Alert';
import SaveFileButton from '../components/SaveFileButton';
const bitcoin = require('bitcoinjs-lib');
const BlueCrypto = require('react-native-blue-crypto');
const encryption = require('../blue_modules/encryption');
@ -320,7 +321,12 @@ export default class Selftest extends Component {
}
})()}
<BlueSpacing20 />
<Button title="Test Save to Storage" onPress={this.onPressSaveToStorage} />
<SaveFileButton
fileName="bluewallet-selftest.txt"
fileContent={'Success on ' + new Date().toUTCString()}
onPress={this.onPressSaveToStorage}
/>
<Button title="Test Save to Storage" />
<BlueSpacing20 />
<Button title="Test File Import" onPress={this.onPressImportDocument} />
</ScrollView>

View File

@ -13,6 +13,7 @@ import { requestCameraAuthorization } from '../../helpers/scan-qr';
import { useTheme } from '../../components/themes';
import SafeArea from '../../components/SafeArea';
import { isDesktop } from '../../blue_modules/environment';
import SaveFileButton from '../../components/SaveFileButton';
const bitcoin = require('bitcoinjs-lib');
const fs = require('../../blue_modules/fs');
@ -111,7 +112,14 @@ const PsbtMultisigQRCode = () => {
{isLoading ? (
<ActivityIndicator />
) : (
<SquareButton style={[styles.exportButton, stylesHook.exportButton]} onPress={exportPSBT} title={loc.multisig.share} />
<SaveFileButton
fileName={fileName}
fileContent={psbt.toBase64()}
onPress={exportPSBT}
style={[styles.exportButton, stylesHook.exportButton]}
>
<SquareButton title={loc.multisig.share} />
</SaveFileButton>
)}
</View>
</ScrollView>

View File

@ -18,6 +18,7 @@ import { useTheme } from '../../components/themes';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import SafeArea from '../../components/SafeArea';
import { SecondButton } from '../../components/SecondButton';
import SaveFileButton from '../../components/SaveFileButton';
const BlueElectrum = require('../../blue_modules/BlueElectrum');
const bitcoin = require('bitcoinjs-lib');
const fs = require('../../blue_modules/fs');
@ -264,15 +265,21 @@ const PsbtWithHardwareWallet = () => {
title={loc.send.psbt_tx_open}
/>
<BlueSpacing20 />
<SecondButton
icon={{
name: 'share-alternative',
type: 'entypo',
color: colors.buttonTextColor,
}}
<SaveFileButton
fileName={`${Date.now()}.psbt`}
fileContent={typeof psbt === 'string' ? psbt : psbt.toBase64()}
style={styles.exportButton}
onPress={exportPSBT}
title={loc.send.psbt_tx_export}
/>
>
<SecondButton
icon={{
name: 'share-alternative',
type: 'entypo',
color: colors.buttonTextColor,
}}
title={loc.send.psbt_tx_export}
/>
</SaveFileButton>
<BlueSpacing20 />
<View style={styles.copyToClipboard}>
<BlueCopyToClipboardButton

View File

@ -38,6 +38,7 @@ import Button from '../../components/Button';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import usePrivacy from '../../hooks/usePrivacy';
import { isDesktop } from '../../blue_modules/environment';
import SaveFileButton from '../../components/SaveFileButton';
const prompt = require('../../helpers/prompt');
const A = require('../../blue_modules/analytics');
@ -672,7 +673,14 @@ const WalletsAddMultisigStep2 = () => {
{isLoading ? (
<ActivityIndicator />
) : (
<SquareButton style={[styles.exportButton, stylesHook.exportButton]} onPress={exportCosigner} title={loc.multisig.share} />
<SaveFileButton
style={[styles.exportButton, stylesHook.exportButton]}
fileName={cosignerXpubFilename}
fileContent={cosignerXpub}
onPress={exportCosigner}
>
<SquareButton title={loc.multisig.share} />
</SaveFileButton>
)}
</View>
</View>

View File

@ -48,6 +48,7 @@ import ListItem from '../../components/ListItem';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import Button from '../../components/Button';
import { SecondButton } from '../../components/SecondButton';
import SaveFileButton from '../../components/SaveFileButton';
const prompt = require('../../helpers/prompt');
@ -419,7 +420,7 @@ const WalletDetails = () => {
}
};
const onExportHistoryPressed = async () => {
const exportHistoryContent = () => {
const csvFileArray = [
loc.transactions.date,
loc.transactions.txid,
@ -458,8 +459,11 @@ const WalletDetails = () => {
csvFile += '\n' + data.join(','); // CSV line
}
return csvFile;
};
await writeFileAndExport(`${wallet.label.replace(' ', '-')}-history.csv`, csvFile);
const onExportHistoryPressed = async () => {
await writeFileAndExport(`${wallet.label.replace(' ', '-')}-history.csv`, exportHistoryContent);
};
const handleDeleteButtonTapped = () => {
@ -659,7 +663,13 @@ const WalletDetails = () => {
{walletTransactionsLength > 0 && (
<>
<BlueSpacing20 />
<SecondButton onPress={onExportHistoryPressed} title={loc.wallets.details_export_history} />
<SaveFileButton
fileName={`${wallet.getLabel().replace(' ', '-')}-history.csv`}
fileContent={exportHistoryContent}
onPress={onExportHistoryPressed}
>
<SecondButton title={loc.wallets.details_export_history} />
</SaveFileButton>
</>
)}
{wallet.type === MultisigHDWallet.type && (

View File

@ -11,6 +11,7 @@ import { BlueStorageContext } from '../../blue_modules/storage-context';
import { useTheme } from '../../components/themes';
import SafeArea from '../../components/SafeArea';
import usePrivacy from '../../hooks/usePrivacy';
import SaveFileButton from '../../components/SaveFileButton';
const fs = require('../../blue_modules/fs');
const ExportMultisigCoordinationSetup = () => {
@ -88,7 +89,14 @@ const ExportMultisigCoordinationSetup = () => {
{isShareButtonTapped ? (
<ActivityIndicator />
) : (
<SquareButton style={[styles.exportButton, stylesHook.exportButton]} onPress={exportTxtFile} title={loc.multisig.share} />
<SaveFileButton
style={[styles.exportButton, stylesHook.exportButton]}
fileName={wallet.getLabel() + '.txt'}
fileContent={wallet.getXpub()}
onPress={exportTxtFile}
>
<SquareButton title={loc.multisig.share} />
</SaveFileButton>
)}
<BlueSpacing20 />
<BlueText style={[styles.secret, stylesHook.secret]}>{wallet.getXpub()}</BlueText>

View File

@ -51,6 +51,7 @@ import usePrivacy from '../../hooks/usePrivacy';
import loc from '../../loc';
import { isDesktop } from '../../blue_modules/environment';
import ActionSheet from '../ActionSheet';
import SaveFileButton from '../../components/SaveFileButton';
const fs = require('../../blue_modules/fs');
const prompt = require('../../helpers/prompt');
@ -570,7 +571,6 @@ const ViewEditMultisigCosigners = ({ route }: Props) => {
const isPad: boolean = Platform.isPad;
return (
// @ts-ignore wtf doneButton
<BottomModal isVisible={isShareModalVisible} onClose={hideShareModal} doneButton coverScreen={false}>
<KeyboardAvoidingView enabled={!isPad} behavior={Platform.OS === 'ios' ? 'position' : undefined}>
<View style={[styles.modalContent, stylesHook.modalContent, styles.alignItemsCenter]}>
@ -580,7 +580,9 @@ const ViewEditMultisigCosigners = ({ route }: Props) => {
<QRCodeComponent value={exportStringURv2} size={260} isLogoRendered={false} />
<BlueSpacing20 />
<View style={styles.squareButtonWrapper}>
<SquareButton style={[styles.exportButton, stylesHook.exportButton]} onPress={exportCosigner} title={loc.multisig.share} />
<SaveFileButton fileContent={exportString} fileName={exportFilename} onPress={exportCosigner}>
<SquareButton style={[styles.exportButton, stylesHook.exportButton]} title={loc.multisig.share} />
</SaveFileButton>
</View>
</View>
</KeyboardAvoidingView>