From 6dad4c2d4695ee55948a7e30e807c780973bc4c7 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Tue, 15 Aug 2023 20:51:44 -0700 Subject: [PATCH 1/3] Add bitwave CSV export --- src/actions/TransactionExportActions.tsx | 76 +++++++++++++++++++ .../scenes/TransactionsExportScene.tsx | 69 ++++++++++++----- src/locales/en_US.ts | 1 + src/locales/strings/enUS.json | 1 + 4 files changed, 126 insertions(+), 21 deletions(-) diff --git a/src/actions/TransactionExportActions.tsx b/src/actions/TransactionExportActions.tsx index 009b0bafd12..556af7a0da4 100644 --- a/src/actions/TransactionExportActions.tsx +++ b/src/actions/TransactionExportActions.tsx @@ -148,6 +148,17 @@ function makeCsvDateTime(date: number): { date: string; time: string } { } } +function makeBitwaveDateTime(date: number): string { + const d = new Date(date * 1000) + const yy = d.getUTCFullYear().toString().slice(-2) + const mm = padZero((d.getUTCMonth() + 1).toString()) + const dd = padZero(d.getUTCDate().toString()) + const hh = padZero(d.getUTCHours().toString()) + const min = padZero(d.getUTCMinutes().toString()) + + return `${mm}/${dd}/${yy} ${hh}:${min}` +} + // // Check if tx is // 1. A transfer @@ -379,3 +390,68 @@ export function exportTransactionsToCSVInner( record_delimiter: '\n' }) } + +export async function exportTransactionsToBitwave( + wallet: EdgeCurrencyWallet, + edgeTransactions: EdgeTransaction[], + currencyCode: string, + multiplier: string, + parentMultiplier: string +): Promise { + const items: any[] = [] + const parentCode = wallet.currencyInfo.currencyCode + + for (const tx of edgeTransactions) { + edgeTxToCsv(tx) + } + + function edgeTxToCsv(edgeTx: EdgeTransaction) { + const { date, isSend, metadata, nativeAmount, networkFee, parentNetworkFee, txid } = edgeTx + const amount: string = abs(div(nativeAmount, multiplier, DECIMAL_PRECISION)) + const time = makeBitwaveDateTime(date) + let fee: string = '' + let feeTicker: string = '' + const { name = '', category = '', notes = '' } = metadata ?? {} + + if (isSend) { + if (parentNetworkFee != null) { + feeTicker = parentCode + fee = div(parentNetworkFee, parentMultiplier, DECIMAL_PRECISION) + } else { + feeTicker = currencyCode + fee = div(networkFee, multiplier, DECIMAL_PRECISION) + } + } + + items.push({ + id: txid, + remoteContactId: '', + amount, + amountTicker: currencyCode, + cost: '', + costTicker: '', + fee, + feeTicker, + time, + blockchainId: txid, + memo: category, + transactionType: isSend ? 'withdrawal' : 'deposit', + accountId: '', + contactId: '', + categoryId: '', + taxExempt: 'FALSE', + tradeId: '', + description: name, + fromAddress: '', + toAddress: '', + groupId: '', + 'metadata:myCustomMetadata1': notes + }) + } + + return csvStringify(items, { + header: true, + quoted_string: true, + record_delimiter: '\n' + }) +} diff --git a/src/components/scenes/TransactionsExportScene.tsx b/src/components/scenes/TransactionsExportScene.tsx index d8834b9e9bd..08f929d1572 100644 --- a/src/components/scenes/TransactionsExportScene.tsx +++ b/src/components/scenes/TransactionsExportScene.tsx @@ -5,10 +5,10 @@ import RNFS from 'react-native-fs' import Share from 'react-native-share' import EntypoIcon from 'react-native-vector-icons/Entypo' -import { exportTransactionsToCSV, exportTransactionsToQBO, updateTxsFiat } from '../../actions/TransactionExportActions' +import { exportTransactionsToBitwave, exportTransactionsToCSV, exportTransactionsToQBO, updateTxsFiat } from '../../actions/TransactionExportActions' import { formatDate } from '../../locales/intl' import { lstrings } from '../../locales/strings' -import { getDisplayDenomination } from '../../selectors/DenominationSelectors' +import { getDisplayDenomination, getExchangeDenomination } from '../../selectors/DenominationSelectors' import { connect } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' import { getWalletName } from '../../util/CurrencyWalletHelpers' @@ -33,6 +33,8 @@ interface OwnProps extends EdgeSceneProps<'transactionsExport'> {} interface StateProps { multiplier: string + exchangeMultiplier: string + parentMultiplier: string } interface DispatchProps { @@ -46,6 +48,7 @@ interface State { endDate: Date isExportQbo: boolean isExportCsv: boolean + isExportBitwave: boolean } class TransactionsExportSceneComponent extends React.PureComponent { @@ -58,7 +61,8 @@ class TransactionsExportSceneComponent extends React.PureComponent startDate: new Date(new Date().getFullYear() - lastYear, lastMonth.getMonth(), 1, 0, 0, 0), endDate: new Date(new Date().getFullYear(), new Date().getMonth(), 1, 0, 0, 0), isExportQbo: false, - isExportCsv: true + isExportCsv: true, + isExportBitwave: false } } @@ -80,7 +84,7 @@ class TransactionsExportSceneComponent extends React.PureComponent } render() { - const { startDate, endDate, isExportCsv, isExportQbo } = this.state + const { startDate, endDate, isExportBitwave, isExportCsv, isExportQbo } = this.state const { theme, route } = this.props const { sourceWallet, currencyCode } = route.params const iconSize = theme.rem(1.25) @@ -88,7 +92,7 @@ class TransactionsExportSceneComponent extends React.PureComponent const walletName = `${getWalletName(sourceWallet)} (${currencyCode})` const startDateString = formatDate(startDate) const endDateString = formatDate(endDate) - const disabledExport = !isExportQbo && !isExportCsv + const disabledExport = !isExportQbo && !isExportCsv && !isExportBitwave return ( @@ -108,21 +112,23 @@ class TransactionsExportSceneComponent extends React.PureComponent } renderAndroidSwitches() { - const { isExportCsv, isExportQbo } = this.state + const { isExportBitwave, isExportCsv, isExportQbo } = this.state return ( <> - - + + + ) } renderIosSwitches() { - const { isExportCsv, isExportQbo } = this.state + const { isExportBitwave, isExportCsv, isExportQbo } = this.state return ( <> + ) } @@ -139,25 +145,34 @@ class TransactionsExportSceneComponent extends React.PureComponent this.setState({ endDate: date }) } - handleAndroidToggle = () => { - this.setState(state => ({ - isExportCsv: !state.isExportCsv, - isExportQbo: !state.isExportQbo - })) - } - handleQboToggle = () => { - this.setState(state => ({ isExportQbo: !state.isExportQbo })) + if (Platform.OS === 'android') { + this.setState({ isExportQbo: true, isExportCsv: false, isExportBitwave: false }) + } else { + this.setState(state => ({ isExportQbo: !state.isExportQbo })) + } } handleCsvToggle = () => { - this.setState(state => ({ isExportCsv: !state.isExportCsv })) + if (Platform.OS === 'android') { + this.setState({ isExportCsv: true, isExportBitwave: false, isExportQbo: false }) + } else { + this.setState(state => ({ isExportCsv: !state.isExportCsv })) + } + } + + handleBitwaveToggle = () => { + if (Platform.OS === 'android') { + this.setState({ isExportBitwave: true, isExportCsv: false, isExportQbo: false }) + } else { + this.setState(state => ({ isExportBitwave: !state.isExportBitwave })) + } } handleSubmit = async (): Promise => { - const { multiplier, route } = this.props + const { exchangeMultiplier, multiplier, parentMultiplier, route } = this.props const { sourceWallet, currencyCode } = route.params - const { isExportQbo, isExportCsv, startDate, endDate } = this.state + const { isExportBitwave, isExportQbo, isExportCsv, startDate, endDate } = this.state if (startDate.getTime() > endDate.getTime()) { showError(lstrings.export_transaction_error) return @@ -222,6 +237,16 @@ class TransactionsExportSceneComponent extends React.PureComponent formats.push('QBO') } + if (isExportBitwave) { + const bitwaveFile = await exportTransactionsToBitwave(sourceWallet, txs, currencyCode, exchangeMultiplier, parentMultiplier) + files.push({ + contents: bitwaveFile, + mimeType: 'text/comma-separated-values', + fileName: fileName + '.bitwave.csv' + }) + formats.push('Bitwave CSV') + } + const title = 'Share Transactions ' + formats.join(', ') if (Platform.OS === 'android') { await this.shareAndroid(title, files[0]) @@ -268,7 +293,9 @@ class TransactionsExportSceneComponent extends React.PureComponent export const TransactionsExportScene = connect( (state, { route: { params } }) => ({ - multiplier: getDisplayDenomination(state, params.sourceWallet.currencyInfo.pluginId, params.currencyCode).multiplier + multiplier: getDisplayDenomination(state, params.sourceWallet.currencyInfo.pluginId, params.currencyCode).multiplier, + exchangeMultiplier: getExchangeDenomination(state, params.sourceWallet.currencyInfo.pluginId, params.currencyCode).multiplier, + parentMultiplier: getExchangeDenomination(state, params.sourceWallet.currencyInfo.pluginId, params.sourceWallet.currencyInfo.currencyCode).multiplier }), dispatch => ({ updateTxsFiatDispatch: async (wallet: EdgeCurrencyWallet, currencyCode: string, txs: EdgeTransaction[]) => diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 27a14f4cfb7..9842668abdc 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1002,6 +1002,7 @@ const strings = { string_end: 'End', export_transaction_quickbooks_qbo: 'Quickbooks QBO', export_transaction_csv: 'CSV', + export_transaction_bitwave_csv: 'Bitwave CSV', string_export: 'Export', string_status: 'Status', string_fee: 'Fee', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 3d02c27dd9c..ac3148a5dfe 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -891,6 +891,7 @@ "string_end": "End", "export_transaction_quickbooks_qbo": "Quickbooks QBO", "export_transaction_csv": "CSV", + "export_transaction_bitwave_csv": "Bitwave CSV", "string_export": "Export", "string_status": "Status", "string_fee": "Fee", From c0c27612fddf9a973e91f61886d7fba37ecbce44 Mon Sep 17 00:00:00 2001 From: Paul Puey Date: Tue, 15 Aug 2023 18:49:46 -0700 Subject: [PATCH 2/3] Add UI to get Bitwave accountId --- src/actions/TransactionExportActions.tsx | 3 +- .../scenes/TransactionsExportScene.tsx | 46 ++++++++++++++++++- src/locales/en_US.ts | 3 ++ src/locales/strings/enUS.json | 3 ++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/actions/TransactionExportActions.tsx b/src/actions/TransactionExportActions.tsx index 556af7a0da4..014f6b5f54b 100644 --- a/src/actions/TransactionExportActions.tsx +++ b/src/actions/TransactionExportActions.tsx @@ -393,6 +393,7 @@ export function exportTransactionsToCSVInner( export async function exportTransactionsToBitwave( wallet: EdgeCurrencyWallet, + accountId: string, edgeTransactions: EdgeTransaction[], currencyCode: string, multiplier: string, @@ -436,7 +437,7 @@ export async function exportTransactionsToBitwave( blockchainId: txid, memo: category, transactionType: isSend ? 'withdrawal' : 'deposit', - accountId: '', + accountId, contactId: '', categoryId: '', taxExempt: 'FALSE', diff --git a/src/components/scenes/TransactionsExportScene.tsx b/src/components/scenes/TransactionsExportScene.tsx index 08f929d1572..4846ab49a4c 100644 --- a/src/components/scenes/TransactionsExportScene.tsx +++ b/src/components/scenes/TransactionsExportScene.tsx @@ -1,3 +1,4 @@ +import { asObject, asString } from 'cleaners' import { EdgeCurrencyWallet, EdgeTransaction } from 'edge-core-js' import * as React from 'react' import { Platform, ScrollView } from 'react-native' @@ -14,6 +15,7 @@ import { EdgeSceneProps } from '../../types/routerTypes' import { getWalletName } from '../../util/CurrencyWalletHelpers' import { SceneWrapper } from '../common/SceneWrapper' import { DateModal } from '../modals/DateModal' +import { TextInputModal } from '../modals/TextInputModal' import { Airship, showError } from '../services/AirshipInstance' import { ThemeProps, withTheme } from '../services/ThemeContext' import { SettingsHeaderRow } from '../settings/SettingsHeaderRow' @@ -51,6 +53,14 @@ interface State { isExportBitwave: boolean } +const EXPORT_TX_INFO_FILE = 'exportTxInfo.json' + +const asExportTxInfo = asObject({ + bitwaveAccountId: asString +}) + +type ExportTxInfo = ReturnType + class TransactionsExportSceneComponent extends React.PureComponent { constructor(props: Props) { super(props) @@ -173,6 +183,40 @@ class TransactionsExportSceneComponent extends React.PureComponent const { exchangeMultiplier, multiplier, parentMultiplier, route } = this.props const { sourceWallet, currencyCode } = route.params const { isExportBitwave, isExportQbo, isExportCsv, startDate, endDate } = this.state + + let accountId = '' + if (isExportBitwave) { + let fileAccountId = '' + try { + const result = await sourceWallet.disklet.getText(EXPORT_TX_INFO_FILE) + fileAccountId = asExportTxInfo(JSON.parse(result)).bitwaveAccountId + } catch (e) { + console.log(`Could not read ${EXPORT_TX_INFO_FILE} ${String(e)}. Failure is ok`) + } + + accountId = + (await Airship.show(bridge => ( + + ))) ?? '' + + if (accountId !== fileAccountId) { + const exportTxInfo: ExportTxInfo = { + bitwaveAccountId: accountId + } + await sourceWallet.disklet.setText(EXPORT_TX_INFO_FILE, JSON.stringify(exportTxInfo)) + } + } + if (startDate.getTime() > endDate.getTime()) { showError(lstrings.export_transaction_error) return @@ -238,7 +282,7 @@ class TransactionsExportSceneComponent extends React.PureComponent } if (isExportBitwave) { - const bitwaveFile = await exportTransactionsToBitwave(sourceWallet, txs, currencyCode, exchangeMultiplier, parentMultiplier) + const bitwaveFile = await exportTransactionsToBitwave(sourceWallet, accountId, txs, currencyCode, exchangeMultiplier, parentMultiplier) files.push({ contents: bitwaveFile, mimeType: 'text/comma-separated-values', diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 9842668abdc..d1716c10cb2 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1003,6 +1003,9 @@ const strings = { export_transaction_quickbooks_qbo: 'Quickbooks QBO', export_transaction_csv: 'CSV', export_transaction_bitwave_csv: 'Bitwave CSV', + export_transaction_bitwave_accountid_modal_title: 'Bitwave Account ID', + export_transaction_bitwave_accountid_modal_message: 'Please enter the Bitwave account ID for this wallet', + export_transaction_bitwave_accountid_modal_input_label: 'Account ID', string_export: 'Export', string_status: 'Status', string_fee: 'Fee', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index ac3148a5dfe..933d91cbba9 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -892,6 +892,9 @@ "export_transaction_quickbooks_qbo": "Quickbooks QBO", "export_transaction_csv": "CSV", "export_transaction_bitwave_csv": "Bitwave CSV", + "export_transaction_bitwave_accountid_modal_title": "Bitwave Account ID", + "export_transaction_bitwave_accountid_modal_message": "Please enter the Bitwave account ID for this wallet", + "export_transaction_bitwave_accountid_modal_input_label": "Account ID", "string_export": "Export", "string_status": "Status", "string_fee": "Fee", From 0a32ce983e41d30e1f4f70c9a7c6f5784d44c2cc Mon Sep 17 00:00:00 2001 From: Paul Puey Date: Tue, 15 Aug 2023 19:11:16 -0700 Subject: [PATCH 3/3] Save export settings to disk per tokenId --- .../scenes/TransactionsExportScene.tsx | 75 +++++++++++++++---- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/src/components/scenes/TransactionsExportScene.tsx b/src/components/scenes/TransactionsExportScene.tsx index 4846ab49a4c..e0daeabd8f0 100644 --- a/src/components/scenes/TransactionsExportScene.tsx +++ b/src/components/scenes/TransactionsExportScene.tsx @@ -1,4 +1,4 @@ -import { asObject, asString } from 'cleaners' +import { asBoolean, asObject, asString } from 'cleaners' import { EdgeCurrencyWallet, EdgeTransaction } from 'edge-core-js' import * as React from 'react' import { Platform, ScrollView } from 'react-native' @@ -12,6 +12,7 @@ import { lstrings } from '../../locales/strings' import { getDisplayDenomination, getExchangeDenomination } from '../../selectors/DenominationSelectors' import { connect } from '../../types/reactRedux' import { EdgeSceneProps } from '../../types/routerTypes' +import { getTokenId } from '../../util/CurrencyInfoHelpers' import { getWalletName } from '../../util/CurrencyWalletHelpers' import { SceneWrapper } from '../common/SceneWrapper' import { DateModal } from '../modals/DateModal' @@ -37,6 +38,7 @@ interface StateProps { multiplier: string exchangeMultiplier: string parentMultiplier: string + tokenId: string | undefined } interface DispatchProps { @@ -56,9 +58,15 @@ interface State { const EXPORT_TX_INFO_FILE = 'exportTxInfo.json' const asExportTxInfo = asObject({ - bitwaveAccountId: asString + bitwaveAccountId: asString, + isExportQbo: asBoolean, + isExportCsv: asBoolean, + isExportBitwave: asBoolean }) +const asExportTxInfoMap = asObject(asExportTxInfo) + +type ExportTxInfoMap = ReturnType type ExportTxInfo = ReturnType class TransactionsExportSceneComponent extends React.PureComponent { @@ -93,6 +101,24 @@ class TransactionsExportSceneComponent extends React.PureComponent }) } + async componentDidMount(): Promise { + try { + const { sourceWallet } = this.props.route.params + const { tokenId = sourceWallet.currencyInfo.currencyCode } = this.props + const { disklet } = sourceWallet + const result = await disklet.getText(EXPORT_TX_INFO_FILE) + const exportTxInfoMap = asExportTxInfoMap(JSON.parse(result)) + const { isExportBitwave, isExportCsv, isExportQbo } = exportTxInfoMap[tokenId] + this.setState({ + isExportBitwave, + isExportCsv, + isExportQbo + }) + } catch (e) { + console.log(`Could not read ${EXPORT_TX_INFO_FILE} ${String(e)}. Failure is ok`) + } + } + render() { const { startDate, endDate, isExportBitwave, isExportCsv, isExportQbo } = this.state const { theme, route } = this.props @@ -183,17 +209,22 @@ class TransactionsExportSceneComponent extends React.PureComponent const { exchangeMultiplier, multiplier, parentMultiplier, route } = this.props const { sourceWallet, currencyCode } = route.params const { isExportBitwave, isExportQbo, isExportCsv, startDate, endDate } = this.state + const { tokenId = sourceWallet.currencyInfo.currencyCode } = this.props + + let exportTxInfo: ExportTxInfo | undefined + let exportTxInfoMap: ExportTxInfoMap | undefined + try { + const result = await sourceWallet.disklet.getText(EXPORT_TX_INFO_FILE) + exportTxInfoMap = asExportTxInfoMap(JSON.parse(result)) + exportTxInfo = exportTxInfoMap[tokenId] + } catch (e) { + console.log(`Could not read ${EXPORT_TX_INFO_FILE} ${String(e)}. Failure is ok`) + } let accountId = '' - if (isExportBitwave) { - let fileAccountId = '' - try { - const result = await sourceWallet.disklet.getText(EXPORT_TX_INFO_FILE) - fileAccountId = asExportTxInfo(JSON.parse(result)).bitwaveAccountId - } catch (e) { - console.log(`Could not read ${EXPORT_TX_INFO_FILE} ${String(e)}. Failure is ok`) - } + const fileAccountId = exportTxInfo?.bitwaveAccountId ?? '' + if (isExportBitwave) { accountId = (await Airship.show(bridge => ( title={lstrings.export_transaction_bitwave_accountid_modal_title} /> ))) ?? '' + } - if (accountId !== fileAccountId) { - const exportTxInfo: ExportTxInfo = { - bitwaveAccountId: accountId - } - await sourceWallet.disklet.setText(EXPORT_TX_INFO_FILE, JSON.stringify(exportTxInfo)) + if ( + exportTxInfo?.bitwaveAccountId !== accountId || + exportTxInfo?.isExportBitwave !== isExportBitwave || + exportTxInfo?.isExportCsv !== isExportCsv || + exportTxInfo?.isExportQbo !== isExportQbo + ) { + if (exportTxInfoMap == null) { + exportTxInfoMap = {} + } + exportTxInfoMap[tokenId] = { + bitwaveAccountId: accountId, + isExportBitwave, + isExportQbo, + isExportCsv } + await sourceWallet.disklet.setText(EXPORT_TX_INFO_FILE, JSON.stringify(exportTxInfoMap)) } if (startDate.getTime() > endDate.getTime()) { @@ -339,7 +381,8 @@ export const TransactionsExportScene = connect ({ multiplier: getDisplayDenomination(state, params.sourceWallet.currencyInfo.pluginId, params.currencyCode).multiplier, exchangeMultiplier: getExchangeDenomination(state, params.sourceWallet.currencyInfo.pluginId, params.currencyCode).multiplier, - parentMultiplier: getExchangeDenomination(state, params.sourceWallet.currencyInfo.pluginId, params.sourceWallet.currencyInfo.currencyCode).multiplier + parentMultiplier: getExchangeDenomination(state, params.sourceWallet.currencyInfo.pluginId, params.sourceWallet.currencyInfo.currencyCode).multiplier, + tokenId: getTokenId(state.core.account, params.sourceWallet.currencyInfo.pluginId, params.currencyCode) }), dispatch => ({ updateTxsFiatDispatch: async (wallet: EdgeCurrencyWallet, currencyCode: string, txs: EdgeTransaction[]) =>