Skip to content

Commit

Permalink
Fix transaction export on Android
Browse files Browse the repository at this point in the history
  • Loading branch information
swansontec committed Sep 30, 2020
1 parent 408755e commit 7e0c912
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 161 deletions.
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ PODS:
- React
- RNOpenAppSettings (1.0.0):
- React
- RNShare (1.1.3):
- RNShare (3.8.3):
- React
- RNSound (0.11.0):
- React
Expand Down Expand Up @@ -326,7 +326,7 @@ SPEC CHECKSUMS:
RNFS: c9bbde46b0d59619f8e7b735991c60e0f73d22c1
RNLocalize: b6df30cc25ae736d37874f9bce13351db2f56796
RNOpenAppSettings: 1169b90a275e9e18c5973e1608949f97d426e7f3
RNShare: 4f206fa36e384e95a0cbf79f2a92490647e93127
RNShare: 883e13860ebb923bf9f7312187c3d744f546e3bb
RNSound: da030221e6ac7e8290c6b43f2b5f2133a8e225b0
RNStoreReview: 62d6afd7c37db711a594bbffca6b0ea3a812b7a8
RNVectorIcons: ac7bf6bfeafaf3ad34552cdc6eed6fb8c4b7c940
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
"react-native-router-flux": "4.0.6",
"react-native-safari-view": "^2.1.0",
"react-native-safe-area-view": "^0.14.4",
"react-native-share": "^1.1.3",
"react-native-share": "^3.8.3",
"react-native-slider": "^0.11.0",
"react-native-slowlog": "^1.0.2",
"react-native-smart-splash-screen": "git://github.com/swansontec/react-native-smart-splash-screen#fix-rn59",
Expand Down
11 changes: 11 additions & 0 deletions patches/react-native-share+3.8.3.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
diff --git a/node_modules/react-native-share/RNShare.podspec b/node_modules/react-native-share/RNShare.podspec
index 32750f0..80944b2 100644
--- a/node_modules/react-native-share/RNShare.podspec
+++ b/node_modules/react-native-share/RNShare.podspec
@@ -14,5 +14,5 @@ Pod::Spec.new do |s|

s.source_files = "ios/**/*.{h,m}"

- s.dependency "React-Core"
+ s.dependency "React"
end
222 changes: 83 additions & 139 deletions src/components/scenes/TransactionsExportScene.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import type { EdgeCurrencyWallet, EdgeGetTransactionsOptions } from 'edge-core-j
import * as React from 'react'
import { Platform, ScrollView } from 'react-native'
import RNFS from 'react-native-fs'
import Mailer from 'react-native-mail'
import Share from 'react-native-share'
import AntDesign from 'react-native-vector-icons/AntDesign'
import Entypo from 'react-native-vector-icons/Entypo'
import { connect } from 'react-redux'
import { base64 } from 'rfc4648'

import { formatExpDate } from '../../locales/intl.js'
import s from '../../locales/strings'
import { getDisplayDenomination } from '../../modules/Settings/selectors.js'
import type { State as StateType } from '../../types/reduxTypes.js'
import { sanitizeForFilename } from '../../util/utils.js'
import { utf8 } from '../../util/utf8.js'
import { SceneWrapper } from '../common/SceneWrapper.js'
import { DateModal } from '../modals/DateModal.js'
import { Airship, showActivity, showError } from '../services/AirshipInstance.js'
Expand All @@ -26,19 +26,10 @@ import { SettingsRow } from '../themed/SettingsRow.js'
import { SettingsSwitchRow } from '../themed/SettingsSwitchRow.js'
import { PrimaryButton } from '../themed/ThemedButtons.js'

type Files = {
qbo?: {
file: string,
format: string,
path: string,
fileName: string
},
csv?: {
file: string,
format: string,
path: string,
fileName: string
}
type File = {
contents: string,
mimeType: string, // 'text/csv'
fileName: string // wallet-btc-2020.csv
}

type OwnProps = {
Expand Down Expand Up @@ -86,19 +77,6 @@ class TransactionsExportSceneComponent extends React.PureComponent<Props, State>
})
}

handleSubmit = (): void => {
const { startDate, endDate, isExportQbo, isExportCsv } = this.state
if (startDate.getTime() > endDate.getTime()) {
showError(s.strings.export_transaction_error)
return
}
if (Platform.OS === 'android' && isExportQbo && isExportCsv) {
showError(s.strings.export_transaction_export_error_2)
return
}
this.exportFiles()
}

render() {
const { startDate, endDate, isExportCsv, isExportQbo } = this.state
const { theme } = this.props
Expand Down Expand Up @@ -176,148 +154,114 @@ class TransactionsExportSceneComponent extends React.PureComponent<Props, State>
this.setState(state => ({ isExportCsv: !state.isExportCsv }))
}

filenameDateString() {
const date = new Date()
const fileNameAppend =
date.getFullYear().toString() +
(date.getMonth() + 1).toString() +
date.getDate().toString() +
date.getHours().toString() +
date.getMinutes().toString() +
date.getSeconds().toString()

return fileNameAppend
handleSubmit = (): void => {
const { startDate, endDate } = this.state
if (startDate.getTime() > endDate.getTime()) {
showError(s.strings.export_transaction_error)
return
}
this.exportFiles().catch(showError)
}

fileName = (format: string) => {
pickFileName() {
const { sourceWallet, currencyCode } = this.props
const now = new Date()

const walletName = sourceWallet.name != null ? sourceWallet.name : s.strings.string_no_wallet_name

const fullCurrencyCode =
sourceWallet.currencyInfo.currencyCode === currencyCode ? currencyCode : `${sourceWallet.currencyInfo.currencyCode}-${currencyCode}`
const walletName = sourceWallet.name ? `${sourceWallet.name}-${fullCurrencyCode}-` : `${s.strings.string_no_wallet_name}-${fullCurrencyCode}-`
return sanitizeForFilename(walletName) + this.filenameDateString() + '.' + format.toLowerCase()
}

filePath = (format: string) => {
const directory = Platform.OS === 'ios' ? RNFS.DocumentDirectoryPath : RNFS.ExternalDirectoryPath
return directory + '/' + this.fileName(format)
const dateString =
now.getFullYear().toString() +
(now.getMonth() + 1).toString() +
now.getDate().toString() +
now.getHours().toString() +
now.getMinutes().toString() +
now.getSeconds().toString()

const fileName = `${walletName}-${fullCurrencyCode}-${dateString}`
return fileName
.replace(/[^\w\s-]/g, '') // Delete weird characters
.trim()
.replace(/[-\s]+/g, '-') // Collapse spaces & dashes
}

exportFiles = async () => {
const { isExportQbo, isExportCsv } = this.state
async exportFiles(): Promise<void> {
const { isExportQbo, isExportCsv, startDate, endDate } = this.state
const { sourceWallet, currencyCode, multiplier } = this.props
const transactionOptions: EdgeGetTransactionsOptions = {
denomination: this.props.multiplier,
currencyCode: this.props.currencyCode,
startDate: this.state.startDate,
endDate: this.state.endDate
denomination: multiplier,
currencyCode,
startDate,
endDate
}

const files = {}
const fileName = this.pickFileName()
const files: File[] = []
const formats: string[] = []

// Error check when no transactions on a given date range
// The non-string result appears to be a bug in the core,
// which we are relying on to determine if the date range is empty:
const csvFile = await showActivity(s.strings.export_transaction_loading, this.props.sourceWallet.exportTransactionsToCSV(transactionOptions))
if (typeof csvFile !== 'string') {
showError(s.strings.export_transaction_export_error)
return
}

if (isExportCsv) {
const format = 'CSV'
files.csv = {
file: csvFile,
format,
path: this.filePath(format),
fileName: this.fileName(format)
}
files.push({
contents: csvFile,
mimeType: 'text/csv',
fileName: fileName + '.csv'
})
formats.push('CSV')
}

if (isExportQbo) {
const format = 'QBO'
files.qbo = {
file: await showActivity(s.strings.export_transaction_loading, this.props.sourceWallet.exportTransactionsToQBO(transactionOptions)),
format,
path: this.filePath(format),
fileName: this.fileName(format)
}
const qboFile = await showActivity(s.strings.export_transaction_loading, sourceWallet.exportTransactionsToCSV(transactionOptions))
files.push({
contents: qboFile,
mimeType: 'application/vnd.intu.qbo',
fileName: fileName + '.qbo'
})
formats.push('QBO')
}

this.write(files)
}

write = async (files: Files) => {
if (!files.qbo && !files.csv) return
const paths = []
let subject = null
try {
if (files.qbo) {
const { file, format, path } = files.qbo
paths.push(path)
subject = `Share Transactions ${format}`
await RNFS.writeFile(path, file, 'utf8')
}

if (files.csv) {
const { file, format, path } = files.csv
subject = subject ? `${subject}, ${format}` : `Share Transactions ${format}`
paths.push(path)
await RNFS.writeFile(path, file, 'utf8')
}

if (Platform.OS === 'ios') {
this.openShareApp(paths, subject || '')
return
}

const androidExport = this.state.isExportQbo ? files.qbo : files.csv
if (androidExport) {
this.openMailApp(androidExport.path, `Share Transactions ${androidExport.format}`, androidExport.format.toLowerCase())
return
} else {
throw new Error(s.strings.export_transaction_export_error_3)
}
} catch (error) {
console.log(error.message)
const title = 'Share Transactions ' + formats.join(', ')
if (Platform.OS === 'android') {
await this.shareAndroid(title, files[0])
} else {
await this.shareIos(title, files)
}
}

openShareApp = (paths: string[], subject: string) => {
const shareOptions = {
title: subject,
async shareAndroid(title: string, file: File): Promise<void> {
const url = `data:${file.mimeType};base64,${base64.stringify(utf8.parse(file.contents))}`
await Share.open({
title,
message: '',
urls: paths.map(path => 'file://' + path),
subject: subject // for email
}
Share.open(shareOptions)
.then(() => {
console.log('FS: Success')
})
.catch(err => {
console.log('FS:error on Share ', err.message)
console.log('FS:error on Share ', err)
})
url,
filename: file.fileName,
subject: title
}).catch(error => console.log(error))
}

openMailApp = (path: string, subject: string, fileType: string) => {
const attachment = {
path: path, // The absolute path of the file from which to read data.
type: fileType // Mime Type: jpg, png, doc, ppt, html, pdf
async shareIos(title: string, files: File[]): Promise<void> {
const directory = RNFS.DocumentDirectoryPath
const urls: string[] = []
for (const file of files) {
const url = `file://${directory}/${file.fileName}`
urls.push(url)
await RNFS.writeFile(`${directory}/${file.fileName}`, file.contents, 'utf8')
}
Mailer.mail(
{
subject: subject,
recipients: [''],
body: ' ',
isHTML: true,
attachment
},
(error, event) => {
if (error) {
console.log(error)
}
if (event === 'sent') {
console.log('ss: This is sent')
}
}
)

await Share.open({
title,
message: '',
urls,
subject: title
}).catch(error => console.log(error))
}
}

Expand Down
2 changes: 0 additions & 2 deletions src/locales/en_US.js
Original file line number Diff line number Diff line change
Expand Up @@ -698,8 +698,6 @@ const strings = {
string_enter_amount: 'Enter Amount',
export_transaction_error: 'Start date should be earlier than the end date',
export_transaction_export_error: 'No transactions in the date range chosen',
export_transaction_export_error_2: 'Select only one export type on android',
export_transaction_export_error_3: 'Error exporting your file on android',
export_transaction_loading: 'Exporting Transactions…'
}

Expand Down
2 changes: 0 additions & 2 deletions src/locales/strings/enUS.json
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,5 @@
"string_enter_amount": "Enter Amount",
"export_transaction_error": "Start date should be earlier than the end date",
"export_transaction_export_error": "No transactions in the date range chosen",
"export_transaction_export_error_2": "Select only one export type on android",
"export_transaction_export_error_3": "Error exporting your file on android",
"export_transaction_loading": "Exporting Transactions…"
}
34 changes: 34 additions & 0 deletions src/util/utf8.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @flow

export const utf8 = {
parse(text: string): Uint8Array {
const byteString = encodeURI(text)
const out = new Uint8Array(byteString.length)

// Treat each character as a byte, except for %XX escape sequences:
let di = 0 // Destination index
for (let i = 0; i < byteString.length; ++i) {
const c = byteString.charCodeAt(i)
if (c === 0x25) {
out[di++] = parseInt(byteString.slice(i + 1, i + 3), 16)
i += 2
} else {
out[di++] = c
}
}

// Trim any over-allocated space (zero-copy):
return out.subarray(0, di)
},

stringify(data: Uint8Array | number[]): string {
// Create a %XX escape sequence for each input byte:
let byteString = ''
for (let i = 0; i < data.length; ++i) {
const byte = data[i]
byteString += '%' + (byte >> 4).toString(16) + (byte & 0xf).toString(16)
}

return decodeURIComponent(byteString)
}
}
11 changes: 0 additions & 11 deletions src/util/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,17 +516,6 @@ export const autoCorrectDate = (dateInSeconds: number, currentDateInSeconds: num
return dateInSeconds
}

// Strips special characters and replaces spaces with hyphens
export const sanitizeForFilename = (s: string) => {
const charRegex = /[^\w\s-]/g
const hyphenRegex = /[-\s]+/g

s = s.replace(charRegex, '').trim()
s = s.replace(hyphenRegex, '-')

return s
}

export const getYesterdayDateRoundDownHour = () => {
const date = new Date()
date.setMinutes(0)
Expand Down
Loading

0 comments on commit 7e0c912

Please sign in to comment.