├── test ├── unit │ ├── mock │ │ ├── index.css │ │ ├── styleMock.js │ │ ├── electron.js │ │ └── keytar.js │ ├── specs │ │ ├── importers.vue.spec.js │ │ ├── transactions.spec.js │ │ └── credentials.spec.js │ └── helpers │ │ └── baseStore.js ├── .eslintrc └── e2e │ ├── specs │ ├── Just.spec.js │ └── Launch.spec.js │ ├── jest.e2e.config.js │ ├── utils │ ├── element.js │ └── interactions.js │ └── screenshotEnvironment.js ├── nuxt ├── .eslintignore ├── test │ ├── .eslintrc │ ├── __snapshots__ │ │ ├── CallToAction.spec.js.snap │ │ ├── Hero.spec.js.snap │ │ ├── Logo.spec.js.snap │ │ ├── index.spec.js.snap │ │ ├── Teasers.spec.js.snap │ │ ├── FeatureRight.spec.js.snap │ │ ├── FeatureLeft.spec.js.snap │ │ ├── Features.spec.js.snap │ │ ├── CallToActionContent.spec.js.snap │ │ ├── TeaserColumn.spec.js.snap │ │ ├── Prices.spec.js.snap │ │ ├── HeroContent.spec.js.snap │ │ ├── PriceHighlighted.spec.js.snap │ │ ├── Price.spec.js.snap │ │ ├── TheHeader.spec.js.snap │ │ └── TheFooter.spec.js.snap │ ├── Hero.spec.js │ ├── index.spec.js │ ├── Prices.spec.js │ ├── Teasers.spec.js │ ├── Features.spec.js │ ├── TheFooter.spec.js │ ├── HeroContent.spec.js │ ├── CallToAction.spec.js │ ├── CallToActionContent.spec.js │ ├── FeatureLeft.spec.js │ ├── FeatureRight.spec.js │ ├── Price.spec.js │ ├── PriceHighlighted.spec.js │ ├── TeaserColumn.spec.js │ ├── Logo.spec.js │ └── TheHeader.spec.js ├── husky.config.js ├── assets │ ├── img │ │ ├── hero.png │ │ ├── main-screenshot.png │ │ ├── wave-top.svg │ │ ├── hero-wave.svg │ │ └── cloud_files.svg │ ├── css │ │ ├── tailwind.css │ │ └── qa.css │ └── README.md ├── static │ └── favicon.ico ├── content │ └── FAQ │ │ ├── 10.no-credit.md │ │ ├── 30.import-dont-work.md │ │ ├── 20.how-long.md │ │ └── index.js ├── jsconfig.json ├── .babelrc ├── layouts │ ├── README.md │ ├── default.vue │ └── dashboard.vue ├── tailwind.config.js ├── globals.config.js ├── components │ ├── CallToAction.vue │ ├── Hero.vue │ ├── FAQ.vue │ ├── QaA.vue │ ├── SectionTitle.vue │ ├── FeatureRight.vue │ ├── Logo.vue │ ├── FeatureLeft.vue │ ├── CallToActionContent.vue │ ├── Features.vue │ ├── HeroContent.vue │ ├── Price.vue │ ├── PriceHighlighted.vue │ ├── Prices.vue │ ├── Teasers.vue │ ├── TeaserColumn.vue │ ├── TheFooter.vue │ ├── TheHeader.vue │ └── DownloadButton.vue ├── mixins │ └── price.js ├── jest.config.js ├── pages │ └── index.vue ├── LICENSE ├── .eslintrc.js ├── package.json ├── .gitignore ├── nuxt.config.js └── README.md ├── babel.config.js ├── docs ├── img │ ├── _ibsd.png │ ├── share.png │ └── insert-mail.png └── share-spreadsheet.md ├── public ├── favicon.ico └── index.html ├── src ├── assets │ └── logo.png ├── plugins │ ├── logger.js │ └── vuetify.js ├── store │ ├── migrations │ │ ├── index.js │ │ ├── 1.js │ │ └── 2.js │ ├── modules │ │ ├── Migrations.js │ │ ├── index.js │ │ ├── Exporters.js │ │ ├── Transactions.js │ │ └── Importers.js │ └── index.js ├── App.vue ├── modules │ ├── dates.js │ ├── encryption │ │ ├── salt.js │ │ ├── keytar.js │ │ ├── crypto.js │ │ └── credentials.js │ ├── hash.js │ ├── downloadChromium.js │ ├── filesystem.js │ ├── reporting │ │ └── index.js │ ├── scrapers.js │ ├── spreadsheet │ │ ├── googleOAuth2.js │ │ └── spreadsheet.js │ └── transactions.js ├── router │ └── index.js ├── logger.js ├── main.js ├── components │ ├── MainPage │ │ ├── LogSheet.vue │ │ ├── Importers │ │ │ ├── DeleteImporterDialog.vue │ │ │ ├── AddScraper.vue │ │ │ └── Importer.vue │ │ ├── DataTable.vue │ │ ├── ProfileChip.vue │ │ ├── Exporters │ │ │ ├── JsonExporter.vue │ │ │ └── SpreadsheetExporter.vue │ │ ├── Exporters.vue │ │ ├── Importers.vue │ │ └── ReportProblemDialog.vue │ └── MainPage.vue └── background.js ├── globals.js ├── jsconfig.json ├── .snyk ├── .github ├── dependabot.yml └── workflows │ ├── nuxt.yml │ ├── ci.yml │ └── release.yml ├── jest.config.js ├── LICENSE ├── .gitignore ├── .eslintrc.js ├── vue.config.js ├── package.json ├── scripts └── sentry-symbols.js └── README.md /test/unit/mock/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/unit/mock/styleMock.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nuxt/.eslintignore: -------------------------------------------------------------------------------- 1 | .nuxt 2 | dist 3 | .eslintrc.js 4 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /nuxt/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /nuxt/husky.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | 'pre-push': 'yarn lint', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/img/_ibsd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baruchiro/israeli-bank-scrapers-desktop/HEAD/docs/img/_ibsd.png -------------------------------------------------------------------------------- /docs/img/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baruchiro/israeli-bank-scrapers-desktop/HEAD/docs/img/share.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baruchiro/israeli-bank-scrapers-desktop/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baruchiro/israeli-bank-scrapers-desktop/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /test/unit/mock/electron.js: -------------------------------------------------------------------------------- 1 | export const ipcRenderer = { 2 | on: jest.fn(), 3 | send: jest.fn(), 4 | }; 5 | -------------------------------------------------------------------------------- /globals.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'SENTRY_DSN', 3 | 'GOOGLE_CLIENT_ID', 4 | 'GOOGLE_CLIENT_SECRET', 5 | ]; 6 | -------------------------------------------------------------------------------- /docs/img/insert-mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baruchiro/israeli-bank-scrapers-desktop/HEAD/docs/img/insert-mail.png -------------------------------------------------------------------------------- /nuxt/assets/img/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baruchiro/israeli-bank-scrapers-desktop/HEAD/nuxt/assets/img/hero.png -------------------------------------------------------------------------------- /nuxt/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baruchiro/israeli-bank-scrapers-desktop/HEAD/nuxt/static/favicon.ico -------------------------------------------------------------------------------- /src/plugins/logger.js: -------------------------------------------------------------------------------- 1 | export default { 2 | install(Vue, { logger }) { 3 | Vue.prototype.$logger = logger; 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /nuxt/assets/img/main-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baruchiro/israeli-bank-scrapers-desktop/HEAD/nuxt/assets/img/main-screenshot.png -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | }); 8 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["./src/*"] 6 | } 7 | }, 8 | "exclude": ["nuxt"] 9 | } -------------------------------------------------------------------------------- /src/store/migrations/index.js: -------------------------------------------------------------------------------- 1 | const files = require.context('.', false, /\d+\.js$/); 2 | 3 | export default files.keys().map((key) => files(key).default).sort((a, b) => a.number - b.number); 4 | -------------------------------------------------------------------------------- /nuxt/assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | @import "./qa.css"; 5 | 6 | * { 7 | direction: rtl; 8 | } 9 | -------------------------------------------------------------------------------- /nuxt/content/FAQ/10.no-credit.md: -------------------------------------------------------------------------------- 1 | # למה לא רואים פירוט אשראי? 2 | 3 | אמנם נכון שחלק מהבנקים מציגים גם פירוט הוצאות מהאשראי, אבל כדי לראות את פירוט האשראי בעו"שי, יש להוסיף את פרטי האשראי כדי למשוך את הנתונים המדויקים ישירות מאתר האשראי. 4 | -------------------------------------------------------------------------------- /test/e2e/specs/Just.spec.js: -------------------------------------------------------------------------------- 1 | // Remove when https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/625 closed 2 | describe('All test should pass', () => { 3 | test('Realy', () => { 4 | expect(1).toBe(1); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /test/e2e/jest.e2e.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testRunner: 'jest-circus/runner', 3 | preset: '@vue/cli-plugin-unit-jest', 4 | testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], 5 | testEnvironment: './screenshotEnvironment.js', 6 | }; 7 | -------------------------------------------------------------------------------- /nuxt/content/FAQ/30.import-dont-work.md: -------------------------------------------------------------------------------- 1 | # למה לא קורה כלום אחרי שלחצתי Import? 2 | 3 | בפעם הראשונה שלוחצים Import, עו"שי מוריד את המנוע של דפדפן כרום כדי להיכנס לאתר הבנק בדיוק כמו שאתה בעצמך נכנס. 4 | 5 | אם אתה גולש על אינטרנט סלולרי זה יכול לקחת הרבה זמן. 6 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /nuxt/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": ["./*"], 6 | "@/*": ["./*"], 7 | "~~/*": ["./*"], 8 | "@@/*": ["./*"] 9 | } 10 | }, 11 | "exclude": ["node_modules", ".nuxt", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /nuxt/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/dates.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | const DATE_FORMAT = 'DD/MM/YY HH:mm'; 4 | 5 | export function formatDate(date) { 6 | return moment(date, moment.ISO).format(DATE_FORMAT); 7 | } 8 | 9 | export function unixMilli(date) { 10 | return moment(date).valueOf(); 11 | } 12 | -------------------------------------------------------------------------------- /nuxt/layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Application Layouts. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts). 8 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/CallToAction.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CallToAction renders properly 1`] = ` 4 | "
\\"\\" 5 | 6 |
" 7 | `; 8 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - israeli-bank-scrapers-core > lodash: 8 | patched: '2020-04-30T22:04:51.539Z' 9 | -------------------------------------------------------------------------------- /nuxt/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** TailwindCSS Configuration File 3 | ** 4 | ** Docs: https://tailwindcss.com/docs/configuration 5 | ** Default: https://github.com/tailwindcss/tailwindcss/blob/master/stubs/defaultConfig.stub.js 6 | */ 7 | module.exports = { 8 | theme: {}, 9 | variants: {}, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/Hero.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Hero renders properly 1`] = ` 4 | "
5 | 6 |
\\"\\"
7 |
" 8 | `; 9 | -------------------------------------------------------------------------------- /nuxt/assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). 8 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/Logo.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Logo renders properly 1`] = ` 4 | " 5 |  עו\\"שי 6 | " 7 | `; 8 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/index.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`index renders properly 1`] = ` 4 | "
5 | 6 | 7 | 8 | 9 | 10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /src/modules/encryption/salt.js: -------------------------------------------------------------------------------- 1 | import { loadSALT, saveSALT } from './keytar'; 2 | 3 | export default async function SALT(defaultValue) { 4 | const existedSALT = await loadSALT(); 5 | if (existedSALT) return existedSALT; 6 | 7 | if (!defaultValue) throw Error('SALT not exist'); 8 | 9 | await saveSALT(defaultValue); 10 | return loadSALT(); 11 | } 12 | -------------------------------------------------------------------------------- /src/store/migrations/1.js: -------------------------------------------------------------------------------- 1 | import { randomHex } from '@/modules/encryption/crypto'; 2 | 3 | export default { 4 | number: 1, 5 | migration(state) { 6 | state.Importers.importers = state.Importers.importers.map((importer) => { 7 | importer.id = importer.id || randomHex(); 8 | return importer; 9 | }); 10 | return state; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /test/unit/mock/keytar.js: -------------------------------------------------------------------------------- 1 | const fakeKeytarValut = { 2 | crypto: 'AAAAAA', 3 | }; 4 | export default { 5 | getPassword(_serviceName, accountName) { 6 | return Promise.resolve(fakeKeytarValut[accountName]); 7 | }, 8 | setPassword(_serviceName, accountName, password) { 9 | fakeKeytarValut[accountName] = password; 10 | return Promise.resolve(); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/modules/hash.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | 3 | export default (str) => { 4 | if (str.length === 0) return 0; 5 | 6 | let hash = 0; 7 | let i; 8 | let chr; 9 | for (i = 0; i < str.length; i++) { 10 | chr = str.charCodeAt(i); 11 | hash = ((hash << 5) - hash) + chr; 12 | hash |= 0; // Convert to 32bit integer 13 | } 14 | return hash; 15 | }; 16 | -------------------------------------------------------------------------------- /src/store/migrations/2.js: -------------------------------------------------------------------------------- 1 | export default { 2 | number: 2, 3 | migration(state) { 4 | state.Importers.importers = state.Importers.importers.map((importer) => { 5 | if (importer.key === 'leumiCard') { 6 | importer.key = 'max'; 7 | importer.name = 'Max'; 8 | } 9 | return importer; 10 | }); 11 | 12 | return state; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/store/modules/Migrations.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | last: 0, 3 | }; 4 | 5 | const mutations = { 6 | update(state, data) { 7 | state.last = data; 8 | }, 9 | }; 10 | 11 | const actions = { 12 | updateLast({ commit }, payload) { 13 | commit('update', parseInt(payload, 10)); 14 | }, 15 | }; 16 | 17 | export default { 18 | state, 19 | mutations, 20 | actions, 21 | }; 22 | -------------------------------------------------------------------------------- /nuxt/content/FAQ/20.how-long.md: -------------------------------------------------------------------------------- 1 | # כמה זמן לוקח לייבא את הנתונים? 2 | 3 | תלוי במהירות הגלישה, אבל בכל מקרה לא יותר מדקה (מלבד הפעם הראשונה). 4 | 5 | במידה ומהירות הגלישה נמוכה מידי, עו"שי לא יצליח לייבא את הנתונים. 6 | 7 | במידה והסיסמה שגויה, ייתכן שייקח מעט יותר זמן לגלות זאת. 8 | 9 | בכל אופן, במידה ויש ספק מומלץ לסמן את כפתור **Show Browser** כדי לראות בפועל את תהליך הורדת הנתונים ולזהות איפה יש בעיה. 10 | -------------------------------------------------------------------------------- /nuxt/globals.config.js: -------------------------------------------------------------------------------- 1 | const faq = require('./content/FAQ'); 2 | 3 | const globals = { 4 | FAQ_STRINGS_LIST: faq, 5 | GITHUB_REPO: 'baruchiro/israeli-bank-scrapers-desktop', 6 | }; 7 | 8 | const stringified = Object.keys(globals).reduce((acc, globalName) => { 9 | acc[globalName] = JSON.stringify(globals[globalName]); 10 | return acc; 11 | }, {}); 12 | 13 | module.exports = { 14 | globals, 15 | stringified, 16 | }; 17 | -------------------------------------------------------------------------------- /test/e2e/utils/element.js: -------------------------------------------------------------------------------- 1 | export default class Element { 2 | constructor(client, jsonElement) { 3 | this.client = client; 4 | this.id = jsonElement.ELEMENT; 5 | this.json = jsonElement; 6 | } 7 | 8 | async click() { 9 | return this.client.elementIdClick(this.id); 10 | } 11 | 12 | async isVisible() { 13 | return this.client.elementIdDisplayed(this.id) 14 | .then((result) => result.value); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import MainPage from '../components/MainPage'; 4 | 5 | Vue.use(VueRouter); 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'MainPage', 11 | component: MainPage, 12 | }, 13 | ]; 14 | 15 | const router = new VueRouter({ 16 | mode: 'history', 17 | base: process.env.BASE_URL, 18 | routes, 19 | }); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /src/store/modules/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The file enables `@/store/index.js` to import all vuex modules 3 | * in a one-shot manner. There should not be any reason to edit this file. 4 | */ 5 | 6 | const files = require.context('.', false, /\.js$/); 7 | const modules = {}; 8 | 9 | files.keys().forEach((key) => { 10 | if (key === './index.js') return; 11 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default; 12 | }); 13 | export default modules; 14 | -------------------------------------------------------------------------------- /nuxt/components/CallToAction.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | reviewers: 8 | - baruchiro 9 | assignees: 10 | - baruchiro 11 | target-branch: "upgrades" 12 | ignore: 13 | - dependency-name: electron 14 | versions: 15 | - "> 5.0.13" 16 | 17 | - package-ecosystem: npm 18 | directory: "/nuxt/" 19 | schedule: 20 | interval: monthly 21 | target-branch: "nuxt-upgrades" 22 | -------------------------------------------------------------------------------- /nuxt/components/Hero.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /nuxt/test/Hero.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import Hero from '@/components/Hero'; 3 | 4 | const factory = () => shallowMount(Hero); 5 | 6 | describe('Hero', () => { 7 | it('is a Vue instance', () => { 8 | const wrapper = factory(); 9 | expect(wrapper.isVueInstance()).toBeTruthy(); 10 | }); 11 | 12 | it('renders properly', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /nuxt/test/index.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import index from '@/pages/index'; 3 | 4 | const factory = () => shallowMount(index); 5 | 6 | describe('index', () => { 7 | it('is a Vue instance', () => { 8 | const wrapper = factory(); 9 | expect(wrapper.isVueInstance()).toBeTruthy(); 10 | }); 11 | 12 | it('renders properly', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /nuxt/test/Prices.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import Prices from '@/components/Prices'; 3 | 4 | const factory = () => shallowMount(Prices); 5 | 6 | describe('Prices', () => { 7 | it('is a Vue instance', () => { 8 | const wrapper = factory(); 9 | expect(wrapper.isVueInstance()).toBeTruthy(); 10 | }); 11 | 12 | it('renders properly', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /nuxt/test/Teasers.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import Teasers from '@/components/Teasers'; 3 | 4 | const factory = () => shallowMount(Teasers); 5 | 6 | describe('Teasers', () => { 7 | it('is a Vue instance', () => { 8 | const wrapper = factory(); 9 | expect(wrapper.isVueInstance()).toBeTruthy(); 10 | }); 11 | 12 | it('renders properly', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /nuxt/test/Features.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import Features from '@/components/Features'; 3 | 4 | const factory = () => shallowMount(Features); 5 | 6 | describe('Features', () => { 7 | it('is a Vue instance', () => { 8 | const wrapper = factory(); 9 | expect(wrapper.isVueInstance()).toBeTruthy(); 10 | }); 11 | 12 | it('renders properly', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /nuxt/test/TheFooter.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import TheFooter from '@/components/TheFooter'; 3 | 4 | const factory = () => shallowMount(TheFooter); 5 | 6 | describe('TheFooter', () => { 7 | it('is a Vue instance', () => { 8 | const wrapper = factory(); 9 | expect(wrapper.isVueInstance()).toBeTruthy(); 10 | }); 11 | 12 | it('renders properly', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /nuxt/content/FAQ/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const sortFiles = (a, b) => { 5 | const num1 = parseInt(a.split('.')[0], 10); 6 | const num2 = parseInt(b.split('.')[0], 10); 7 | 8 | return num1 - num2; 9 | }; 10 | 11 | const allMD = fs.readdirSync(__dirname).filter((file) => path.extname(file) === '.md').sort(sortFiles); 12 | const allContent = allMD.map((file) => fs.readFileSync(path.join(__dirname, file)).toString()); 13 | 14 | module.exports = allContent; 15 | -------------------------------------------------------------------------------- /nuxt/mixins/price.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | name: { 4 | type: String, 5 | required: true, 6 | }, 7 | price: { 8 | type: String, 9 | required: true, 10 | }, 11 | limited: { 12 | type: Boolean, 13 | default: true, 14 | }, 15 | list: { 16 | type: Array, 17 | required: true, 18 | }, 19 | }, 20 | computed: { 21 | pricePer() { 22 | return this.limited ? 'for one user' : '/ per user'; 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /nuxt/test/HeroContent.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import HeroContent from '@/components/HeroContent'; 3 | 4 | const factory = () => shallowMount(HeroContent); 5 | 6 | describe('HeroContent', () => { 7 | it('is a Vue instance', () => { 8 | const wrapper = factory(); 9 | expect(wrapper.isVueInstance()).toBeTruthy(); 10 | }); 11 | 12 | it('renders properly', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /nuxt/test/CallToAction.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import CallToAction from '@/components/CallToAction'; 3 | 4 | const factory = () => shallowMount(CallToAction); 5 | 6 | describe('CallToAction', () => { 7 | it('is a Vue instance', () => { 8 | const wrapper = factory(); 9 | expect(wrapper.isVueInstance()).toBeTruthy(); 10 | }); 11 | 12 | it('renders properly', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/Teasers.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Teasers renders properly 1`] = ` 4 | "
5 |
6 | עקרונות האבטחה 7 | 8 | 9 |
10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /src/modules/downloadChromium.js: -------------------------------------------------------------------------------- 1 | import download from 'download-chromium'; 2 | import { getPuppeteerConfig } from 'israeli-bank-scrapers-core'; 3 | 4 | const revision = getPuppeteerConfig().chromiumRevision; 5 | 6 | export default async (installPath, onProgress) => { 7 | onProgress({ percent: 0.02, message: 'Step 1: Downloading Chrome...' }); 8 | // onProgress: track download progress. receives one argument { percent, transferred, total } 9 | return download({ 10 | revision, 11 | installPath, 12 | onProgress, 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /nuxt/test/CallToActionContent.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import CallToActionContent from '@/components/CallToActionContent'; 3 | 4 | const factory = () => shallowMount(CallToActionContent); 5 | 6 | describe('CallToActionContent', () => { 7 | it('is a Vue instance', () => { 8 | const wrapper = factory(); 9 | expect(wrapper.isVueInstance()).toBeTruthy(); 10 | }); 11 | 12 | it('renders properly', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.html()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /nuxt/jest.config.js: -------------------------------------------------------------------------------- 1 | const { globals } = require('./globals.config'); 2 | 3 | module.exports = { 4 | moduleNameMapper: { 5 | '^@/(.*)$': '/$1', 6 | '^~/(.*)$': '/$1', 7 | '^vue$': 'vue/dist/vue.common.js', 8 | }, 9 | moduleFileExtensions: ['js', 'vue', 'json'], 10 | transform: { 11 | '^.+\\.js$': 'babel-jest', 12 | '.*\\.(vue)$': 'vue-jest', 13 | }, 14 | collectCoverage: true, 15 | collectCoverageFrom: [ 16 | '/components/**/*.vue', 17 | '/pages/**/*.vue', 18 | ], 19 | globals, 20 | }; 21 | -------------------------------------------------------------------------------- /src/modules/filesystem.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | export function readFileIfExist(filename) { 4 | try { 5 | return fs.readFileSync(filename).toString(); 6 | } catch (err) { 7 | if (err.code === 'ENOENT') { 8 | return ''; 9 | } 10 | throw err; 11 | } 12 | } 13 | 14 | export function readFileToObject(filename, defaultObject) { 15 | const content = readFileIfExist(filename); 16 | if (content && content.trim()) { 17 | return JSON.parse(content); 18 | } 19 | return defaultObject; 20 | } 21 | 22 | export function writeFile(filename, content) { 23 | fs.writeFileSync(filename, content); 24 | } 25 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/FeatureRight.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FeatureRight renders properly 1`] = ` 4 | "
5 |
6 |

7 | Lorem Ipsum 8 |

9 |

10 | Lorem Ipsum dolor sit 11 |

Images from: 12 | undraw.co

13 |
14 |
15 |
" 16 | `; 17 | -------------------------------------------------------------------------------- /src/store/modules/Exporters.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | JsonExporter: { 3 | 4 | }, 5 | }; 6 | 7 | const mutations = { 8 | setProperties(state, { name, properties }) { 9 | state[name] = properties; 10 | }, 11 | }; 12 | 13 | const actions = { 14 | saveExporterProperties({ commit }, payload) { 15 | if (!payload.name) throw new Error('The payload must include a \'name\' property'); 16 | if (!payload.properties) throw new Error('The payload must include a \'properties\' property'); 17 | commit('setProperties', payload); 18 | }, 19 | }; 20 | 21 | export default { 22 | state, 23 | mutations, 24 | actions, 25 | }; 26 | -------------------------------------------------------------------------------- /src/modules/encryption/keytar.js: -------------------------------------------------------------------------------- 1 | import keytar from 'keytar'; 2 | 3 | const serviceName = 'israeli-bank-scrapers-desktop'; 4 | const accountName = 'crypto'; 5 | 6 | export async function loadSALT() { 7 | return keytar.getPassword(serviceName, accountName); 8 | } 9 | 10 | export async function saveSALT(newSALT) { 11 | return keytar.setPassword(serviceName, accountName, newSALT); 12 | } 13 | 14 | export async function saveIntoAccount(account, password) { 15 | return keytar.setPassword(serviceName, account, password); 16 | } 17 | 18 | export async function getFromAccount(account) { 19 | return keytar.getPassword(serviceName, account); 20 | } 21 | -------------------------------------------------------------------------------- /nuxt/components/FAQ.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | -------------------------------------------------------------------------------- /nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 29 | -------------------------------------------------------------------------------- /nuxt/components/QaA.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 34 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | import logger from 'electron-log'; 2 | import fs from 'fs'; 3 | import { EOL } from 'os'; 4 | 5 | export default function CreateLogger(app) { 6 | logger.info(`Welcome to ${app.getName()} log`); 7 | logger.info(`Version: ${app.getVersion()}`); 8 | 9 | const onError = (error) => { 10 | logger.error(error.message ? error.message : error); 11 | if (error.stack) logger.trace(error.stack); 12 | }; 13 | logger.catchErrors({ onError }); 14 | 15 | logger.getLastLines = (n) => { 16 | const lines = fs.readFileSync(logger.transports.file.getFile().path).toString().split(EOL); 17 | const lastLines = lines.slice(lines.length - n); 18 | return lastLines.join(EOL); 19 | }; 20 | 21 | return logger; 22 | } 23 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/FeatureLeft.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FeatureLeft renders properly 1`] = ` 4 | "
5 |
6 |
7 |
8 |

9 | Lorem Ipsum 10 |

11 |

12 | Lorem Ipsum dolor sit 13 |

Images from: 14 | undraw.co

15 |
16 |
17 |
" 18 | `; 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { defaults } = require('jest-config'); 2 | 3 | module.exports = { 4 | preset: '@vue/cli-plugin-unit-jest', 5 | testMatch: ['**/__test__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], 6 | roots: ['test/unit', 'src'], 7 | moduleFileExtensions: [...defaults.moduleFileExtensions, 'vue'], 8 | moduleDirectories: [...defaults.moduleDirectories, 'src'], 9 | transformIgnorePatterns: [ 10 | 'node_modules/(?!(babel-jest|jest-vue-preprocessor)/)', 11 | ], 12 | moduleNameMapper: { 13 | '\\.css$': 'identity-obj-proxy', 14 | electron: '/test/unit/mock/electron.js', 15 | '^keytar$': '/test/unit/mock/keytar.js', 16 | }, 17 | transform: { 18 | '.*\\.(vue)$': 'vue-jest', 19 | '^.+\\.js$': 'babel-jest', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /test/e2e/screenshotEnvironment.js: -------------------------------------------------------------------------------- 1 | const JSDOMEnvironment = require('jest-environment-jsdom'); 2 | 3 | class ScreenshotEnvironment extends JSDOMEnvironment { 4 | constructor(config, context) { 5 | super(config, context); 6 | 7 | this.global.lastTest = {}; 8 | } 9 | 10 | handleTestEvent(event) { 11 | switch (event.name) { 12 | case 'test_fn_success': 13 | this.global.lastTest = { 14 | failed: false, 15 | test: event.test, 16 | }; 17 | break; 18 | case 'test_fn_failure': 19 | this.global.lastTest = { 20 | failed: true, 21 | test: event.test, 22 | }; 23 | break; 24 | default: 25 | break; 26 | } 27 | } 28 | } 29 | 30 | module.exports = ScreenshotEnvironment; 31 | -------------------------------------------------------------------------------- /nuxt/components/SectionTitle.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 36 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/Features.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Features renders properly 1`] = ` 4 | "
5 |
6 | תכונות 7 | \\"\\" 8 | \\"\\" 9 |
10 |
" 11 | `; 12 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import electron from 'electron'; 3 | import Vue from 'vue'; 4 | import App from './App'; 5 | import { initializeReporter } from './modules/reporting'; 6 | import LoggerPlugin from './plugins/logger'; 7 | import vuetify from './plugins/vuetify'; 8 | import router from './router'; 9 | import store from './store'; 10 | 11 | initializeReporter(); 12 | 13 | const logger = electron.remote.getGlobal('logger'); 14 | logger.info('The renderer process got the logger'); 15 | Vue.use(LoggerPlugin, { logger }); 16 | 17 | Vue.config.productionTip = process.env.NODE_ENV !== 'production'; 18 | 19 | new Vue({ 20 | router, 21 | store, 22 | 23 | created() { 24 | logger.info('Main Vue component registered'); 25 | }, 26 | 27 | vuetify, 28 | render: (h) => h(App), 29 | }).$mount('#app'); 30 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/CallToActionContent.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CallToActionContent renders properly 1`] = ` 4 | "
5 | 6 | צור קשר 7 | 8 |

9 | אנחנו שמחים לשמוע פידבקים, דיווחי באגים ובקשות לשיפור. 10 |

11 |

12 | אם יש לך רעיון לייבוא נתונים ממקומות נוספים חוץ מאשר הבנקים וחברות האשראי.
13 | או תוכנה נוספת שאפשר לייצא אליה את הנתונים.
14 | נשמח מאוד לשמוע! 15 |

16 | שלח מייל! 17 | 18 |
" 19 | `; 20 | -------------------------------------------------------------------------------- /test/unit/specs/importers.vue.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, createLocalVue } from '@vue/test-utils'; 2 | import Vuex from 'vuex'; 3 | 4 | import fakeStore from '../helpers/baseStore'; 5 | import Importers from '../../../src/components/MainPage/Importers'; 6 | import AddScraper from '../../../src/components/MainPage/Importers/AddScraper'; 7 | import { scrapers } from './../../../src/modules/scrapers'; 8 | 9 | const localVue = createLocalVue(); 10 | 11 | localVue.use(Vuex); 12 | 13 | describe('Importers', () => { 14 | let wrapper; 15 | let store; 16 | 17 | beforeEach(() => { 18 | store = new Vuex.Store(fakeStore); 19 | wrapper = shallowMount(Importers, { store, localVue }); 20 | }); 21 | 22 | it('Should contain an AddScraper component for each scraper', () => { 23 | expect(wrapper.findAll(AddScraper).length).toBe(scrapers.length); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | israeli-bank-scrapers-desktop 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/modules/reporting/index.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/electron'; 2 | 3 | // https://github.com/getsentry/sentry-electron/issues/142 4 | const { init } = (process.type === 'browser' 5 | ? require('@sentry/electron/dist/main') 6 | : require('@sentry/electron/dist/renderer')); 7 | 8 | const reporterConfiguration = { 9 | dsn: SENTRY_DSN, 10 | defaultIntegrations: false, 11 | environment: process.env.NODE_ENV, 12 | enableJavaScript: false, 13 | enableNative: false, 14 | enableUnresponsive: false, 15 | }; 16 | 17 | export function initializeReporter() { 18 | init(reporterConfiguration); 19 | } 20 | 21 | export function ReportProblem(title, body, logs, email, extra) { 22 | return Sentry.captureEvent({ 23 | message: title, 24 | logger: logs, 25 | user: { 26 | email, 27 | }, 28 | extra: { 29 | body, 30 | ...extra, 31 | }, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /nuxt/components/FeatureRight.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 38 | -------------------------------------------------------------------------------- /src/modules/encryption/crypto.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copied from https://github.com/eshaham/israeli-ynab-updater/blob/b207a6b2468fa2904412fe9563b8f65ac1e4cfaa/src/helpers/crypto.js 3 | */ 4 | 5 | import crypto from 'crypto'; 6 | import SALT from './salt'; 7 | 8 | const ALGORITHM = 'aes-256-ctr'; 9 | 10 | export function randomHex(characters = 16) { 11 | return crypto.randomBytes(characters).toString('hex'); 12 | } 13 | 14 | export async function encrypt(text) { 15 | const salt = await SALT(randomHex()); 16 | const cipher = crypto.createCipher(ALGORITHM, salt); 17 | const crypted = cipher.update(text, 'utf8', 'hex'); 18 | return crypted + cipher.final('hex'); 19 | } 20 | 21 | export async function decrypt(text) { 22 | const salt = await SALT(); 23 | const decipher = crypto.createDecipher(ALGORITHM, salt); 24 | const decrypted = decipher.update(text, 'hex', 'utf8'); 25 | return decrypted + decipher.final('utf8'); 26 | } 27 | -------------------------------------------------------------------------------- /test/unit/helpers/baseStore.js: -------------------------------------------------------------------------------- 1 | export const Transactions = { 2 | '1541628000000-501420296066': { 3 | chargedAmount: -50, 4 | date: '2018-11-07T22:00:00.000Z', 5 | description: 'איקאה', 6 | installments: null, 7 | memo: '', 8 | originalAmount: -50, 9 | originalCurrency: 'ILS', 10 | processedDate: '2018-11-10T22:00:00.000Z', 11 | status: 'completed', 12 | type: 'normal', 13 | }, 14 | '1541887200000-0.94-1236173698': { 15 | chargedAmount: -0.94, 16 | date: '2018-11-10T22:00:00.000Z', 17 | description: 'עיגול לטובה', 18 | installments: null, 19 | memo: '', 20 | originalAmount: 0, 21 | originalCurrency: 'ILS', 22 | processedDate: '2018-11-10T22:00:00.000Z', 23 | status: 'completed', 24 | type: 'normal', 25 | }, 26 | }; 27 | 28 | export default { 29 | modules: { 30 | Importers: { 31 | importers: [], 32 | }, 33 | Transactions, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /nuxt/test/FeatureLeft.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import FeatureLeft from '@/components/FeatureLeft'; 3 | 4 | const factory = () => shallowMount(FeatureLeft, { 5 | propsData: { 6 | headline: 'Lorem Ipsum', 7 | content: 'Lorem Ipsum dolor sit', 8 | }, 9 | }); 10 | 11 | describe('FeatureLeft', () => { 12 | it('is a Vue instance', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.isVueInstance()).toBeTruthy(); 15 | }); 16 | 17 | it('renders properly', () => { 18 | const wrapper = factory(); 19 | expect(wrapper.html()).toMatchSnapshot(); 20 | }); 21 | 22 | it('has the property headline', () => { 23 | const wrapper = factory(); 24 | expect(wrapper.props('headline')).toBe('Lorem Ipsum'); 25 | }); 26 | 27 | it('has the property content', () => { 28 | const wrapper = factory(); 29 | expect(wrapper.props('content')).toBe('Lorem Ipsum dolor sit'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/store/modules/Transactions.js: -------------------------------------------------------------------------------- 1 | import { properties, transactionArrayToObject } from '../../modules/transactions'; 2 | 3 | const state = { 4 | transactions: {}, 5 | properties, 6 | }; 7 | 8 | const getters = { 9 | transactionsArray: (state) => Object.values(state.transactions), 10 | }; 11 | 12 | const mutations = { 13 | initTransactionIfNot(state) { 14 | if (!state.transactions) { 15 | state.transactions = {}; 16 | } 17 | }, 18 | addTransactions(state, transactions) { 19 | state.transactions = { ...state.transactions, ...transactions }; 20 | }, 21 | }; 22 | 23 | const actions = { 24 | addTransactionsAction({ commit }, account) { 25 | commit('initTransactionIfNot'); 26 | 27 | const transactionsObject = transactionArrayToObject(account.txns); 28 | commit('addTransactions', transactionsObject); 29 | }, 30 | }; 31 | 32 | export default { 33 | state, 34 | getters, 35 | mutations, 36 | actions, 37 | }; 38 | -------------------------------------------------------------------------------- /nuxt/test/FeatureRight.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import FeatureRight from '@/components/FeatureRight'; 3 | 4 | const factory = () => shallowMount(FeatureRight, { 5 | propsData: { 6 | headline: 'Lorem Ipsum', 7 | content: 'Lorem Ipsum dolor sit', 8 | }, 9 | }); 10 | 11 | describe('FeatureRight', () => { 12 | it('is a Vue instance', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.isVueInstance()).toBeTruthy(); 15 | }); 16 | 17 | it('renders properly', () => { 18 | const wrapper = factory(); 19 | expect(wrapper.html()).toMatchSnapshot(); 20 | }); 21 | 22 | it('has the property headline', () => { 23 | const wrapper = factory(); 24 | expect(wrapper.props('headline')).toBe('Lorem Ipsum'); 25 | }); 26 | 27 | it('has the property content', () => { 28 | const wrapper = factory(); 29 | expect(wrapper.props('content')).toBe('Lorem Ipsum dolor sit'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/TeaserColumn.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TeaserColumn renders properly 1`] = ` 4 | "
5 | 10 |
11 |
14 |
15 |
" 16 | `; 17 | -------------------------------------------------------------------------------- /nuxt/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 42 | -------------------------------------------------------------------------------- /nuxt/assets/css/qa.css: -------------------------------------------------------------------------------- 1 | .qa { 2 | @apply bg-white; 3 | @apply text-gray-700; 4 | @apply py-1; 5 | } 6 | 7 | .qa h1 { 8 | font-weight: bold; 9 | cursor: pointer; 10 | } 11 | 12 | .qa h1::before { 13 | content: " "; 14 | display: inline-block; 15 | 16 | border-top: 5px solid transparent; 17 | border-bottom: 5px solid transparent; 18 | border-right: 5px solid black; 19 | 20 | vertical-align: middle; 21 | margin-left: 0.7rem; 22 | transform: translateY(-2px); 23 | 24 | transition: transform 0.2s ease-out; 25 | } 26 | 27 | .qa p { 28 | overflow: hidden; 29 | 30 | transition: max-height 0.25s ease-in-out; 31 | 32 | @apply pr-4; 33 | } 34 | 35 | .qa-close p { 36 | max-height: 0px; 37 | } 38 | 39 | .qa-open p { 40 | max-height: 100vh; 41 | } 42 | 43 | .qa-open h1::before { 44 | transform: rotate(-90deg) translateX(3px); 45 | } 46 | 47 | .qa-open h1 { 48 | border-bottom-right-radius: 0; 49 | border-bottom-left-radius: 0; 50 | } 51 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/Prices.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Prices renders properly 1`] = ` 4 | "
5 |
6 |

7 | Pricing 8 |

9 |
10 |
11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 |
" 19 | `; 20 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import { createPersistedState, createSharedMutations } from 'vuex-electron'; 5 | 6 | import modules from './modules'; 7 | 8 | import migrations from './migrations'; 9 | 10 | Vue.use(Vuex); 11 | 12 | const store = new Vuex.Store({ 13 | modules, 14 | plugins: [ 15 | // Win location: AppData\Roaming\Electron\vuex.json 16 | // linux location: ~/.config/israeli-bank-scrapers-desktop/vuex.json 17 | createPersistedState(), 18 | createSharedMutations(), 19 | ], 20 | strict: process.env.NODE_ENV !== 'production', 21 | }); 22 | 23 | const previousMigration = store.state.Migrations.last; 24 | migrations.filter((migration) => migration.number > previousMigration).forEach((migration) => { 25 | const stateCopy = JSON.parse(JSON.stringify(store.state)); 26 | store.replaceState(migration.migration(stateCopy)); 27 | }); 28 | 29 | store.dispatch('updateLast', migrations.slice(-1)[0].number); 30 | 31 | export default store; 32 | -------------------------------------------------------------------------------- /nuxt/components/FeatureLeft.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 40 | -------------------------------------------------------------------------------- /nuxt/components/CallToActionContent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 37 | -------------------------------------------------------------------------------- /nuxt/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | 39 | -------------------------------------------------------------------------------- /.github/workflows/nuxt.yml: -------------------------------------------------------------------------------- 1 | name: Test and Deploy Github Pages with Nuxt 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: master 7 | paths: 8 | - 'nuxt/**' 9 | - '.github/workflows/*nuxt*.yml' 10 | 11 | jobs: 12 | test: 13 | runs-on: [ubuntu-latest] 14 | defaults: 15 | run: 16 | working-directory: nuxt 17 | steps: 18 | - uses: actions/checkout@v2 19 | - run: yarn 20 | - run: yarn lint 21 | - run: yarn test 22 | 23 | 24 | deploy: 25 | needs: test 26 | defaults: 27 | run: 28 | working-directory: nuxt 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: yarn 33 | run: yarn 34 | - name: generate 35 | if: github.ref != 'refs/heads/master' 36 | run: yarn generate 37 | - name: deploy 38 | if: github.ref == 'refs/heads/master' 39 | run: yarn deploy -r "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/baruchiro/israeli-bank-scrapers-desktop.git" -u "Baruch Odem " -m "$GITHUB_SHA" 40 | -------------------------------------------------------------------------------- /src/components/MainPage/LogSheet.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 49 | 50 | 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Baruch Rothkoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nuxt/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tailwind Toolbox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/HeroContent.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`HeroContent renders properly 1`] = ` 4 | "
5 |
6 |
7 |

8 | קשה לך לעקוב אחרי ההוצאות? 9 |

10 |

11 | עו\\"שי מרכז למקום אחד את כל פירוטי ההוצאות שלך 12 |

13 |

14 | בצורה מאובטחת, ללא צד שלישי ובפיקוח הקהילה 15 |

16 |
17 | 18 |
19 |
20 |
\\"\\"
21 |
22 |
" 23 | `; 24 | -------------------------------------------------------------------------------- /nuxt/layouts/dashboard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 56 | -------------------------------------------------------------------------------- /nuxt/test/Price.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import Price from '@/components/Price'; 3 | 4 | const factory = () => shallowMount(Price, { 5 | propsData: { 6 | name: 'Pro', 7 | list: ['Thing', 'Thing', 'Thing', 'Thing'], 8 | price: '29,99 €', 9 | limited: false, 10 | }, 11 | }); 12 | 13 | describe('Prices', () => { 14 | it('is a Vue instance', () => { 15 | const wrapper = factory(); 16 | expect(wrapper.isVueInstance()).toBeTruthy(); 17 | }); 18 | 19 | it('renders properly', () => { 20 | const wrapper = factory(); 21 | expect(wrapper.html()).toMatchSnapshot(); 22 | }); 23 | 24 | it('has the property name', () => { 25 | const wrapper = factory(); 26 | expect(wrapper.props('name')).toBe('Pro'); 27 | }); 28 | 29 | it('has the property list', () => { 30 | const wrapper = factory(); 31 | expect(wrapper.props('list')).toEqual(['Thing', 'Thing', 'Thing', 'Thing']); 32 | }); 33 | 34 | it('has the property price', () => { 35 | const wrapper = factory(); 36 | expect(wrapper.props('price')).toBe('29,99 €'); 37 | }); 38 | 39 | it('has the property limited', () => { 40 | const wrapper = factory(); 41 | expect(wrapper.props('limited')).toBe(false); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /nuxt/test/PriceHighlighted.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import PriceHighlighted from '@/components/PriceHighlighted'; 3 | 4 | const factory = () => shallowMount(PriceHighlighted, { 5 | propsData: { 6 | name: 'Basic', 7 | list: ['Thing', 'Thing', 'Thing'], 8 | price: '9,99 €', 9 | limited: false, 10 | }, 11 | }); 12 | 13 | describe('Prices', () => { 14 | it('is a Vue instance', () => { 15 | const wrapper = factory(); 16 | expect(wrapper.isVueInstance()).toBeTruthy(); 17 | }); 18 | 19 | it('renders properly', () => { 20 | const wrapper = factory(); 21 | expect(wrapper.html()).toMatchSnapshot(); 22 | }); 23 | 24 | it('has the property name', () => { 25 | const wrapper = factory(); 26 | expect(wrapper.props('name')).toBe('Basic'); 27 | }); 28 | 29 | it('has the property list', () => { 30 | const wrapper = factory(); 31 | expect(wrapper.props('list')).toEqual(['Thing', 'Thing', 'Thing']); 32 | }); 33 | 34 | it('has the property price', () => { 35 | const wrapper = factory(); 36 | expect(wrapper.props('price')).toBe('9,99 €'); 37 | }); 38 | 39 | it('has the property limited', () => { 40 | const wrapper = factory(); 41 | expect(wrapper.props('limited')).toBe(false); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /docs/share-spreadsheet.md: -------------------------------------------------------------------------------- 1 | # Share a Google-Spreadsheet with iseali-bank-scrapers-desktop 2 | 3 | 1. [Create a Google Spreadsheet](https://docs.google.com/spreadsheets/create). 4 | 5 | 2. Click on the **Share** button: 6 | ![share](img/share.png) 7 | 8 | 3. Add the app's email address- drive-updater@israeli-bank-scrapers-desktop.iam.gserviceaccount.com. 9 | (This email address doesn't have a mailbox, so don't send emails there). 10 | 11 | 4. Make sure you give **edit permission**. 12 | 13 | 5. Unmark **Notify people**. 14 | (As I said, emails cannot be sent to this address) 15 | 16 | 6. Your input should look like this: 17 | ![imput](img/insert-mail.png) 18 | 19 | 7. The document address (from the address bar) can now be shared with the app. 20 | (To be sure, the address should look like this: `https://docs.google.com/spreadsheets/d/{Document-ID}/{Other-things}`) 21 | 22 | Using the app to upload data will create a worksheet called `_ibsd`: 23 | 24 | ![_ibsd sheet](img/_ibsd.png) 25 | 26 | **Don't edit this worksheet!** 27 | 28 | The app expects a fixed format to keep this worksheet up to date. If you change it, the app will change the data back, and in the worst case the app will fall. 29 | 30 | I recommend that you create a different worksheet, which read the data from the worksheet `_ibsd` and analyze them. 31 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/PriceHighlighted.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Prices renders properly 1`] = ` 4 | "
5 |
6 |
7 | Basic 8 |
9 |
10 |
    11 |
  • 12 | Thing 13 |
  • 14 |
  • 15 | Thing 16 |
  • 17 |
  • 18 | Thing 19 |
  • 20 |
21 |
22 |
23 |
24 | 9,99 € 25 | / per user
26 |
29 |
30 |
" 31 | `; 32 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/Price.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Prices renders properly 1`] = ` 4 | "
5 |
6 |
7 | Pro 8 |
9 |
    10 |
  • 11 | Thing 12 |
  • 13 |
  • 14 | Thing 15 |
  • 16 |
  • 17 | Thing 18 |
  • 19 |
  • 20 | Thing 21 |
  • 22 |
23 |
24 |
25 |
26 | 29,99 € 27 | / per user
28 |
31 |
32 |
" 33 | `; 34 | -------------------------------------------------------------------------------- /nuxt/components/Features.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 46 | -------------------------------------------------------------------------------- /nuxt/components/HeroContent.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 44 | -------------------------------------------------------------------------------- /src/store/modules/Importers.js: -------------------------------------------------------------------------------- 1 | import { randomHex } from '@/modules/encryption/crypto'; 2 | 3 | const mutations = { 4 | addImporter(state, data) { 5 | state.importers.push(data); 6 | }, 7 | removeImporter(state, data) { 8 | state.importers = state.importers.filter((importer) => importer.id !== data); 9 | }, 10 | updateStatus(state, { id, status }) { 11 | state.importers = state.importers.map((importer) => { 12 | if (importer.id === id) { 13 | importer.status = status; 14 | } 15 | return importer; 16 | }); 17 | }, 18 | }; 19 | 20 | const actions = { 21 | addImporterAction({ commit }, importer) { 22 | importer.id = randomHex(); 23 | commit('addImporter', importer); 24 | }, 25 | removeImporterAction({ commit }, importerId) { 26 | commit('removeImporter', importerId); 27 | }, 28 | updateImporterStatus({ commit }, { id, status }) { 29 | commit('updateStatus', { id, status }); 30 | }, 31 | }; 32 | 33 | const emptyStatusObj = { 34 | success: null, 35 | lastMessage: null, 36 | }; 37 | 38 | const getters = { 39 | importers: (state) => state.importers.map((importer) => { 40 | importer.status = importer.status || { ...emptyStatusObj }; 41 | return importer; 42 | }), 43 | }; 44 | 45 | export default { 46 | state: { 47 | importers: [], 48 | }, 49 | mutations, 50 | actions, 51 | getters, 52 | }; 53 | -------------------------------------------------------------------------------- /nuxt/components/Price.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 43 | -------------------------------------------------------------------------------- /src/components/MainPage/Importers/DeleteImporterDialog.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 63 | -------------------------------------------------------------------------------- /nuxt/components/PriceHighlighted.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 44 | -------------------------------------------------------------------------------- /nuxt/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const { stringified } = require('./globals.config'); 2 | 3 | const globals = Object.keys(stringified).reduce((acc, globalName) => { 4 | acc[globalName] = 'readonly'; 5 | return acc 6 | }, {}); 7 | 8 | module.exports = { 9 | root: true, 10 | settings: { 11 | 'import/resolver': { 12 | alias: { 13 | map: [ 14 | ['@', './'], 15 | ], 16 | extensions: ['.js', '.vue'] 17 | } 18 | } 19 | }, 20 | 21 | env: { 22 | browser: true, 23 | }, 24 | 25 | extends: ['airbnb-base', 'plugin:vue/recommended'], 26 | 27 | plugins: [ 28 | 'import', 29 | 'vue', 30 | 'html', 31 | ], 32 | 33 | globals, 34 | 35 | rules: { 36 | 'no-param-reassign': ['error', { ignorePropertyModificationsFor: ['state'] }], 37 | 'no-shadow': ['error', { allow: ['state'] }], 38 | 'import/extensions': ['error', { js: 'never', vue: 'never', json: 'always' }], 39 | 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], 40 | // allow debugger during development 41 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 42 | 'no-console': process.env.NODE_ENV === 'production' ? 2 : 1, 43 | 'linebreak-style': process.platform === 'win32' ? 0 : 2, 44 | 'max-len': 0, 45 | "import/no-extraneous-dependencies": ["error", {"devDependencies": ["*.config.js", "**/*.spec.js"]}] 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /test/unit/specs/transactions.spec.js: -------------------------------------------------------------------------------- 1 | import { getHash } from 'modules/transactions'; 2 | import { Transactions } from '../helpers/baseStore'; 3 | 4 | describe('transaction.js (The transactions helper)', () => { 5 | it('Should create hash key for transaction', () => { 6 | const firstTransaction = Object.values(Transactions)[0]; 7 | const hashKey = getHash(firstTransaction); 8 | 9 | expect(hashKey).not.toBe(0); 10 | }); 11 | 12 | it('Should be a string hash', () => { 13 | const firstTransaction = Object.values(Transactions)[0]; 14 | const hashKey = getHash(firstTransaction); 15 | 16 | expect(typeof hashKey).toBe('string'); 17 | }); 18 | 19 | it('Should be the expected hash', () => { 20 | const firstTransactionHash = Object.keys(Transactions)[0]; 21 | const firstTransaction = Object.values(Transactions)[0]; 22 | 23 | const hashKey = getHash(firstTransaction); 24 | 25 | expect(hashKey).toBe(firstTransactionHash); 26 | }); 27 | 28 | it('Two empty transactions in the same day - should be different hash', () => { 29 | const transactionObjects = Object.values(Transactions); 30 | transactionObjects[0].chargedAmount = transactionObjects[1].chargedAmount = 0; 31 | transactionObjects[0].date = transactionObjects[1].date; 32 | 33 | const firstHashKey = getHash(transactionObjects[0]); 34 | const secondHashKey = getHash(transactionObjects[1]); 35 | expect(firstHashKey).not.toMatch(secondHashKey); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "private": true, 4 | "scripts": { 5 | "dev": "nuxt", 6 | "build": "nuxt build", 7 | "start": "nuxt start", 8 | "generate": "nuxt generate", 9 | "generate:dev": "nuxt generate && serve ./dist", 10 | "deploy": "yarn generate && gh-pages -d dist -t", 11 | "test": "jest", 12 | "test:dev": "jest --watch", 13 | "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter .", 14 | "lint:fix": "yarn lint --fix" 15 | }, 16 | "dependencies": { 17 | "@nuxtjs/markdownit": "^2.0.0", 18 | "@nuxtjs/svg": "^0.1.11", 19 | "axios": "^0.21.1", 20 | "nuxt": "^2.14.12", 21 | "vue-scrollto": "^2.18.1" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.12.10", 25 | "@babel/preset-env": "^7.12.7", 26 | "@nuxtjs/tailwindcss": "^3.1.0", 27 | "@vue/test-utils": "^1.1.2", 28 | "babel-core": "^7.0.0-bridge.0", 29 | "babel-jest": "^26.6.3", 30 | "eslint": "^7.21.0", 31 | "eslint-config-airbnb-base": "^14.1.0", 32 | "eslint-friendly-formatter": "^4.0.1", 33 | "eslint-import-resolver-alias": "^1.1.2", 34 | "eslint-plugin-html": "^6.0.1", 35 | "eslint-plugin-import": "^2.20.2", 36 | "eslint-plugin-vue": "^7.0.1", 37 | "gh-pages": "^3.1.0", 38 | "husky": "^5.1.3", 39 | "jest": "^26.5.0", 40 | "jest-serializer-vue": "^2.0.2", 41 | "serve": "^11.3.0", 42 | "vue-jest": "^3.0.6", 43 | "webpack": "^4.42.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/e2e/utils/interactions.js: -------------------------------------------------------------------------------- 1 | import Element from './element'; 2 | 3 | const CollapseAddImporter = 'aside[data-test="ToggleAddImporter"]'; 4 | const CollapseAddImporterButton = 'button[data-test="CollapseAddImporter"]'; 5 | const AddScrapers = `${CollapseAddImporter} div:nth-of-type(1) [data-test]`; 6 | const DrawerLeftToggle = 'button[data-test="drawerLeftToggle"]'; 7 | 8 | const wait = async (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 9 | 10 | export default class Interactions { 11 | constructor(client) { 12 | this.client = client; 13 | } 14 | 15 | async click(json) { 16 | const element = json.ELEMENT || json.value.ELEMENT; 17 | return this.client.elementIdClick(element); 18 | } 19 | 20 | async getCollapseAddImporter() { 21 | const json = await this.client.$(CollapseAddImporter); 22 | return new Element(this.client, json.value); 23 | } 24 | 25 | async getAddScrapers() { 26 | return (await this.client.$$(AddScrapers)) 27 | .map((element) => new Element(this.client, element)); 28 | } 29 | 30 | async waitForAddScrapersVisible() { 31 | return this.client.waitForVisible(AddScrapers, 1000); 32 | } 33 | 34 | async toggleLeftDrawer() { 35 | await this.client.$(DrawerLeftToggle).then((json) => this.click(json.value)); 36 | await wait(1000); 37 | } 38 | 39 | async clickCollapseAddImporter() { 40 | await this.client.$(CollapseAddImporterButton).then((json) => this.click(json)); 41 | return this.client.waitForVisible(`${AddScrapers}`, 1000); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /nuxt/components/Prices.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "nuxt/**" 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Use Node.js 12.x 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 12.x 17 | - name: lint 18 | run: | 19 | yarn 20 | yarn lint 21 | 22 | test: 23 | 24 | runs-on: ${{ matrix.os }} 25 | env: 26 | DISPLAY: :99.0 27 | 28 | strategy: 29 | matrix: 30 | node-version: [12.x] 31 | os: [ubuntu-latest, windows-latest] 32 | 33 | steps: 34 | - uses: actions/checkout@v1 35 | - name: Use Node.js ${{ matrix.node-version }} 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | - name: Install xvfb 40 | if: matrix.os == 'ubuntu-latest' 41 | run: | 42 | sudo apt update 43 | sudo apt install -y xvfb graphicsmagick 44 | npm install -g xvfb-maybe 45 | Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 46 | - name: yarn install 47 | run: yarn 48 | - name: yarn unit 49 | run: yarn unit 50 | - name: yarn e2e 51 | run: yarn e2e 52 | - name: Upload screenshots 53 | if: failure() 54 | uses: actions/upload-artifact@v1.0.0 55 | with: 56 | # Artifact name 57 | name: ${{ matrix.os }}-screenshots 58 | # Directory containing files to upload 59 | path: screenshots 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | screenshots/ 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | .eslintcache 36 | dist/electron/* 37 | dist/web/* 38 | !build/icons 39 | build/ 40 | dist_electron 41 | 42 | # Dependency directory 43 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 44 | node_modules 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # next.js build output 69 | .next 70 | 71 | # OSX 72 | .DS_Store 73 | 74 | thumbs.db 75 | !.gitkeep 76 | 77 | # IDE 78 | .idea 79 | .vscode 80 | 81 | # App cache 82 | .persistance 83 | 84 | # Google API 85 | client_secret.json 86 | -------------------------------------------------------------------------------- /nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # Mac OSX 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const globals = require('./globals') 2 | 3 | module.exports = { 4 | root: true, 5 | 6 | parserOptions: { 7 | parser: '@babel/eslint-parser', 8 | sourceType: 'module', 9 | }, 10 | 11 | env: { 12 | browser: true, 13 | node: true, 14 | }, 15 | 16 | extends: ['airbnb-base', 'plugin:vue/recommended'], 17 | 18 | globals: { 19 | __static: 'writable', 20 | ...globals.reduce((prev, curr) => { 21 | prev[curr] = 'readonly'; 22 | return prev; 23 | }, {}), 24 | }, 25 | 26 | plugins: [ 27 | 'import', 28 | 'vue', 29 | 'html', 30 | ], 31 | 32 | settings: { 33 | 'import/core-modules': [ 'electron' ], 34 | 'import/resolver': { 35 | alias: { 36 | map: [ 37 | ['@', './src'], 38 | ], 39 | extensions: ['.js', '.vue'] 40 | } 41 | } 42 | }, 43 | 44 | rules: { 45 | 'no-param-reassign': ['error', { ignorePropertyModificationsFor: ['state'] }], 46 | 'no-shadow': ['error', { allow: ['state'] }], 47 | 'import/extensions': ['error', { js: 'never', vue: 'never', json: 'always' }], 48 | 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], 49 | // allow debugger during development 50 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 51 | 'no-console': process.env.NODE_ENV === 'production' ? 2 : 1, 52 | 'linebreak-style': process.platform === 'win32' ? 0 : 2, 53 | }, 54 | 55 | overrides: [ 56 | { 57 | files: [ 58 | '**/__tests__/*.{j,t}s?(x)', 59 | '**/tests/unit/**/*.spec.{j,t}s?(x)', 60 | ], 61 | env: { 62 | jest: true, 63 | }, 64 | }, 65 | ], 66 | }; 67 | -------------------------------------------------------------------------------- /nuxt/test/TeaserColumn.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import TeaserColumn from '@/components/TeaserColumn'; 3 | 4 | const factory = () => shallowMount(TeaserColumn, { 5 | propsData: { 6 | rows: 3, 7 | action: 'Action', 8 | }, 9 | }); 10 | 11 | describe('TeaserColumn', () => { 12 | it('is a Vue instance', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.isVueInstance()).toBeTruthy(); 15 | }); 16 | 17 | it('renders properly', () => { 18 | const wrapper = factory(); 19 | expect(wrapper.html()).toMatchSnapshot(); 20 | }); 21 | 22 | it('has the property rows', () => { 23 | const wrapper = factory(); 24 | expect(wrapper.props('rows')).toBe(3); 25 | }); 26 | 27 | it('has the property action', () => { 28 | const wrapper = factory(); 29 | expect(wrapper.props('action')).toBe('Action'); 30 | }); 31 | 32 | describe('when the user clicks on the button', () => { 33 | it('emits the event', () => { 34 | const wrapper = factory(); 35 | wrapper.find('button').trigger('click'); 36 | expect(wrapper.emitted().clicked).toBeTruthy(); 37 | }); 38 | }); 39 | 40 | describe('when the property action is not an empty string', () => { 41 | it('renders the button', () => { 42 | const wrapper = factory(); 43 | expect(wrapper.find('button').exists()).toBe(true); 44 | }); 45 | }); 46 | 47 | describe('when the property action is an empty string', () => { 48 | it.skip('renders the button', () => { 49 | const wrapper = factory(); 50 | wrapper.setProps({ action: '' }); 51 | expect(wrapper.find('button').exists()).toBe(false); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /nuxt/components/Teasers.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 61 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const globals = require('./globals'); 2 | 3 | const defineGlobals = (config) => { 4 | config.plugin('define').tap((args) => { 5 | const defined = globals.reduce((prev, curr) => { 6 | prev[curr] = JSON.stringify(process.env[curr]); 7 | return prev; 8 | }, {}); 9 | args[0] = defined; 10 | return args; 11 | }); 12 | }; 13 | 14 | module.exports = { 15 | pluginOptions: { 16 | electronBuilder: { 17 | chainWebpackMainProcess: defineGlobals, 18 | chainWebpackRendererProcess: defineGlobals, 19 | // List native deps here if they don't work 20 | externals: [ 21 | 'keytar', 22 | 'israeli-bank-scrapers-core', 23 | ], 24 | builderOptions: { 25 | productName: 'israeli-bank-scrapers-desktop', 26 | appId: 'com.electron.israeli-bank-scrapers-desktop', 27 | dmg: { 28 | contents: [ 29 | { 30 | x: 410, 31 | y: 150, 32 | type: 'link', 33 | path: '/Applications', 34 | }, 35 | { 36 | x: 130, 37 | y: 150, 38 | type: 'file', 39 | }, 40 | ], 41 | }, 42 | mac: { 43 | icon: 'build/icons/icon.icns', 44 | }, 45 | win: { 46 | icon: 'build/icons/icon.ico', 47 | }, 48 | linux: { 49 | target: [ 50 | 'AppImage', 51 | 'snap', 52 | 'deb', 53 | ], 54 | icon: 'build/icons', 55 | }, 56 | }, 57 | }, 58 | mainProcessWatch: [ 59 | 'src/service', 60 | ], 61 | }, 62 | transpileDependencies: [ 63 | 'vuetify', 64 | ], 65 | }; 66 | -------------------------------------------------------------------------------- /nuxt/assets/img/wave-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/modules/scrapers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import { createScraper, SCRAPERS } from 'israeli-bank-scrapers-core'; 3 | import getChrome from './downloadChromium'; 4 | 5 | export const scrapers = Object.keys(SCRAPERS) 6 | .filter((key) => key !== 'leumiCard') 7 | .map((key) => ({ 8 | key, 9 | ...SCRAPERS[key], 10 | })); 11 | 12 | export async function scrape( 13 | installPath, 14 | scraperName, 15 | loginFields, 16 | showBrowser, 17 | onProgress, 18 | logger, 19 | ) { 20 | onProgress({ percent: 0.01, message: 'Step 1: check if Chrome exists' }); 21 | const chromePath = await getChrome(installPath, onProgress); 22 | const options = { 23 | companyId: scraperName, // mandatory; one of 'hapoalim', 'leumi', 'discount', 'otsarHahayal', 'visaCal', 'leumiCard', 'isracard', 'amex' 24 | // startDate: Date, // the date to fetch transactions from (can't be before the minimum allowed time difference for the scraper) 25 | // combineInstallments: boolean, // if set to true, all installment transactions will be combine into the first one 26 | showBrowser, // shows the browser while scraping, good for debugging (default false) 27 | verbose: showBrowser, // include more debug info about in the output 28 | // browser : Browser, // optional option from init puppeteer browser instance outside the libary scope. you can get browser diretly from puppeteer via `puppeteer.launch()` command. 29 | executablePath: chromePath, // string // optional. provide a patch to local chromium to be used by puppeteer. Relevant when using `israeli-bank-scrapers-core` library 30 | }; 31 | if (logger) logger.info(JSON.stringify(options)); 32 | onProgress({ percent: 0.5, message: `Step 2: Starting to scrape ${scraperName}` }); 33 | const scraper = createScraper(options); 34 | return scraper.scrape(loginFields); 35 | } 36 | -------------------------------------------------------------------------------- /nuxt/test/Logo.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import Logo from '@/components/Logo'; 3 | 4 | const factory = () => shallowMount(Logo, { 5 | propsData: { 6 | isStickable: true, 7 | isSticky: false, 8 | }, 9 | }); 10 | 11 | describe('Logo', () => { 12 | it('is a Vue instance', () => { 13 | const wrapper = factory(); 14 | expect(wrapper.isVueInstance()).toBeTruthy(); 15 | }); 16 | 17 | it('renders properly', () => { 18 | const wrapper = factory(); 19 | expect(wrapper.html()).toMatchSnapshot(); 20 | }); 21 | 22 | it('has the property isStickable', () => { 23 | const wrapper = factory(); 24 | expect(wrapper.props('isStickable')).toBe(true); 25 | }); 26 | 27 | it('has the property isSticky', () => { 28 | const wrapper = factory(); 29 | expect(wrapper.props('isSticky')).toBe(false); 30 | }); 31 | 32 | describe('when creating the classlist', () => { 33 | describe('when isStickable is true and isSticky is false', () => { 34 | it('has the class', () => { 35 | const wrapper = factory(); 36 | const anchor = wrapper.find('a'); 37 | expect(anchor.classes('text-white')).toBe(true); 38 | }); 39 | }); 40 | 41 | describe('when isStickable is true and isSticky is true', () => { 42 | it.skip('has the class', () => { 43 | const wrapper = factory(); 44 | wrapper.setProps({ isSticky: true }); 45 | const anchor = wrapper.find('a'); 46 | expect(anchor.classes('text-gray-800')).toBe(true); 47 | }); 48 | }); 49 | 50 | describe('when isStickable is false', () => { 51 | it.skip('has the class', () => { 52 | const wrapper = factory(); 53 | wrapper.setProps({ isStickable: false }); 54 | const anchor = wrapper.find('a'); 55 | expect(anchor.classes('text-orange-600')).toBe(true); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /nuxt/assets/img/hero-wave.svg: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 17 | 22 | 23 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/modules/spreadsheet/googleOAuth2.js: -------------------------------------------------------------------------------- 1 | import { encryptObject, decryptObject } from '@/modules/encryption/credentials'; 2 | import { saveIntoAccount, getFromAccount } from '@/modules/encryption/keytar'; 3 | import ElectronGoogleOAuth2 from '@getstation/electron-google-oauth2'; 4 | 5 | const keytarAccount = 'googleOauth2Token'; 6 | const successRedirectURL = 'https://github.com/baruchiro/israeli-bank-scrapers-desktop'; 7 | 8 | // eslint-disable-next-line camelcase 9 | async function saveToken({ expiry_date, refresh_token }) { 10 | const encryptedToken = await encryptObject({ expiry_date, refresh_token }); 11 | const strToken = JSON.stringify(encryptedToken); 12 | return saveIntoAccount(keytarAccount, strToken); 13 | } 14 | 15 | async function loadToken() { 16 | const strToken = await getFromAccount(keytarAccount); 17 | if (strToken === null) return null; 18 | const encryptedToken = JSON.parse(strToken); 19 | return decryptObject(encryptedToken); 20 | } 21 | 22 | export async function CreateClient() { 23 | const myApiOauth = new ElectronGoogleOAuth2( 24 | GOOGLE_CLIENT_ID, 25 | GOOGLE_CLIENT_SECRET, 26 | [ 27 | 'https://www.googleapis.com/auth/drive.metadata.readonly', 28 | 'https://www.googleapis.com/auth/spreadsheets', 29 | ], 30 | { successRedirectURL }, 31 | ); 32 | 33 | myApiOauth.on('tokens', async (tokens) => { 34 | if (tokens.refresh_token) { 35 | await saveToken(tokens); 36 | } 37 | }); 38 | 39 | const savedToken = await loadToken(); 40 | 41 | if (savedToken?.expiry_date && savedToken.expiry_date > Date.now()) { 42 | myApiOauth.setTokens(savedToken); 43 | } else { 44 | const token = await myApiOauth.openAuthWindowAndGetTokens(); 45 | await saveToken(token); 46 | myApiOauth.setTokens(token); 47 | } 48 | return myApiOauth.oauth2Client; 49 | } 50 | 51 | export async function isConnected() { 52 | return (await loadToken()) !== null; 53 | } 54 | -------------------------------------------------------------------------------- /nuxt/components/TeaserColumn.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 72 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/TheHeader.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TheHeader renders properly 1`] = ` 4 | "" 26 | `; 27 | -------------------------------------------------------------------------------- /nuxt/components/TheFooter.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 74 | -------------------------------------------------------------------------------- /nuxt/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import { DefinePlugin } from 'webpack'; 2 | import { stringified } from './globals.config'; 3 | 4 | // only add `router.base = '//'` if `GITHUB_ACTIONS` is `true` 5 | const routerBase = process.env.GITHUB_ACTIONS ? { 6 | router: { 7 | base: '/israeli-bank-scrapers-desktop/', 8 | }, 9 | } : {}; 10 | 11 | export default { 12 | ...routerBase, 13 | /* 14 | ** Headers of the page 15 | */ 16 | head: { 17 | title: 'עו"שי', 18 | meta: [ 19 | { charset: 'utf-8' }, 20 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 21 | { 22 | hid: 'description', 23 | name: 'description', 24 | content: 'ניהול הוצאות', 25 | }, 26 | ], 27 | link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], 28 | }, 29 | /* 30 | ** Customize the progress-bar color 31 | */ 32 | loading: { color: '#fff' }, 33 | /* 34 | ** Global CSS 35 | */ 36 | css: [], 37 | /* 38 | ** Plugins to load before mounting the App 39 | */ 40 | plugins: [], 41 | /* 42 | ** Nuxt.js dev-modules 43 | */ 44 | buildModules: [ 45 | // Doc: https://github.com/nuxt-community/nuxt-tailwindcss 46 | '@nuxtjs/tailwindcss', 47 | ], 48 | /* 49 | ** Nuxt.js modules 50 | */ 51 | modules: [ 52 | '@nuxtjs/svg', 53 | 'vue-scrollto/nuxt', 54 | '@nuxtjs/markdownit', 55 | ], 56 | markdownit: { 57 | injected: true, 58 | }, 59 | purgeCSS: { 60 | whitelist: ['hidden'], 61 | whitelistPatterns: [/md:w-[1-6]/], 62 | }, 63 | /* 64 | ** Build configuration 65 | */ 66 | build: { 67 | /* 68 | ** You can extend webpack config here 69 | */ 70 | // eslint-disable-next-line no-unused-vars 71 | extend(config, ctx) { 72 | config.module.rules.push({ 73 | test: /\.icon?$/, 74 | loader: 'url-loader', 75 | query: { 76 | limit: 1000, // 1kB 77 | name: 'img/[name].[hash:7].[ext]', 78 | }, 79 | }); 80 | }, 81 | 82 | plugins: [ 83 | new DefinePlugin(stringified), 84 | ], 85 | }, 86 | }; 87 | -------------------------------------------------------------------------------- /src/components/MainPage/DataTable.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 80 | 81 | 86 | -------------------------------------------------------------------------------- /src/components/MainPage/ProfileChip.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 82 | 83 | 86 | -------------------------------------------------------------------------------- /src/components/MainPage/Exporters/JsonExporter.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 88 | 89 | 91 | -------------------------------------------------------------------------------- /src/components/MainPage/Exporters.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 94 | 95 | 97 | -------------------------------------------------------------------------------- /src/modules/encryption/credentials.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | /* 3 | Copied from https://github.com/eshaham/israeli-ynab-updater/blob/b207a6b2468fa2904412fe9563b8f65ac1e4cfaa/src/helpers/credentials.js 4 | */ 5 | 6 | import { randomHex, encrypt, decrypt } from './crypto'; 7 | import { saveIntoAccount, getFromAccount } from './keytar'; 8 | 9 | export const defaultEncryptProperty = 'encrypted'; 10 | 11 | function isObject(obj) { 12 | return typeof obj === 'object' && !Array.isArray(obj) && obj !== null; 13 | } 14 | 15 | async function encryptValue(val) { 16 | if (isObject(val)) { 17 | return encryptObject(val); 18 | } 19 | if (Array.isArray(val)) { 20 | return encryptArray(val); 21 | } 22 | if (typeof val === 'number') { 23 | console.warn('Number can\'t be encrypted'); // eslint-disable-line no-console 24 | return val; 25 | } 26 | return encrypt(val); 27 | } 28 | 29 | async function decryptValue(val) { 30 | if (isObject(val)) { 31 | return decryptObject(val); 32 | } 33 | if (Array.isArray(val)) { 34 | return decryptArray(val); 35 | } 36 | if (typeof val === 'number') { 37 | return val; 38 | } 39 | return decrypt(val); 40 | } 41 | 42 | async function encryptArray(arr) { 43 | return Promise.all(arr.map((item) => encryptValue(item))); 44 | } 45 | 46 | async function decryptArray(arr) { 47 | return Promise.all(arr.map((item) => decryptValue(item))); 48 | } 49 | 50 | export async function encryptObject(obj) { 51 | const encrypted = await Object.keys(obj).reduce(async (accPromise, key) => { 52 | const acc = await accPromise; 53 | acc[key] = await encryptValue(obj[key]); 54 | return acc; 55 | }, Promise.resolve({})); 56 | return encrypted; 57 | } 58 | 59 | export async function decryptObject(obj) { 60 | const decrypted = await Object.keys(obj).reduce(async (accPromise, key) => { 61 | const acc = await accPromise; 62 | acc[key] = await decryptValue(obj[key]); 63 | return acc; 64 | }, Promise.resolve({})); 65 | return decrypted; 66 | } 67 | 68 | export async function encryptProperty(obj, property) { 69 | const resultObj = { ...obj }; 70 | const propertyJSON = JSON.stringify(resultObj[property]); 71 | const encryptedJSON = await encrypt(propertyJSON); 72 | const account = randomHex(); 73 | await saveIntoAccount(account, encryptedJSON); 74 | resultObj[property] = account; 75 | return resultObj; 76 | } 77 | 78 | export async function decryptProperty(obj, property) { 79 | const resultObj = { ...obj }; 80 | const account = resultObj[property]; 81 | const encryptedJSON = await getFromAccount(account); 82 | const propertyJSON = await decrypt(encryptedJSON); 83 | resultObj[property] = JSON.parse(propertyJSON); 84 | return resultObj; 85 | } 86 | -------------------------------------------------------------------------------- /nuxt/test/__snapshots__/TheFooter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TheFooter renders properly 1`] = ` 4 | "" 41 | `; 42 | -------------------------------------------------------------------------------- /nuxt/README.md: -------------------------------------------------------------------------------- 1 | # [Tailwind Toolbox](https://www.tailwindtoolbox.com/) - [Nuxt Version of Landing Page Template](https://www.tailwindtoolbox.com/templates/landing-page) 2 | 3 | > [LIVE DEMO](https://tailwind-landing-page-nuxt.netlify.com/) 4 | 5 | [Nuxt Version Landing Page](https://www.tailwindtoolbox.com/templates/landing-page) is an open source landing page template for [Tailwind CSS](https://tailwindcss.com/) specificly for the [Nuxt.js](https://nuxtjs.org/) framework created by [Vannsl](https://github.com/Vannsl) for the [Tailwind Toolbox](https://www.tailwindtoolbox.com/). 6 | 7 | ![Landing Page](https://www.tailwindtoolbox.com/templates/landing-page.png) 8 | 9 | ## Build Setup 10 | 11 | ```bash 12 | # install dependencies 13 | $ npm run install 14 | 15 | # serve with hot reload at localhost:3000 16 | $ npm run dev 17 | 18 | # build for production and launch server 19 | $ npm run build 20 | $ npm run start 21 | 22 | # generate static project 23 | $ npm run generate 24 | ``` 25 | 26 | ## Test Coverage 27 | | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | 28 | | ----------------------- | ------- | -------- | ------- | ------- | ----------------- | 29 | | All files | 100 | 100 | 100 | 100 | | 30 | | components | 100 | 100 | 100 | 100 | | 31 | | CallToAction.vue | 100 | 100 | 100 | 100 | | 32 | | CallToActionContent.vue | 100 | 100 | 100 | 100 | | 33 | | FeatureLeft.vue | 100 | 100 | 100 | 100 | | 34 | | FeatureRight.vue | 100 | 100 | 100 | 100 | | 35 | | Features.vue | 100 | 100 | 100 | 100 | | 36 | | Hero.vue | 100 | 100 | 100 | 100 | | 37 | | HeroContent.vue | 100 | 100 | 100 | 100 | | 38 | | Logo.vue | 100 | 100 | 100 | 100 | | 39 | | Price.vue | 100 | 100 | 100 | 100 | | 40 | | PriceHighlighted.vue | 100 | 100 | 100 | 100 | | 41 | | Prices.vue | 100 | 100 | 100 | 100 | | 42 | | TeaserColumn.vue | 100 | 100 | 100 | 100 | | 43 | | Teasers.vue | 100 | 100 | 100 | 100 | | 44 | | TheFooter.vue | 100 | 100 | 100 | 100 | | 45 | | TheHeader.vue | 100 | 100 | 100 | 100 | | 46 | | pages | 100 | 100 | 100 | 100 | | 47 | | index.vue | 100 | 100 | 100 | 100 | | 48 | -------------------------------------------------------------------------------- /src/components/MainPage/Importers/AddScraper.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 99 | 100 | 103 | -------------------------------------------------------------------------------- /src/modules/transactions.js: -------------------------------------------------------------------------------- 1 | import { formatDate, unixMilli } from './dates'; 2 | import calculateHash from './hash'; 3 | 4 | /* 5 | { 6 | success: boolean, 7 | accounts: [{ 8 | accountNumber: string, 9 | txns: [{ 10 | type: string, // can be either 'normal' or 'installments' 11 | identifier: int, // only if exists 12 | date: string, // ISO date string 13 | processedDate: string, // ISO date string 14 | originalAmount: double, 15 | originalCurrency: string, 16 | chargedAmount: double, 17 | description: string, 18 | memo: string, // can be null or empty 19 | installments: { 20 | number: int, // the current installment number 21 | total: int, // the total number of installments 22 | }, 23 | status: string //can either be 'completed' or 'pending' 24 | }], 25 | }], 26 | errorType: "invalidPassword"|"changePassword"|"timeout"|"generic", // only on success=false 27 | errorMessage: string, // only on success=false 28 | } 29 | */ 30 | 31 | export const properties = [ 32 | { 33 | name: 'type', 34 | title: 'Type', 35 | }, 36 | { 37 | name: 'identifier', 38 | title: 'Identifier', 39 | }, 40 | { 41 | name: 'date', 42 | title: 'Date', 43 | column: true, 44 | hash: (value) => unixMilli(value), 45 | }, 46 | { 47 | name: 'processedDate', 48 | title: 'Processed Date', 49 | }, 50 | { 51 | name: 'originalAmount', 52 | title: 'Original Amount', 53 | }, 54 | { 55 | name: 'originalCurrency', 56 | title: 'Original Currency', 57 | }, 58 | { 59 | name: 'chargedAmount', 60 | title: 'Charged Amount', 61 | column: true, 62 | hash: true, 63 | }, 64 | { 65 | name: 'description', 66 | title: 'Description', 67 | column: true, 68 | hash: (value) => calculateHash(value), 69 | }, 70 | { 71 | name: 'memo', 72 | title: 'Memo', 73 | }, 74 | { 75 | name: 'number', 76 | title: 'Installments Number', 77 | }, 78 | { 79 | name: 'total', 80 | title: 'Installments Total', 81 | }, 82 | { 83 | name: 'status', 84 | title: 'Status', 85 | }, 86 | ]; 87 | 88 | const formatters = { 89 | date: (value) => formatDate(value), 90 | processedDate: (value) => formatDate(value), 91 | }; 92 | 93 | export function format(property, value) { 94 | return formatters[property] ? formatters[property](value) : value; 95 | } 96 | 97 | export function getHash(transaction) { 98 | const hashProps = properties.filter((p) => p.hash); 99 | const key = hashProps.reduce((prev, prop) => { 100 | const value = transaction[prop.name]; 101 | if (typeof prop.hash === 'function') { 102 | return prev + prop.hash(value).toString(); 103 | } 104 | return prev + value.toString(); 105 | }, ''); 106 | return key; 107 | } 108 | 109 | export function transactionArrayToObject(transactions) { 110 | return transactions.reduce((prev, current) => { 111 | const hash = current.hash || getHash(current); 112 | prev[getHash(current)] = { ...current, hash }; 113 | return prev; 114 | }, {}); 115 | } 116 | -------------------------------------------------------------------------------- /test/e2e/specs/Launch.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { testWithSpectron } from 'vue-cli-plugin-electron-builder'; 4 | import { scrapers } from '../../../src/modules/scrapers'; 5 | import Interactions from '../utils/interactions'; 6 | 7 | const screenshotsDir = './screenshots'; 8 | 9 | jest.setTimeout(1000000); 10 | 11 | // Remove when https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/625 closed 12 | const skip = process.env.GITHUB_ACTIONS && process.platform === 'win32'; 13 | 14 | (skip ? describe.skip : describe)('Launch', () => { 15 | let app; 16 | let stopServe; 17 | let browserWindow; 18 | let client; 19 | let interactions; 20 | 21 | beforeAll(async () => { 22 | let stdout; 23 | ({ app, stopServe, stdout } = await testWithSpectron()); 24 | 25 | // eslint-disable-next-line no-console 26 | console.log(stdout); 27 | }); 28 | 29 | beforeEach(async () => { 30 | app = await app.restart(); 31 | 32 | ({ client, browserWindow } = app); 33 | await client.waitUntilWindowLoaded(); 34 | interactions = new Interactions(client); 35 | }); 36 | 37 | test('shows the proper application title', async () => { 38 | // Window was created 39 | expect(await client.getWindowCount()).toBe(1); 40 | // It is not minimized 41 | expect(await browserWindow.isMinimized()).toBe(false); 42 | // Window is visible 43 | expect(await browserWindow.isVisible()).toBe(true); 44 | // Size is correct 45 | const { width, height } = await browserWindow.getBounds(); 46 | expect(width).toBeGreaterThan(0); 47 | expect(height).toBeGreaterThan(0); 48 | // App is loaded properly 49 | expect(await client.getHTML('#app')).toMatch(/Israeli Bank Scrapers Desktop/); 50 | }); 51 | 52 | test('should be AddScraper per scraper', async () => { 53 | const addScrapers = await interactions.getAddScrapers(); 54 | expect(addScrapers.length).toEqual(scrapers.length); 55 | }); 56 | 57 | test('Hide AddScraper components by default', async () => { 58 | const addScrapers = await interactions.getAddScrapers(); 59 | const visiblities = await Promise.all(addScrapers.map((scraper) => scraper.isVisible())); 60 | expect(visiblities).not.toContain(true); 61 | expect(visiblities).toContain(false); 62 | }); 63 | 64 | test('Show AddScraper components when clicking on AddScraper', async () => { 65 | await interactions.toggleLeftDrawer(); 66 | await interactions.clickCollapseAddImporter(); 67 | 68 | const addScrapers = await interactions.getAddScrapers(); 69 | const visiblities = await Promise.all(addScrapers.map((scraper) => scraper.isVisible())); 70 | expect(visiblities).not.toContain(false); 71 | }); 72 | 73 | afterEach(async () => { 74 | if (global.lastTest.failed) { 75 | if (!fs.existsSync(screenshotsDir)) { 76 | fs.mkdirSync(screenshotsDir); 77 | } 78 | 79 | const screenshotFile = path.join(screenshotsDir, `${global.lastTest.test.name.replace(/\s/g, '')}.png`); 80 | const imgBuffer = await browserWindow.capturePage(); 81 | fs.writeFileSync(screenshotFile, imgBuffer); 82 | } 83 | }); 84 | 85 | afterAll(async () => stopServe()); 86 | }); 87 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | import { createProtocol, installVueDevtools } from 'vue-cli-plugin-electron-builder/lib'; 4 | import CreateLogger from './logger'; 5 | import { initializeReporter } from './modules/reporting'; 6 | import './store'; 7 | 8 | initializeReporter(); 9 | 10 | const isDevelopment = process.env.NODE_ENV !== 'production'; 11 | 12 | // Keep a global reference of the window object, if you don't, the window will 13 | // be closed automatically when the JavaScript object is garbage collected. 14 | let mainWindow; 15 | 16 | global.logger = CreateLogger(app); 17 | const { logger } = global; 18 | 19 | function createWindow() { 20 | // Create the browser window. 21 | mainWindow = new BrowserWindow({ 22 | height: 563, 23 | useContentSize: true, 24 | width: 1000, 25 | webPreferences: { 26 | nodeIntegration: true, 27 | }, 28 | }); 29 | 30 | if (process.env.WEBPACK_DEV_SERVER_URL) { 31 | // Load the url of the dev server if in development mode 32 | mainWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL); 33 | if (!process.env.IS_TEST) mainWindow.webContents.openDevTools(); 34 | } else { 35 | createProtocol('app'); 36 | // Load the index.html when not in development 37 | mainWindow.loadURL('app://./index.html'); 38 | } 39 | mainWindow.on('closed', () => { 40 | mainWindow = null; 41 | }); 42 | } 43 | 44 | // Quit when all windows are closed. 45 | app.on('window-all-closed', () => { 46 | // On macOS it is common for applications and their menu bar 47 | // to stay active until the user quits explicitly with Cmd + Q 48 | if (process.platform !== 'darwin') { 49 | app.quit(); 50 | } 51 | }); 52 | 53 | app.on('activate', () => { 54 | // On macOS it's common to re-create a window in the app when the 55 | // dock icon is clicked and there are no other windows open. 56 | if (mainWindow === null) { 57 | createWindow(); 58 | } 59 | }); 60 | 61 | // This method will be called when Electron has finished 62 | // initialization and is ready to create browser windows. 63 | // Some APIs can only be used after this event occurs. 64 | app.on('ready', async () => { 65 | if (isDevelopment && !process.env.IS_TEST) { 66 | // Install Vue Devtools 67 | // Devtools extensions are broken in Electron 6.0.0 and greater 68 | // See https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/378 for more info 69 | // Electron will not launch with Devtools extensions installed on Windows 10 with dark mode 70 | // If you are not using Windows 10 dark mode,you may uncomment these lines 71 | // In addition, if the linked issue is closed, 72 | // you can upgrade electron and uncomment these lines 73 | try { 74 | await installVueDevtools(); 75 | } catch (e) { 76 | logger.info('Vue Devtools failed to install:', e.toString()); 77 | } 78 | } 79 | createWindow(); 80 | }); 81 | 82 | // Exit cleanly on request from parent process in development mode. 83 | if (isDevelopment) { 84 | if (process.platform === 'win32') { 85 | process.on('message', (data) => { 86 | if (data === 'graceful-exit') { 87 | app.quit(); 88 | } 89 | }); 90 | } else { 91 | process.on('SIGTERM', () => { 92 | app.quit(); 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build/Release 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "nuxt/**" 7 | push: 8 | branches: master 9 | paths-ignore: 10 | - "nuxt/**" 11 | 12 | jobs: 13 | release: 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | # Platforms to build on/for 18 | strategy: 19 | matrix: 20 | os: [windows-2019, ubuntu-18.04] 21 | 22 | steps: 23 | - uses: actions/checkout@v1 24 | - name: Set version in Env 25 | shell: bash 26 | run: | 27 | package_version=`awk -F ':' '/version/ {print $2}' package.json | sed 's/\"//g' | sed 's/,//g' | sed 's/ //g'` 28 | echo "package_version=$package_version" >> $GITHUB_ENV 29 | echo "Version is $package_version" 30 | - uses: actions/github-script@0.3.0 31 | name: Check if current version is a published release (by release tag) 32 | id: check_release 33 | with: 34 | github-token: ${{secrets.GITHUB_TOKEN}} 35 | # debug: true 36 | script: | 37 | const releases = await github.repos.listReleases({ 38 | owner: 'baruchiro', 39 | repo: 'israeli-bank-scrapers-desktop' 40 | }) 41 | // console.log(releases) 42 | const published_release_tags = releases.data.filter(release => !release.draft).map(release => release.tag_name) 43 | // console.log(published_release_tags) 44 | // github.event_name: ${{ github.event_name }} 45 | // github.ref: ${{ github.ref }} 46 | const isPushToMaster = ${{ startsWith(github.event_name, 'push') && github.ref == 'refs/heads/master' }} 47 | console.log('Is push to mster: ' + isPushToMaster) 48 | const publishToRelease = !published_release_tags.includes('v${{ env.package_version }}') && isPushToMaster 49 | console.log('publishToRelease: ' + publishToRelease) 50 | return publishToRelease 51 | - name: Install Node.js, NPM and Yarn 52 | uses: actions/setup-node@v1 53 | with: 54 | node-version: 10 55 | - name: Install Snapcraft 56 | uses: samuelmeuli/action-snapcraft@10d7d0a84d9d86098b19f872257df314b0bd8e2d 57 | # Only install Snapcraft on Ubuntu 58 | if: startsWith(matrix.os, 'ubuntu') 59 | with: 60 | snapcraft_token: ${{ secrets.snapcraft_token }} 61 | - name: Set 'publish' parameter 62 | shell: python 63 | run: | 64 | import os 65 | param = 'always' if '${{ steps.check_release.outputs.result }}' == 'true' else 'never' 66 | print("Will {} (for this run) publish files to draft release".format(param)) 67 | with open(os.getenv('GITHUB_ENV'), 'a') as envFile: envFile.write('PUBLISH_PARAM=' + param) 68 | - name: Build & release Electron app 69 | shell: bash 70 | env: 71 | GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} 72 | GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} 73 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | NODE_OPTIONS: --max-old-space-size=4096 75 | run: | 76 | yarn 77 | yarn build --publish $PUBLISH_PARAM 78 | - name: Upload artifact 79 | uses: actions/upload-artifact@v1.0.0 80 | with: 81 | # Artifact name 82 | name: ${{ matrix.os }}-artifact 83 | # Directory containing files to upload 84 | path: dist_electron 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "israeli-bank-scrapers-desktop", 3 | "version": "0.3.0-beta", 4 | "author": "Baruch Rothkoff ", 5 | "description": "An Electron-vue project for israeli-bank-scrapers", 6 | "license": "MIT", 7 | "main": "background.js", 8 | "private": true, 9 | "scripts": { 10 | "serve": "vue-cli-service electron:serve", 11 | "build": "vue-cli-service electron:build", 12 | "postinstall": "electron-builder install-app-deps", 13 | "postuninstall": "electron-builder install-app-deps", 14 | "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src", 15 | "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src", 16 | "test": "yarn unit && yarn e2e", 17 | "unit": "vue-cli-service test:unit", 18 | "e2e": "vue-cli-service test:unit --config='test/e2e/jest.e2e.config.js'", 19 | "snyk-protect": "snyk protect" 20 | }, 21 | "husky": { 22 | "hooks": { 23 | "pre-push": "yarn lint" 24 | } 25 | }, 26 | "dependencies": { 27 | "@getstation/electron-google-oauth2": "2.1.0", 28 | "@sentry/electron": "^2.0.0", 29 | "core-js": "^3.9.1", 30 | "download-chromium": "^2.2.1", 31 | "electron-log": "^4.2.4", 32 | "googleapis": "^67.1.1", 33 | "israeli-bank-scrapers-core": "^1.0.2", 34 | "keytar": "^7.4.0", 35 | "moment": "^2.27.0", 36 | "vue": "^2.6.12", 37 | "vue-router": "^3.1.6", 38 | "vuetify": "^2.4.5", 39 | "vuex": "^3.5.1", 40 | "vuex-electron": "^1.0.3" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.13.8", 44 | "@babel/eslint-parser": "^7.11.3", 45 | "@babel/preset-env": "^7.11.0", 46 | "@babel/register": "^7.13.8", 47 | "@vue/cli-plugin-babel": "^4.3.1", 48 | "@vue/cli-plugin-eslint": "^4.5.3", 49 | "@vue/cli-plugin-router": "^4.4.6", 50 | "@vue/cli-plugin-unit-jest": "^4.3.1", 51 | "@vue/cli-plugin-vuex": "^4.3.1", 52 | "@vue/cli-service": "^4.4.6", 53 | "@vue/eslint-config-prettier": "^6.0.0", 54 | "@vue/test-utils": "^1.0.3", 55 | "babel-core": "^7.0.0-bridge.0", 56 | "babel-jest": "^26.0.1", 57 | "babel-plugin-component": "^1.1.1", 58 | "electron": "^5.0.0", 59 | "eslint": "^7.19.0", 60 | "eslint-config-airbnb-base": "^14.2.0", 61 | "eslint-config-prettier": "^6.11.0", 62 | "eslint-friendly-formatter": "^4.0.1", 63 | "eslint-import-resolver-alias": "^1.1.2", 64 | "eslint-loader": "^4.0.2", 65 | "eslint-plugin-html": "^6.0.3", 66 | "eslint-plugin-import": "^2.22.0", 67 | "eslint-plugin-jest": "^24.1.0", 68 | "eslint-plugin-node": "^11.1.0", 69 | "eslint-plugin-prettier": "^3.1.4", 70 | "eslint-plugin-promise": "^4.2.1", 71 | "eslint-plugin-standard": "^5.0.0", 72 | "eslint-plugin-vue": "^7.5.0", 73 | "husky": "^4.2.5", 74 | "identity-obj-proxy": "^3.0.0", 75 | "jest": "^26.6.3", 76 | "jest-circus": "^26.0.1", 77 | "jest-config": "^26.0.1", 78 | "jest-environment-jsdom": "^26.6.2", 79 | "prettier": "^2.2.1", 80 | "replace-in-file": "6.1.0", 81 | "sass": "^1.32.8", 82 | "sass-loader": "^10.0.2", 83 | "snyk": "^1.440.1", 84 | "vue-cli-plugin-electron-builder": "^1.4.6", 85 | "vue-cli-plugin-vuetify": "~2.2.2", 86 | "vue-jest": "^3.0.5", 87 | "vue-template-compiler": "^2.6.12", 88 | "vuetify-loader": "^1.4.4" 89 | }, 90 | "resolutions": { 91 | "deepmerge": "^4.0.0", 92 | "electron-builder": "^22.0.0" 93 | }, 94 | "snyk": true 95 | } 96 | -------------------------------------------------------------------------------- /scripts/sentry-symbols.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | To upload debug information for native crashes when updating Electron, run: 5 | 6 | npm install --save-dev @sentry/cli electron-download 7 | node sentry-symbols.js 8 | 9 | For more information, see https://docs.sentry.io/clients/electron/ 10 | */ 11 | let SentryCli; 12 | let download; 13 | 14 | try { 15 | SentryCli = require('@sentry/cli'); 16 | download = require('electron-download'); 17 | } catch (e) { 18 | console.error('ERROR: Missing required packages, please run:'); 19 | console.error('npm install --save-dev @sentry/cli electron-download'); 20 | process.exit(1); 21 | } 22 | 23 | const VERSION = /\bv?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?\b/i; 24 | const SYMBOL_CACHE_FOLDER = '.electron-symbols'; 25 | const package = require('./package.json'); 26 | const sentryCli = new SentryCli('./sentry.properties'); 27 | 28 | async function main() { 29 | let version = getElectronVersion(); 30 | if (!version) { 31 | console.error('Cannot detect electron version, check package.json'); 32 | return; 33 | } 34 | 35 | console.log('We are starting to download all possible electron symbols'); 36 | console.log('We need it in order to symbolicate native crashes'); 37 | console.log( 38 | 'This step is only needed once whenever you update your electron version', 39 | ); 40 | console.log('Just call this script again it should do everything for you.'); 41 | 42 | let zipPath = await downloadSymbols({ 43 | version, 44 | platform: 'darwin', 45 | arch: 'x64', 46 | dsym: true, 47 | }); 48 | await sentryCli.execute(['upload-dif', '-t', 'dsym', zipPath], true); 49 | 50 | zipPath = await downloadSymbols({ 51 | version, 52 | platform: 'win32', 53 | arch: 'ia32', 54 | symbols: true, 55 | }); 56 | await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true); 57 | 58 | zipPath = await downloadSymbols({ 59 | version, 60 | platform: 'win32', 61 | arch: 'x64', 62 | symbols: true, 63 | }); 64 | await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true); 65 | 66 | zipPath = await downloadSymbols({ 67 | version, 68 | platform: 'linux', 69 | arch: 'x64', 70 | symbols: true, 71 | }); 72 | await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true); 73 | 74 | console.log('Finished downloading and uploading to Sentry'); 75 | console.log(`Feel free to delete the ${SYMBOL_CACHE_FOLDER}`); 76 | } 77 | 78 | function getElectronVersion() { 79 | if (!package) { 80 | return false; 81 | } 82 | 83 | let electronVersion = 84 | (package.dependencies && package.dependencies.electron) || 85 | (package.devDependencies && package.devDependencies.electron); 86 | 87 | if (!electronVersion) { 88 | return false; 89 | } 90 | 91 | const matches = VERSION.exec(electronVersion); 92 | return matches ? matches[0] : false; 93 | } 94 | 95 | async function downloadSymbols(options) { 96 | return new Promise((resolve, reject) => { 97 | download( 98 | { 99 | ...options, 100 | cache: SYMBOL_CACHE_FOLDER, 101 | }, 102 | (err, zipPath) => { 103 | if (err) { 104 | reject(err); 105 | } else { 106 | resolve(zipPath); 107 | } 108 | }, 109 | ); 110 | }); 111 | } 112 | 113 | main().catch(e => console.error(e)); 114 | -------------------------------------------------------------------------------- /src/components/MainPage/Importers.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 124 | 125 | 130 | -------------------------------------------------------------------------------- /src/components/MainPage.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 117 | 118 | 121 | -------------------------------------------------------------------------------- /nuxt/components/TheHeader.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 142 | -------------------------------------------------------------------------------- /nuxt/components/DownloadButton.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 150 | 151 | 154 | -------------------------------------------------------------------------------- /test/unit/specs/credentials.spec.js: -------------------------------------------------------------------------------- 1 | import { encryptObject, decryptObject } from 'modules/encryption/credentials.js'; 2 | 3 | function isNotNullOrEmptyOrUndefined(value) { 4 | return value && value !== null && value !== ''; 5 | } 6 | 7 | test('Should encrypt value of simple object', async () => { 8 | const original = { key: 'value' }; 9 | const encrypted = await encryptObject(original); 10 | 11 | const originalKeys = Object.keys(original); 12 | const encryptedKeys = Object.keys(encrypted); 13 | 14 | // keys 15 | expect(encryptedKeys.length).toEqual(1); 16 | expect(encryptedKeys[0]).toEqual(originalKeys[0]); 17 | 18 | // values 19 | expect(isNotNullOrEmptyOrUndefined(encrypted[encryptedKeys[0]])).toBe(true); 20 | expect(encrypted[encryptedKeys[0]]).not.toEqual(original[originalKeys[0]]); 21 | }); 22 | 23 | test('Should not encrypt a number value', async () => { 24 | const original = { key: 1234 }; 25 | const encrypted = await encryptObject(original) 26 | expect(encrypted).toStrictEqual(original); 27 | }); 28 | 29 | test('Should encrypt an array values', async () => { 30 | const original = { key: ['arr1', 'arr2', 'arr3'] }; 31 | const encrypted = await encryptObject(original); 32 | 33 | const originalKeys = Object.keys(original); 34 | const encryptedKeys = Object.keys(encrypted); 35 | 36 | const originalValue = original[originalKeys[0]]; 37 | const encryptedValue = encrypted[encryptedKeys[0]]; 38 | 39 | // keys 40 | expect(encryptedKeys.length).toEqual(1); 41 | expect(encryptedKeys[0]).toEqual(originalKeys[0]); 42 | 43 | // values 44 | expect(isNotNullOrEmptyOrUndefined(encryptedValue)).toBe(true); 45 | expect(Array.isArray(encryptedValue)).toBe(true); 46 | expect(encryptedValue).not.toEqual(originalValue); 47 | }); 48 | 49 | test('Should encrypt a complex object values', async () => { 50 | const original = { 51 | arrKey: ['arr1', 'arr2', 'arr3'], 52 | arrComplex: [ 53 | 'arrval1', 54 | { objKey: 'objval' }, 55 | ['arrarrval1', 'arrarrval2'], 56 | ], 57 | objKey: { 58 | strKey: 'strValue', 59 | arrKey: ['arrval1', 'arrval2'], 60 | }, 61 | strKey: 'strval', 62 | }; 63 | const encrypted = await encryptObject(original); 64 | 65 | // values - arrKey 66 | expect(isNotNullOrEmptyOrUndefined(encrypted.arrKey)).toBe(true); 67 | expect(Array.isArray(encrypted.arrKey)).toBe(true); 68 | expect(encrypted.arrKey).toHaveLength(original.arrKey.length); 69 | expect(encrypted.arrKey).not.toStrictEqual(original.arrKey); 70 | 71 | // values - arrComplex 72 | expect(isNotNullOrEmptyOrUndefined(encrypted.arrComplex)).toBe(true); 73 | expect(Array.isArray(encrypted.arrComplex)).toBe(true); 74 | expect(encrypted.arrComplex).toHaveLength(original.arrComplex.length); 75 | expect(encrypted.arrComplex).not.toStrictEqual(original.arrComplex); 76 | 77 | // values - arrComplex[1] (objKey) 78 | expect(isNotNullOrEmptyOrUndefined(encrypted.arrComplex[1].objKey)).toBe(true); 79 | expect(typeof encrypted.arrComplex[1]).toBe('object'); 80 | expect(encrypted.arrComplex[1]).toMatchObject(encrypted.arrComplex[1]); 81 | expect(encrypted.arrComplex[1]).not.toStrictEqual(original.arrComplex[1]); 82 | 83 | // values - arrComplex[2] (Array) 84 | expect(isNotNullOrEmptyOrUndefined(encrypted.arrComplex[2])).toBe(true); 85 | expect(Array.isArray(encrypted.arrComplex[2])).toBe(true); 86 | expect(encrypted.arrComplex[2]).toHaveLength(original.arrComplex[2].length); 87 | expect(encrypted.arrComplex[2]).not.toStrictEqual(original.arrComplex[2]); 88 | 89 | // values - objKey 90 | expect(isNotNullOrEmptyOrUndefined(encrypted.objKey)).toBe(true); 91 | expect(typeof encrypted.objKey).toBe('object'); 92 | expect(encrypted.objKey).toMatchObject(encrypted.objKey); 93 | expect(encrypted.objKey).not.toStrictEqual(original.objKey); 94 | 95 | // values - objKey.arrKey (Array) 96 | expect(isNotNullOrEmptyOrUndefined(encrypted.objKey.arrKey)).toBe(true); 97 | expect(Array.isArray(encrypted.objKey.arrKey)).toBe(true); 98 | expect(encrypted.objKey.arrKey).toHaveLength(original.objKey.arrKey.length); 99 | expect(encrypted.objKey.arrKey).not.toStrictEqual(original.objKey.arrKey); 100 | }); 101 | 102 | test('Should encrypt and decrypt and get exactly the same object', async () => { 103 | const original = { 104 | arrKey: ['arr1', 'arr2', 'arr3'], 105 | arrComplex: [ 106 | 'arrval1', 107 | { objKey: 'objval' }, 108 | ['arrarrval1', 'arrarrval2'], 109 | ], 110 | objKey: { 111 | strKey: 'strValue', 112 | arrKey: ['arrval1', 'arrval2'], 113 | }, 114 | strKey: 'strval', 115 | }; 116 | const actual = await decryptObject(await encryptObject(original)); 117 | 118 | expect(actual).toStrictEqual(original); 119 | }); 120 | -------------------------------------------------------------------------------- /nuxt/assets/img/cloud_files.svg: -------------------------------------------------------------------------------- 1 | cloud_files -------------------------------------------------------------------------------- /src/components/MainPage/Importers/Importer.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 161 | 162 | 178 | -------------------------------------------------------------------------------- /src/modules/spreadsheet/spreadsheet.js: -------------------------------------------------------------------------------- 1 | import { google } from 'googleapis'; 2 | import { properties as transactionProperties, transactionArrayToObject } from '../transactions'; 3 | 4 | // TODO: I think this module should be a class or two. 5 | const spreadsheetConstName = '_ibsd'; 6 | 7 | const headers = transactionProperties.map((property) => property.name); 8 | const requiredHeaders = transactionProperties.filter((property) => property.hash) 9 | .map((property) => property.name); 10 | 11 | const getSpreadsheetRequest = (spreadsheetId) => ({ 12 | spreadsheetId, 13 | includeGridData: false, 14 | }); 15 | 16 | const createSheetRequest = (spreadsheetId) => ({ 17 | spreadsheetId, 18 | requestBody: 19 | { 20 | includeSpreadsheetInResponse: true, 21 | responseIncludeGridData: false, 22 | requests: [ 23 | { 24 | addSheet: { 25 | properties: { 26 | title: spreadsheetConstName, 27 | }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | }); 33 | 34 | async function getSheetsTitles(sheets, spreadsheetId) { 35 | const spreadsheet = await sheets.spreadsheets.get(getSpreadsheetRequest(spreadsheetId)) 36 | .then((res) => res.data); 37 | return spreadsheet.sheets.map((sheet) => sheet.properties.title); 38 | } 39 | 40 | async function createNewSheetIfNotExist(sheets, spreadsheetId) { 41 | const sheetsTitles = await getSheetsTitles(sheets, spreadsheetId); 42 | if (!sheetsTitles.includes(spreadsheetConstName)) { 43 | await sheets.spreadsheets.batchUpdate(createSheetRequest(spreadsheetId)); 44 | } 45 | } 46 | 47 | async function fetchExistingTransactions(sheets, spreadsheetId) { 48 | try { 49 | const response = await sheets.spreadsheets.values.get({ 50 | spreadsheetId, 51 | range: spreadsheetConstName, 52 | }); 53 | return response.data.values || [headers]; 54 | } catch (error) { 55 | if (error.code && error.code === 400) { 56 | return [headers]; 57 | } 58 | throw error; 59 | } 60 | } 61 | 62 | function zip(keys, values) { 63 | return keys.reduce((prev, current, index) => ({ ...prev, [current]: values[index] }), {}); 64 | } 65 | 66 | function validateHeaders(headersRow) { 67 | const missingHeaders = requiredHeaders.filter((header) => !headersRow.includes(header)); 68 | if (missingHeaders.length > 0) { 69 | throw new Error(`Missing required headers: [${missingHeaders}] in [${headersRow}]`); 70 | } 71 | } 72 | 73 | function convertTransactionsArraysToObject(transactionsArrays) { 74 | const headersRow = transactionsArrays.shift(); 75 | validateHeaders(headersRow); 76 | const arrayOfObjects = transactionsArrays.map((transaction) => zip(headers, transaction)); 77 | return transactionArrayToObject(arrayOfObjects); 78 | } 79 | 80 | async function getExistingTransactionsObject(sheets, spreadsheetId) { 81 | const existsArrays = await fetchExistingTransactions(sheets, spreadsheetId); 82 | return convertTransactionsArraysToObject(existsArrays); 83 | } 84 | 85 | function convertTransactionObjectToArrays(transactionObject) { 86 | const rows = [headers]; 87 | Object.values(transactionObject).forEach((transaction) => { 88 | const transactionRow = []; 89 | headers.forEach((header) => { 90 | transactionRow.push(transaction[header]); 91 | }); 92 | rows.push(transactionRow); 93 | }); 94 | return rows; 95 | } 96 | 97 | async function saveTransactionsAsObjectToGoogleSheets(sheets, spreadsheetId, transactionObject) { 98 | const arrays = convertTransactionObjectToArrays(transactionObject); 99 | await createNewSheetIfNotExist(sheets, spreadsheetId); 100 | const request = { 101 | spreadsheetId, 102 | range: spreadsheetConstName, 103 | valueInputOption: 'USER_ENTERED', 104 | includeValuesInResponse: true, 105 | resource: { 106 | values: arrays, 107 | }, 108 | }; 109 | return sheets.spreadsheets.values.update(request); 110 | } 111 | 112 | export async function saveTransactionsToGoogleSheets( 113 | auth, 114 | spreadsheetId, 115 | transactionsObject, 116 | ) { 117 | const sheets = google.sheets({ version: 'v4', auth }); 118 | const existTransactionsObject = await getExistingTransactionsObject(sheets, spreadsheetId); 119 | const existTransactions = Object.keys(existTransactionsObject).length; 120 | const combinedTransactions = { ...existTransactionsObject, ...transactionsObject }; 121 | const response = await saveTransactionsAsObjectToGoogleSheets( 122 | sheets, 123 | spreadsheetId, 124 | combinedTransactions, 125 | ); 126 | 127 | return { 128 | status: response.status, 129 | existTransactions, 130 | updatedTransactions: response.data.updatedRows - 1, 131 | statusText: response.statusText, 132 | }; 133 | } 134 | 135 | export async function listAllSpreadsheets(auth) { 136 | const drive = google.drive({ version: 'v3', auth }); 137 | 138 | const response = await drive.files.list({ 139 | q: 'mimeType="application/vnd.google-apps.spreadsheet"', 140 | }); 141 | 142 | return response.data.files; 143 | } 144 | 145 | export async function createNewSpreadsheet(auth, title) { 146 | const resource = { 147 | properties: { 148 | title, 149 | }, 150 | sheets: [ 151 | { 152 | properties: { 153 | title: spreadsheetConstName, 154 | }, 155 | }, 156 | ], 157 | }; 158 | const sheets = google.sheets({ version: 'v4', auth }); 159 | 160 | return (await sheets.spreadsheets.create({ resource })).data; 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The project moved to https://github.com/brafdlog/budget-tracking. 2 | 3 | ----- 4 | 5 | ----- 6 | 7 | ----- 8 | 9 | # israeli-bank-scrapers-desktop 10 | 11 | Secure desktop app for retriving your bank transactions. Works for all israeli banks and credit cards. 12 | 13 | Based on [israeli-bank-scrapers](https://github.com/eshaham/israeli-bank-scrapers) project. 14 | 15 | ![Build/Release](https://github.com/baruchiro/israeli-bank-scrapers-desktop/workflows/Build/Release/badge.svg?branch=master&event=push) 16 | [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/baruchiro/israeli-bank-scrapers-desktop.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/baruchiro/israeli-bank-scrapers-desktop/context:javascript) 17 | [![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=baruchiro/israeli-bank-scrapers-desktop)](https://dependabot.com) 18 | 19 | ## Security Principles 20 | 21 | This app has two main principles: 22 | 23 | 1. **Local running:** This app accesses the bank's website exactly the same way you do. 24 | Therefore, you don't have to rely on a third party to keep your passwords. 25 | 26 | 2. **Open Source:** Don't believe me the information wasn't sent out? 27 | The source code is right here. Read it! (or ask a friend to). See for yourself that there is no malicious code here. 28 | 29 | ## Welcome to the beta 30 | 31 | The project is currently in beta, which means we focus on two main goals: 32 | 33 | 1. **Minimal Valuable Flow:** 34 | Bringing the app to a level where the user can perform the minimum flow - importing and exporting data in a convenient and clear way, without any critical bugs. 35 | 36 | 2. **Open Source Project:** 37 | Bringing the project to a stage where it is easy for new contributors to understand and contribute to. This includes good documentation, testing, etc. 38 | 39 | When you look at the project, please try to think about both of these goals. 40 | 41 | [Beta milestone](https://github.com/baruchiro/israeli-bank-scrapers-desktop/issues?q=is%3Aopen+is%3Aissue+milestone%3ABeta) 42 | 43 | ## Contributing 44 | 45 | The project is an *Electron* app, with *Vue* in the front-end, and mainly uses [israeli-bank-scrapers](https://github.com/eshaham/israeli-bank-scrapers) project. 46 | 47 | ### I need your help 48 | 49 | This is the first time I'm writing code in *NodeJS*, and of course it's the first time I'm using *Electron* and *Vue*. 50 | 51 | I wrote this version "quick and dirty", because the goal was to get to the solution up and running as quickly as possible. If there is interest, I can continue to develop the app as I work on other projects as well, and of course I would love to get help. 52 | 53 | #### You can help with the following: 54 | 55 | Please note that we are currently in **beta**, and issues under [Beta Milestone](https://github.com/baruchiro/israeli-bank-scrapers-desktop/issues?q=is%3Aopen+is%3Aissue+milestone%3ABeta) are prioritized. 56 | 57 | - [Opening new issues](https://github.com/baruchiro/israeli-bank-scrapers-desktop/issues/new) 58 | - [Good first issues](https://github.com/baruchiro/israeli-bank-scrapers-desktop/contribute) 59 | - English Corrections- I know I need help with English. Please report language mistakes. 60 | - [Brainstorming](https://github.com/baruchiro/israeli-bank-scrapers-desktop/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abrainstorming+)- Design issues that I need help with and consultation from experienced people. 61 | - [Help wanted](https://github.com/baruchiro/israeli-bank-scrapers-desktop/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)- Issues I don't think I can do at this time. 62 | 63 | ### Prerequisites 64 | 65 | - [NodeJS](https://nodejs.org/) version `^10.13.0` 66 | - [Yarn](https://classic.yarnpkg.com) (`v1`-classic) 67 | 68 | #### Linux 69 | 70 | Currently, this project depends on `libsecret`, so you may need to install it before running `yarn`. 71 | 72 | Depending on your distribution, you will need to run the following command: 73 | 74 | * Debian/Ubuntu: `sudo apt-get install libsecret-1-dev` 75 | * Red Hat-based: `sudo yum install libsecret-devel` 76 | * Arch Linux: `sudo pacman -S libsecret` 77 | 78 | Then you can run the commands below: 79 | 80 | ### Build Setup 81 | 82 | ``` bash 83 | # install dependencies 84 | yarn 85 | 86 | # serve with hot reload at localhost:9080 87 | yarn serve 88 | 89 | # build electron application for production 90 | yarn build 91 | 92 | # run unit & end-to-end tests 93 | yarn test 94 | 95 | # lint all JS/Vue component files in `src/` 96 | yarn lint 97 | 98 | ``` 99 | 100 | ### Project Structure 101 | 102 | ``` 103 | +---.github/workflows // Github Actions files 104 | +---build/icons // Icons for Electron-Builder (used in vue.config.js) 105 | +---dist/electron // Webpack temporary output 106 | | 107 | +---dist_electron // Installer and info files 108 | | +---bundled // Bundle 109 | | +---win-unpacked // Unpacked- what you will get after you run the installer 110 | | 111 | +---docs // Resources for documentation porpuse 112 | +---public 113 | +---scripts // Scripts for Git hooks and other needs 114 | | 115 | +---src 116 | | +---assets 117 | | +---components 118 | | +---modules 119 | | +---plugins // Vue plugins 120 | | +---router // Vue-Router (for future use) 121 | | \---store // Vuex 122 | | +---migrations 123 | | +---modules 124 | | 125 | \---test 126 | | +---e2e 127 | | +---unit 128 | | 129 | | vue.config.js // vue-cli-plugin-electron-builder (includes electron-builder config) 130 | ``` 131 | -------------------------------------------------------------------------------- /src/components/MainPage/ReportProblemDialog.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 192 | 193 | 195 | -------------------------------------------------------------------------------- /src/components/MainPage/Exporters/SpreadsheetExporter.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /nuxt/test/TheHeader.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import TheHeader from '@/components/TheHeader'; 3 | 4 | const clickEvent = () => { 5 | const event = new MouseEvent('click', { 6 | view: window, 7 | bubbles: true, 8 | cancelable: true, 9 | }); 10 | document.dispatchEvent(event); 11 | }; 12 | 13 | const scrollEvent = () => { 14 | const event = new CustomEvent('scroll'); 15 | document.dispatchEvent(event); 16 | }; 17 | 18 | const onClickSpy = jest.fn(); 19 | const onScrollSpy = jest.fn(); 20 | 21 | const factory = () => shallowMount(TheHeader); 22 | 23 | const spiedFactory = () => shallowMount(TheHeader, { 24 | attachToDocument: true, 25 | methods: { onClick: onClickSpy, onScroll: onScrollSpy }, 26 | }); 27 | 28 | describe('TheHeader', () => { 29 | it('is a Vue instance', () => { 30 | const wrapper = factory(); 31 | expect(wrapper.isVueInstance()).toBeTruthy(); 32 | }); 33 | 34 | it('renders properly', () => { 35 | const wrapper = factory(); 36 | expect(wrapper.html()).toMatchSnapshot(); 37 | }); 38 | 39 | it('sets the attribute scrollY to 0', () => { 40 | const wrapper = factory(); 41 | expect(wrapper.vm.scrollY).toBe(0); 42 | }); 43 | 44 | it('sets the attribute isOpen to false', () => { 45 | const wrapper = factory(); 46 | expect(wrapper.vm.isOpen).toBe(false); 47 | }); 48 | 49 | describe('when mounted', () => { 50 | let addEventSpy; 51 | 52 | beforeEach(() => { 53 | addEventSpy = jest.spyOn(document, 'addEventListener'); 54 | }); 55 | 56 | it('adds a click event listener', () => { 57 | const wrapper = factory(); 58 | expect(addEventSpy).toHaveBeenCalledWith('scroll', wrapper.vm.onScroll); 59 | }); 60 | 61 | it('adds a scroll event listener', () => { 62 | const wrapper = factory(); 63 | expect(addEventSpy).toHaveBeenCalledWith('click', wrapper.vm.onClick); 64 | }); 65 | }); 66 | 67 | describe('when destroyed', () => { 68 | let wrapper; 69 | let removeEventSpy; 70 | 71 | beforeEach(() => { 72 | wrapper = factory(); 73 | removeEventSpy = jest.spyOn(document, 'removeEventListener'); 74 | }); 75 | 76 | it('removes a click event listener', () => { 77 | wrapper.destroy(); 78 | expect(removeEventSpy).toHaveBeenCalledWith( 79 | 'scroll', 80 | wrapper.vm.onScroll, 81 | true, 82 | ); 83 | }); 84 | 85 | it('removes a scroll event listener', () => { 86 | wrapper.destroy(); 87 | expect(removeEventSpy).toHaveBeenCalledWith( 88 | 'click', 89 | wrapper.vm.onClick, 90 | true, 91 | ); 92 | }); 93 | }); 94 | 95 | describe('when the user clicks on the hamburger menu', () => { 96 | it('onToggleClick is triggered', () => { 97 | const wrapper = factory(); 98 | jest.spyOn(wrapper.vm, 'onToggleClick'); 99 | wrapper.find('button').trigger('click'); 100 | expect(wrapper.vm.onToggleClick).toBeCalled(); 101 | }); 102 | 103 | it('sets the property isOpen to true', () => { 104 | const wrapper = factory(); 105 | expect(wrapper.vm.isOpen).toBe(false); 106 | wrapper.vm.onToggleClick(); 107 | expect(wrapper.vm.isOpen).toBe(true); 108 | }); 109 | 110 | it('displays the actions', (done) => { 111 | const wrapper = factory(); 112 | expect(wrapper.vm.navContentClassList).toEqual( 113 | expect.stringContaining('hidden'), 114 | ); 115 | wrapper.find('button').trigger('click'); 116 | wrapper.vm.$nextTick(() => { 117 | expect(wrapper.vm.navContentClassList).toEqual( 118 | expect.not.stringContaining('hidden'), 119 | ); 120 | done(); 121 | }); 122 | }); 123 | }); 124 | 125 | describe('when the user clicks outside of the burger menu', () => { 126 | it('onClick is triggered', () => { 127 | spiedFactory(); 128 | clickEvent(); 129 | expect(onClickSpy).toBeCalled(); 130 | }); 131 | 132 | it('sets the property isOpen to false', () => { 133 | const wrapper = factory(); 134 | wrapper.vm.onClick(); 135 | expect(wrapper.vm.isOpen).toBe(false); 136 | }); 137 | 138 | it('hides the actions', (done) => { 139 | const wrapper = factory(); 140 | clickEvent(); 141 | wrapper.vm.$nextTick(() => { 142 | expect(wrapper.vm.navContentClassList).toEqual( 143 | expect.stringContaining('hidden'), 144 | ); 145 | done(); 146 | }); 147 | }); 148 | }); 149 | 150 | describe('when the user clicks scrolls on the page', () => { 151 | it('onScroll is triggered', () => { 152 | spiedFactory(); 153 | scrollEvent(); 154 | expect(onScrollSpy).toBeCalled(); 155 | }); 156 | 157 | it('sets the property isOpen to false', () => { 158 | const wrapper = factory(); 159 | window.scrollY = 10; 160 | scrollEvent(); 161 | expect(wrapper.vm.scrollY).toBe(10); 162 | }); 163 | 164 | describe('<= 10px scrollY', () => { 165 | it('does not add the sticky classes to the header', () => { 166 | const wrapper = factory(); 167 | wrapper.setData({ scrollY: 10 }); 168 | expect(wrapper.vm.headerClassList).toEqual( 169 | expect.not.stringContaining('bg-white shadow'), 170 | ); 171 | }); 172 | 173 | it('does not add the sticky classes to the navActions', () => { 174 | const wrapper = factory(); 175 | wrapper.setData({ scrollY: 10 }); 176 | expect(wrapper.vm.navActionClassList).toEqual( 177 | expect.not.stringContaining('gradient text-white'), 178 | ); 179 | expect(wrapper.vm.navActionClassList).toEqual( 180 | expect.stringContaining('bg-white text-gray-800'), 181 | ); 182 | }); 183 | 184 | it('does not add the sticky classes to the navContent', () => { 185 | const wrapper = factory(); 186 | wrapper.setData({ scrollY: 10 }); 187 | expect(wrapper.vm.navContentClassList).toEqual( 188 | expect.not.stringContaining('bg-white'), 189 | ); 190 | expect(wrapper.vm.navContentClassList).toEqual( 191 | expect.stringContaining('bg-gray-100'), 192 | ); 193 | }); 194 | }); 195 | 196 | describe('> 10px scrollY', () => { 197 | it('adds the sticky classes to the header', () => { 198 | const wrapper = factory(); 199 | wrapper.setData({ scrollY: 11 }); 200 | expect(wrapper.vm.headerClassList).toEqual( 201 | expect.stringContaining('bg-white shadow'), 202 | ); 203 | }); 204 | 205 | it('adds the sticky classes to the navActions', () => { 206 | const wrapper = factory(); 207 | wrapper.setData({ scrollY: 11 }); 208 | expect(wrapper.vm.navActionClassList).toEqual( 209 | expect.stringContaining('gradient text-white'), 210 | ); 211 | expect(wrapper.vm.navActionClassList).toEqual( 212 | expect.not.stringContaining('bg-white text-gray-800'), 213 | ); 214 | }); 215 | 216 | it('adds the sticky classes to the navContent', () => { 217 | const wrapper = factory(); 218 | wrapper.setData({ scrollY: 11 }); 219 | expect(wrapper.vm.navContentClassList).toEqual( 220 | expect.stringContaining('bg-white'), 221 | ); 222 | expect(wrapper.vm.navContentClassList).toEqual( 223 | expect.not.stringContaining('bg-gray-100'), 224 | ); 225 | }); 226 | }); 227 | }); 228 | }); 229 | --------------------------------------------------------------------------------