src/renderer/components/Lyft.vue
<template>
<div class="d-flex h-100 w-100 flex-column">
<header class="p-4 mb-auto">
<img
src="static/ride-receipts.svg"
alt="Ride Receipts"
width="253"
>
</header>
<main
class="mt-5"
@keyup.enter="submitForm"
>
<transition
name="fade"
mode="out-in"
>
<section
v-if="form === 'LOGIN_FORM'"
key="loginForm"
class="p-3 text-center"
>
<div class="row">
<div class="col-md-10 mx-auto">
<p class="sign-in-text mb-5">
Sign in to your Gmail account to automatically download your Lyft receipts. <i
id="privacy"
class="far fa-2x fa-question-circle"
/>
</p>
<p
v-if="!awaiting_code"
class="text-center"
>
<button
v-if="!loading"
type="button"
class="btn btn-lg btn-started"
@click="signInWithgoogle('google')"
>
Sign In to Gmail
</button>
</p>
<b-popover
ref="popover"
target="privacy"
triggers="click focus"
placement="bottom"
>
<template slot="title">
Privacy
</template>
Ride Receipts is an automation app that has no database; therefore, it does not store your login credentials, personal information or any other data. Once you log in, we’ll fetch your Uber or Lyft receipts and auto-generate PDFs for you.
<br>
<p class="text-right">
<a
class="js-external-link"
href="https://ridereceipts.io/privacy"
>Learn more</a>
</p>
</b-popover>
<div
v-if="awaiting_code"
class="col-md-10 mx-auto"
>
<div class="form-group">
<b-form-input
id="input-live"
v-model="approval_code"
placeholder="Enter approval code from browser"
trim
/>
</div>
<p class="text-center">
<button
type="button"
class="btn btn-lg btn-started"
@click="fetchGoogleToken('google')"
>
Submit
</button>
</p>
</div>
</div>
</div>
</section>
<section
v-if="form === 'FILTER_OPTION'"
key="filteroption"
>
<div class="row">
<div class="col-8 mx-auto">
<p class="sign-in-text text-center mb-5">
Which receipts would you like to <br> download?
</p>
</div>
<div class="col-7 mx-auto">
<div class="form-group">
<div class="row mx-auto">
<div class="col">
<div class="form-check">
<label class="form-check-label">
<input
v-model="filter_option"
class="form-check-input"
type="radio"
name="filter"
value="previousyear"
>
Previous year
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input
v-model="filter_option"
class="form-check-input"
type="radio"
name="filter"
value="currentyear"
>
Current year
</label>
</div>
</div>
<div class="col">
<div class="form-check">
<label class="form-check-label">
<input
v-model="filter_option"
class="form-check-input"
type="radio"
name="filter"
value="lastthreemonths"
>
Last 3 months
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input
v-model="filter_option"
class="form-check-input"
type="radio"
name="filter"
value="lastmonth"
>
Last month
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section
v-if="form === 'INVOICE_COUNT'"
key="invoicecounts"
>
<div class="row">
<div class="col-8 mx-auto">
<p class="sign-in-text mb-5 text-center">
{{ downloadingMessage }}
</p>
<br>
<div class="progress">
<div
class="progress-bar"
role="progressbar"
:style="{ width: progress + '%' }"
:aria-valuenow="progress"
aria-valuemin="0"
aria-valuemax="100"
/>
</div>
</div>
</div>
</section>
<section
v-if="form === 'DOWNLOADED'"
key="downloaded"
>
<div class="row">
<div class="col-8 mx-auto">
<p
v-if="invoiceCount > 0"
class="sign-in-text mb-4 text-center"
>
Success! All receipts have been downloaded for you.
</p>
<p
v-if="invoiceCount === 0"
class="sign-in-text mb-4 text-center"
>
{{ downloadingMessage }}
</p>
<div
v-if="invoiceCount > 0"
class="card-deck mb-5"
>
<div class="card">
<div class="card-body d-flex flex-row">
<img
src="static/rideshare-car.svg"
width="86"
class="mr-4"
>
<p class="card-text">
Number of trips<br>
<span
v-if="invoiceCount > 1"
class="trip-count"
>{{ invoiceCount }} trips</span>
<span
v-if="invoiceCount === 1"
class="trip-count"
>{{ invoiceCount }} trip</span>
</p>
</div>
</div>
<div class="card">
<div class="card-body">
<carousel
:navigation-enabled="navigation"
:pagination-enabled="pagination"
:per-page="perPage"
>
<slide
v-for="rate in rates"
:key="rate.currency"
class="d-flex flex-row"
>
<img
src="static/piggy-bank.svg"
width="86"
class="mr-4"
>
<p class="card-text">
Total spend<br>
<span class="trip-count">${{ Number.parseFloat(rate.amount.reduce((a, b) => a + b, 0) * 100 / 100).toFixed(2) }} {{ rate.currency }}</span>
</p>
</slide>
</carousel>
</div>
</div>
</div>
<p
v-if="invoiceCount > 0"
class="text-center"
>
<button
type="button"
class="btn btn-lg btn-started"
@click.stop.prevent="openInvoiceFolder()"
>
View Receipts
</button>
</p>
<p
v-if="invoiceCount > 0"
class="text-center"
>
Run again: <router-link
:to="{ name: 'uber'}"
class="mr-1 font-weight-bold"
tag="a"
>
Uber
</router-link> <a
href="#"
class="mr-1 font-weight-bold"
@click="startAgain"
>Lyft</a>
</p>
<p
v-if="invoiceCount === 0"
class="text-center"
>
<router-link
:to="{ name: 'main-page' }"
class="btn btn-lg btn-started"
tag="button"
>
Start again
</router-link>
</p>
</div>
</div>
</section>
<section
v-if="form === 'ERROR'"
key="debugturnedon"
>
<div class="row">
<div class="col-10 mx-auto">
<p class="sign-in-text text-center">
It seems like you have turned on debug mode. Please turn it off and click on start again button below.
</p>
<br>
<p
v-if="invoiceCount === 0"
class="text-center"
>
<router-link
:to="{ name: 'main-page' }"
class="btn btn-lg btn-started text-center mx-auto"
tag="button"
>
Start again
</router-link>
</p>
</div>
</div>
</section>
</transition>
</main>
<footer
v-if="form === 'DOWNLOADED'"
class="mt-auto"
/>
<footer
v-if="form === 'DOWNLOADED'"
class="mt-auto p-4"
>
<div class="row">
<div class="col-md-10 mx-auto">
<p class="text-center">
<a
href="https://ridereceipts.io"
class="upgrade-link js-external-link"
>Upgrade to Ride Receipts PRO and get an itemized Excel doc of all your trips. <svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-arrow-right"
><line
x1="5"
y1="12"
x2="19"
y2="12"
/><polyline points="12 5 19 12 12 19" /></svg></a>
</p>
</div>
</div>
</footer>
<footer
v-if="form !== 'DOWNLOADED'"
class="mt-auto p-4"
>
<div class="row">
<div class="col">
<router-link
v-if="!hideBackButton"
:to="{ name: 'main-page' }"
tag="button"
class="btn btn-outline-primary btn--submit back-btn float-left mt-3"
>
<img
class="arrow"
src="static/back-arrow.svg"
>Back
</router-link>
</div>
<div class="col">
<button
v-if="!hideButton"
type="button"
class="btn btn-outline-primary btn--submit float-right mt-3"
@click="submitForm"
>
Next<img
class="arrow"
src="static/next-arrow.svg"
>
</button>
</div>
</div>
</footer>
</div>
</template>
<script>
import oauth from '../services/oauth'
import dayjs from 'dayjs'
import axios from 'axios'
import _ from 'lodash'
import cheerio from 'cheerio'
import Store from 'electron-store'
const store = new Store()
export default {
data () {
return {
form: 'LOGIN_FORM',
approval_code: null,
awaiting_code: false,
filter_option: null,
loading: false,
downloadingMessage: null,
totalAmount: [],
pagination: false,
perPage: 1,
rates: [],
invoiceCount: 0,
navigation: true,
progress: ''
}
},
computed: {
hideBackButton () {
if (this.form === 'INVOICE_COUNT') {
return true
}
if (this.form === 'FILTER_OPTION') {
return true
}
if (this.form === 'ERROR') {
return true
}
if (this.form === 'DOWNLOADED') {
return true
}
return false
},
hideButton () {
if (this.form === 'LOGIN_FORM') {
return true
}
if (this.form === 'INVOICE_COUNT') {
return true
}
if (this.form === 'DOWNLOADED') {
return true
}
if (this.form === 'ERROR') {
return true
}
return false
}
},
mounted () {
if (store.get('debug')) {
this.form = 'ERROR'
}
},
methods: {
startAgain () {
this.form = 'LOGIN_FORM'
this.filter_option = null
this.loading = false
this.downloadingMessage = null
this.totalAmount = []
this.pagination = false
this.perPage = 1
this.rates = []
this.invoiceCount = 0
this.navigation = true
this.progress = ''
this.awaiting_code = false
this.approval_code = null
},
downloadMessage (count) {
if (count > 76) {
this.downloadingMessage = `Wow this could take a while! Let Ride Receipts do its thing and we'll let you know once your ${count} trips are in order.`
} else if (count > 56 && count <= 76) {
this.downloadingMessage = `Whoa ${count} trips! Put your feet up and relax. This will take a while, my friend.`
} else if (count > 46 && count <= 56) {
this.downloadingMessage = `You have ${count} trips. Give the little robot some time to download and organize them for you.`
} else if (count > 36 && count <= 46) {
this.downloadingMessage = `Whoa someone's been busy! You have ${count} trips. Downloading now.`
} else if (count > 26 && count <= 36) {
this.downloadingMessage = `You have ${count} trips. Downloading and organizing them for you now. Sweet deal, huh?`
} else if (count > 11 && count <= 26) {
this.downloadingMessage = `You have ${count} trips. Pour yourself a drink and relax. We got this.`
} else if (count > 6 && count <= 11) {
this.downloadingMessage = `You have ${count} trips! This should download fairly quickly.`
} else if (count > 1 && count <= 6) {
this.downloadingMessage = `Running this app for just ${count} trips?\nThat's okay, we won't judge ;)`
} else if (count === 1) {
this.downloadingMessage = `Running this app for just ${count} trip?\nThat's okay, we won't judge ;)`
} else {
this.form = 'DOWNLOADED'
this.downloadingMessage = 'You have 0 trips within the time frame you selected.'
}
},
async fetchGoogleToken () {
const token = await oauth.fetchToken('google', this.approval_code)
if (token) {
const profile = await oauth.fetchGoogleProfile('google', token.access_token)
this.loading = false
localStorage.setItem('provider', 'google')
localStorage.setItem('token_data', JSON.stringify(token))
localStorage.setItem('user_data', JSON.stringify(profile))
this.form = 'FILTER_OPTION'
}
},
signInWithgoogle (provider) {
const authUrl = oauth.buildAuthUrl(provider)
this.$electron.shell.openExternal(authUrl)
this.awaiting_code = true
},
async submitForm () {
let startDate, endDate
const user = JSON.parse(localStorage.getItem('user_data'))
let messages
const self = this
if (this.filter_option === 'currentyear') {
startDate = dayjs().startOf('year').unix()
endDate = dayjs().endOf('year').unix()
} else if (this.filter_option === 'previousyear') {
startDate = dayjs().subtract(1, 'years').startOf('year').unix()
endDate = dayjs().subtract(1, 'years').endOf('year').unix()
} else if (this.filter_option === 'lastmonth') {
startDate = dayjs().subtract(1, 'month').startOf('month').unix()
endDate = dayjs().startOf('month').unix()
} else if (this.filter_option === 'lastthreemonths') {
startDate = dayjs().subtract(3, 'month').startOf('month').unix()
endDate = dayjs().startOf('month').unix()
} else {
return
}
const emails = []
let nextToken = null
do {
let apiUrl
if (nextToken) {
apiUrl = `https://www.googleapis.com/gmail/v1/users/me/messages?pageToken=${nextToken}&q='from:"Lyft Ride Receipt" after:${startDate} before:${endDate}'`
} else {
apiUrl = `https://www.googleapis.com/gmail/v1/users/me/messages?q='from:"Lyft Ride Receipt" after:${startDate} before:${endDate}'`
}
const list = await axios.get(encodeURI(apiUrl), {
headers: {
Authorization: `Bearer ${JSON.parse(localStorage.getItem('token_data')).access_token}`
}
})
if (typeof list.data.messages === 'undefined') {
this.invoiceCount = 0
this.downloadMessage(0)
self.form = 'DOWNLOADED'
return
}
if (list.data.messages.length > 0) {
for (let i = 0; i < list.data.messages.length; i++) {
emails.push(list.data.messages[i])
}
}
if (typeof list.data.nextPageToken !== 'undefined') {
nextToken = list.data.nextPageToken
} else {
nextToken = null
}
} while (nextToken !== null)
if (emails.length === 0) {
this.invoiceCount = 0
this.downloadMessage(0)
self.form = 'DOWNLOADED'
} else {
this.downloadMessage(emails.length)
messages = emails
this.invoiceCount = messages.length
if (messages.length > 0) {
this.form = 'INVOICE_COUNT'
}
if (typeof messages !== 'undefined') {
for (let i = 0; i < messages.length; i++) {
const data = await axios.get(`https://www.googleapis.com/gmail/v1/users/me/messages/${messages[i].id}`, {
headers: {
Authorization: `Bearer ${JSON.parse(localStorage.getItem('token_data')).access_token}`
}
})
const processed = self.processEmails(data.data, user)
console.log(processed)
if (processed) {
const number = i + 1
self.progress = _.ceil(_.divide(number, messages.length) * 100)
}
if (self.progress === 100) {
self.form = 'DOWNLOADED'
const notification = new Notification('Ride Receipts', {
body: 'Success! All receipts have been downloaded for you.'
})
notification.onclick = () => {
console.log('Notification clicked')
}
}
}
}
}
},
processEmails (data, user) {
let html
const htmlData = _.find(data.payload.parts, { mimeType: 'text/html' })
if (htmlData) {
html = Buffer.from(htmlData.body.data, 'base64')
} else {
html = Buffer.from(data.payload.body.data, 'base64')
}
const date = new Date(parseInt(data.internalDate))
const dom = cheerio.load(html.toString(), {
normalizeWhitespace: true
})
const totalRate = parseFloat(_.trim(dom('span.p-amount').text()))
const currency = _.trim(dom('span.p-currency').text())
const check = _.findIndex(this.rates, ['currency', currency])
if (check < 0) {
this.rates.push({
currency: currency,
amount: [totalRate]
})
} else {
this.rates[check].amount.push(totalRate)
}
if (this.rates.length === 1) {
this.navigation = false
} else {
this.navigation = true
}
this.$electron.ipcRenderer.send('downloadPDF', {
email: user.email,
date: date,
year: dayjs(date).format('YYYY'),
invoiceDate: dayjs(date).format('MMMM-DD-YYYY_hh-mm-a'),
html: `data:text/html;charset=UTF-8,${encodeURIComponent(html)}`,
rideType: 'Lyft'
})
setTimeout(() => { console.log('Resting.....') }, 2000)
return true
},
openInvoiceFolder () {
const documentDir = this.$electronstore.get('invoicePath')
this.$electron.shell.openItem(documentDir)
}
}
}
</script>