Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Commit

Permalink
Merge pull request #5909 from willy-b/payment-history-receipt-pdf-reb…
Browse files Browse the repository at this point in the history
…ase-11-28

Replace Payment History CSV export with Contribution Statement PDF (rebased)
  • Loading branch information
bsclifton authored Nov 29, 2016
2 parents 5166ffe + 380ee91 commit fa04026
Show file tree
Hide file tree
Showing 20 changed files with 1,013 additions and 99 deletions.
100 changes: 54 additions & 46 deletions js/lib/ledgerExportUtil.js → app/common/lib/ledgerExportUtil.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
const base64Encode = require('./base64').encode
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */

const base64Encode = require('../../../js/lib/base64').encode
const underscore = require('underscore')

/**
* Generates a contribution breakdown by publisher as a CSV data URL from an array of one or more transactions
* @param {Object[]} transactions - array of transactions
*/
let transactionsToCSVDataURL = function (transactions) {
let csvText = getTransactionCSVText(transactions, null, true)
module.exports.transactionsToCSVDataURL = (transactions) => {
const csvText = module.exports.getTransactionCSVText(transactions, null, true)
return 'data:text/csv;base64,' + base64Encode(csvText)
}

Expand All @@ -24,7 +28,7 @@ let transactionsToCSVDataURL = function (transactions) {
* @param {string[]=} viewingIds - OPTIONAL array of one or more viewingIds to filter transactions (single string viewingId supported too)
* if null or undefined, all transactions are returned
*/
let getTransactionsByViewingIds = function getTransactionsByViewingIds (transactions, viewingIds) {
module.exports.getTransactionsByViewingIds = (transactions, viewingIds) => {
if (!transactions) {
return []
}
Expand Down Expand Up @@ -67,18 +71,18 @@ let getTransactionsByViewingIds = function getTransactionsByViewingIds (transact
* @param {string[]} viewingIds - OPTIONAL array/string containing one or more viewingIds to filter by
* if null or undefined, all transactions are used
*/
let getTotalContribution = function getTotalContribution (transactions, viewingIds) {
var txs = getTransactionsByViewingIds(transactions, viewingIds)
module.exports.getTotalContribution = (transactions, viewingIds) => {
const txs = module.exports.getTransactionsByViewingIds(transactions, viewingIds)

var totalContribution = {
const totalContribution = {
satoshis: 0,
fiat: { amount: 0, currency: null },
fee: 0
}

for (var i = txs.length - 1; i >= 0; i--) {
var tx = txs[i] || {}
var txContribution = tx.contribution || {}
for (let i = txs.length - 1; i >= 0; i--) {
const tx = txs[i] || {}
const txContribution = tx.contribution || {}

totalContribution.satoshis += 0 || txContribution.satoshis

Expand All @@ -101,7 +105,7 @@ let getTotalContribution = function getTotalContribution (transactions, viewingI
}

/**
* Gives a summary of votes/contributions by Publisher from an array of one or ore transactions
* Gives a summary of votes/contributions by Publisher from an array of one or more transactions
* @example
* txUtil.getPublisherVoteData(client.state.transactions)
* // {
Expand All @@ -123,13 +127,13 @@ let getTotalContribution = function getTotalContribution (transactions, viewingI
* @param {Object[]} transactions - array of transactions
* @param {string[]=} viewingIds - OPTIONAL array/string with one or more viewingIds to filter transactions by (if empty, uses all tx)
**/
let getPublisherVoteData = function getPublisherVoteData (transactions, viewingIds) {
transactions = getTransactionsByViewingIds(transactions, viewingIds)
module.exports.getPublisherVoteData = (transactions, viewingIds) => {
transactions = module.exports.getTransactionsByViewingIds(transactions, viewingIds)

var publishersWithVotes = {}
var totalVotes = 0
const publishersWithVotes = {}
let totalVotes = 0

for (var i = transactions.length - 1; i >= 0; i--) {
for (let i = transactions.length - 1; i >= 0; i--) {
var tx = transactions[i]
var ballots = tx.ballots

Expand Down Expand Up @@ -157,7 +161,7 @@ let getPublisherVoteData = function getPublisherVoteData (transactions, viewingI
var totalContributionAmountFiat = null
var currency = null

var totalContribution = getTotalContribution(transactions)
const totalContribution = module.exports.getTotalContribution(transactions)

if (totalContribution) {
totalContributionAmountSatoshis = totalContributionAmountSatoshis || totalContribution.satoshis
Expand All @@ -166,7 +170,7 @@ let getPublisherVoteData = function getPublisherVoteData (transactions, viewingI
}

for (let publisher in publishersWithVotes) {
let voteDataForPublisher = publishersWithVotes[publisher]
const voteDataForPublisher = publishersWithVotes[publisher]
let fraction = voteDataForPublisher.fraction = voteDataForPublisher.votes / totalVotes

let contribution = voteDataForPublisher.contribution || {}
Expand All @@ -192,30 +196,44 @@ let getPublisherVoteData = function getPublisherVoteData (transactions, viewingI
* Generates a contribution breakdown by publisher in an array of CSV rows from an array of transactions
* @example
* txUtil.getTransactionCSVRows(client.state.transactions)
* // [ 'Publisher,Votes,Fraction,BTC,USD',
* // 'chronicle.com,2,0.04081632653061224,0.0000033221,0.20 USD',
* // 'waitbutwhy.com,3,0.061224489795918366,0.0000049832,0.31 USD',
* // 'archlinux.org,1,0.02040816326530612,0.0000016611,0.10 USD',
* // [ ['Publisher,Votes,Fraction,BTC,USD'],
* // ['chronicle.com,2,0.04081632653061224,0.0000033221,0.20 USD'],
* // ['waitbutwhy.com,3,0.061224489795918366,0.0000049832,0.31 USD'],
* // ['archlinux.org,1,0.02040816326530612,0.0000016611,0.10 USD'],
* // /.../
* // ]
*
* @param {Object[]} transactions - array of transactions
* @param {string[]=} viewingIds - OPTIONAL array/string with one or more viewingIds to filter transactions by (if empty, uses all tx)
* @param {boolean=} addTotalRow - OPTIONAL boolean indicating whether to add a TOTALS row (defaults false)
**/
let getTransactionCSVRows = function (transactions, viewingIds, addTotalRow) {
let txContribData = getPublisherVoteData(transactions, viewingIds)
module.exports.getTransactionCSVRows = (transactions, viewingIds, addTotalRow, sortByContribution) => {
let txContribData = module.exports.getPublisherVoteData(transactions, viewingIds)
var publishers = (underscore.keys(txContribData) || [])

// sort publishers alphabetically
// TODO: take locale argument and pass to localeCompare below
publishers = publishers.sort(function (a, b) {
return (a && typeof a === 'string' ? a : '').localeCompare(b && typeof b === 'string' ? b : '')
})
let publisherSortFunction

if (sortByContribution) {
// sort publishers by contribution
publisherSortFunction = function (a, b) {
var getVotes = function (pubStr) {
return (pubStr && typeof pubStr === 'string' && txContribData[pubStr] && txContribData[pubStr].votes ? parseInt(txContribData[pubStr].votes) : 0)
}
return (getVotes(a) > getVotes(b) ? -1 : 1)
}
} else {
// sort publishers alphabetically by default (per spec)
// TODO: take locale argument and pass to localeCompare below
publisherSortFunction = function (a, b) {
return (a && typeof a === 'string' ? a : '').localeCompare(b && typeof b === 'string' ? b : '')
}
}

publishers = publishers.sort(publisherSortFunction)

var currency = (publishers.length ? txContribData[publishers[0]].contribution.currency : 'USD')
const currency = (publishers.length ? txContribData[publishers[0]].contribution.currency : 'USD')

var headerRow = ['Publisher', 'Votes', 'Fraction', 'BTC', currency].join(',')
const headerRow = ['Publisher', 'Votes', 'Fraction', 'BTC', currency].join(',')

var totalsRow = {
label: 'TOTAL',
Expand Down Expand Up @@ -275,8 +293,8 @@ let getTransactionCSVRows = function (transactions, viewingIds, addTotalRow) {
*
* returns a CSV with only a header row if input is empty or invalid
**/
let getTransactionCSVText = function (transactions, viewingIds, addTotalRow) {
return getTransactionCSVRows(transactions, viewingIds, addTotalRow).join('\n')
module.exports.getTransactionCSVText = (transactions, viewingIds, addTotalRow) => {
return module.exports.getTransactionCSVRows(transactions, viewingIds, addTotalRow).join('\n')
}

/**
Expand All @@ -287,7 +305,7 @@ let getTransactionCSVText = function (transactions, viewingIds, addTotalRow) {
*
* @returns {Object[]} transactions (with each element having an added field `exportFilenamePrefix`)
*/
let addExportFilenamePrefixToTransactions = function (transactions) {
module.exports.addExportFilenamePrefixToTransactions = (transactions) => {
transactions = transactions || []

if (!underscore.isArray(transactions)) {
Expand All @@ -298,10 +316,10 @@ let addExportFilenamePrefixToTransactions = function (transactions) {
return transactions
}

var dateCountMap = {}
const dateCountMap = {}

return transactions.map(function (transaction) {
let timestamp = transaction.submissionStamp
const timestamp = transaction.submissionStamp
let numericDateStr = (new Date(timestamp)).toLocaleDateString().replace(/\//g, '-')

let dateCount = (dateCountMap[numericDateStr] ? dateCountMap[numericDateStr] : 1)
Expand All @@ -316,13 +334,3 @@ let addExportFilenamePrefixToTransactions = function (transactions) {
return transaction
})
}

module.exports = {
transactionsToCSVDataURL,
getTransactionCSVText,
getTransactionCSVRows,
getPublisherVoteData,
getTransactionsByViewingIds,
getTotalContribution,
addExportFilenamePrefixToTransactions
}
24 changes: 24 additions & 0 deletions app/extensions/brave/about-contributions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html>
<head>
<meta charset="utf-8">
<meta name="availableLanguages" content="">
<meta name="defaultLanguage" content="en-US">
<meta name='theme-color' content='#ff5000'>
<link rel="shortcut icon"type="image/x-icon" href="data:image/x-icon;,">
<title data-l10n-id="contributionStatement"></title>
<script src='js/about.js'></script>
<script src="ext/l20n.min.js" async></script>
<link rel="localization" href="locales/{locale}/preferences.properties">
<link rel="localization" href="locales/{locale}/common.properties">
<link rel="localization" href="locales/{locale}/bravery.properties">
<link rel="localization" href="locales/{locale}/error.properties">
<link rel="localization" href="locales/{locale}/errorMessages.properties">
</head>
<body>
<div id="appContainer"/>
</body>
</html>
18 changes: 18 additions & 0 deletions app/extensions/brave/locales/en-US/preferences.properties
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ viewPaymentHistory=View Payment History
paymentHistoryTitle=Your Payment History
paymentHistoryFooterText=Your next payment contribution is {{reconcileDate}}.
paymentHistoryOKText=OK
bravePayments=Brave Payments
beta=beta
contributionDate=Contribution Date
contributionTime=Contribution Time
contributionAmount=Contribution Amount
contributionStatement=Contribution Statement
percentPaid=% Paid
dollarsPaid=$ Paid
verifiedExplainerText= = publisher has verified their wallet
pageNofMText=Page {{n}} of {{m}}
contributionStatementFooterNoteBoxHeading1=Note:
contributionStatementFooterNoteBoxBody1=To protect your privacy, this Brave Payments contribution statement is not saved, recorded or logged anywhere other than on your device (this computer). It cannot be retrieved from Brave in the event of data loss on your device.
contributionStatementFooterNoteBoxHeading2=About publisher distributions
contributionStatementFooterNoteBoxBody2=Brave Payments uses a statistical model that removes any ability to identify Brave users based on their browsing behaviors. Anonymous contributions are first combined in the Brave vault and then redistributed into publisher wallets which are confirmed and then collected by the publisher.
contributionStatementCopyrightFooter=&copy;2016 Brave Software. Brave is a registered trademark of Brave Software. Site names may be trademarks or registered trademarks of the site owner.
contributionStatements=Contribution statements
listOfContributionStatements=List of contribution statements
bitcoinAddress=Your Brave wallet address is:
bitcoinPaymentURL=Your Brave wallet address
bitcoinQR=Your Brave wallet QR code
Expand Down Expand Up @@ -89,6 +106,7 @@ totalAmount=Total Amount
receiptLink=Receipt Link
advanced=Advanced
rank=Rank
site=Site
views=Views
timeSpent=Time Spent
include=Include
Expand Down
12 changes: 11 additions & 1 deletion app/filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,17 @@ function updateDownloadState (downloadId, item, state) {
function registerForDownloadListener (session) {
session.on('will-download', function (event, item, webContents) {
const win = BrowserWindow.getFocusedWindow()
const defaultPath = path.join(getSetting(settings.DEFAULT_DOWNLOAD_SAVE_PATH) || app.getPath('downloads'), item.getFilename())

// special handling for data URLs where another 'will-download' event handler is trying to suggest a filename via item.setSavePath
// see the IPC handler for RENDER_URL_TO_PDF in app/index.js for example
let itemFilename
if (item.getURL().match(/^data:/) && item.getSavePath()) {
itemFilename = path.basename(item.getSavePath())
} else {
itemFilename = item.getFilename()
}

const defaultPath = path.join(getSetting(settings.DEFAULT_DOWNLOAD_SAVE_PATH) || app.getPath('downloads'), itemFilename)
const savePath = dialog.showSaveDialog(win, { defaultPath })
// User cancelled out of save dialog prompt
if (!savePath) {
Expand Down
1 change: 1 addition & 0 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const async = require('async')
const tabs = require('./browser/tabs')
const settings = require('../js/constants/settings')
const webtorrent = require('./browser/webtorrent')
const base64Encode = require('../js/lib/base64').encode

// temporary fix for #4517, #4518 and #4472
app.commandLine.appendSwitch('enable-use-zoom-for-dsf', 'false')
Expand Down
1 change: 1 addition & 0 deletions app/ledger.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const doAction = (action) => {
if (publisherInfo._internal.debugP) {
console.log('\napplication event: ' + JSON.stringify(underscore.pick(action, [ 'actionType', 'key' ]), null, 2))
}

switch (action.actionType) {
case appConstants.APP_IDLE_STATE_CHANGED:
visit('NOOP', underscore.now(), null)
Expand Down
77 changes: 77 additions & 0 deletions app/pdf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict'

const electron = require('electron')
const BrowserWindow = electron.BrowserWindow

const renderUrlToPdf = (appState, action, testingMode) => {
let url = action.url
let savePath = action.savePath
let openAfterwards = action.openAfterwards

let currentBw = BrowserWindow.getFocusedWindow()

let bw = new BrowserWindow({show: !!testingMode, backgroundColor: '#ffffff'})

let wv = bw.webContents

let whenReadyToGeneratePDF = () => {
wv.printToPDF({}, function (err, data) {
if (err) {
throw err
}

let pdfDataURI = 'data:application/pdf;base64,' + data.toString('base64')

// need to put our event handler first so we can set filename
// specifically, needs to execute ahead of app/filtering.js:registerForDownloadListener (which opens the dialog box)
let listeners = wv.session.listeners('will-download')
wv.session.removeAllListeners('will-download')

wv.downloadURL(pdfDataURI)
wv.session.once('will-download', function (event, item) {
if (savePath) {
item.setSavePath(savePath)
}

item.once('done', function (event, state) {
if (state === 'completed') {
let finalSavePath = item && item.getSavePath()

if (openAfterwards && savePath) {
currentBw.webContents.loadURL('file://' + finalSavePath)
}

if (bw && !testingMode) {
try {
bw.close()
} catch (exc) {}
}
}
})
})
// add back other event handlers (esp. add/filtering.js:registerForDownloadListener which opens the dialog box)
listeners.forEach(function (listener) {
wv.session.on('will-download', listener)
})
})
}

let afterLoaded = () => {
let removeCharEncodingArtifactJS = 'document.body.outerHTML = document.body.outerHTML.replace(/Â/g, "")'
wv.executeJavaScript(removeCharEncodingArtifactJS, whenReadyToGeneratePDF)
}

bw.loadURL(url)
wv.on('did-finish-load', afterLoaded)

return appState
}

module.exports = {
renderUrlToPdf
}

11 changes: 11 additions & 0 deletions js/about/aboutActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,17 @@ const aboutActions = {
actionType: appConstants.APP_DEFAULT_BROWSER_UPDATED,
useBrave: true
})
},

/**
* Dispatches a message to render a URL into a PDF file
*/
renderUrlToPdf: function (url, savePath) {
aboutActions.dispatchAction({
actionType: appConstants.APP_RENDER_URL_TO_PDF,
url: url,
savePath: savePath
})
}
}
module.exports = aboutActions
Loading

0 comments on commit fa04026

Please sign in to comment.