├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config.xml ├── config ├── karma-test-shim.js ├── karma.conf.js ├── protractor.conf.js ├── uglifyjs.conf.js └── webpack.test.js ├── e2e ├── account-creation │ ├── account-creation.e2e-spec.ts │ └── account-creation.po.ts ├── account │ ├── account.e2e-spec.ts │ └── account.po.ts ├── amount │ ├── amount.e2e-spec.ts │ └── amount.po.ts ├── main.po.ts └── tsconfig.json ├── hooks └── README.md ├── ionic.config.json ├── package-lock.json ├── package.json ├── res ├── drawable-port-hdpi │ └── screen.png ├── drawable-port-ldpi │ └── screen.png ├── drawable-port-mdpi │ └── screen.png ├── drawable-port-xhdpi │ └── screen.png ├── drawable-port-xxhdpi │ └── screen.png ├── drawable-port-xxxhdpi │ └── screen.png ├── mipmap-hdpi │ └── icon.png ├── mipmap-ldpi │ └── icon.png ├── mipmap-mdpi │ └── icon.png ├── mipmap-xhdpi │ └── icon.png ├── mipmap-xxhdpi │ └── icon.png └── mipmap-xxxhdpi │ └── icon.png ├── resources ├── android │ ├── icon │ │ ├── drawable-hdpi-icon.png │ │ ├── drawable-ldpi-icon.png │ │ ├── drawable-mdpi-icon.png │ │ ├── drawable-xhdpi-icon.png │ │ ├── drawable-xxhdpi-icon.png │ │ └── drawable-xxxhdpi-icon.png │ └── splash │ │ ├── drawable-land-hdpi-screen.png │ │ ├── drawable-land-ldpi-screen.png │ │ ├── drawable-land-mdpi-screen.png │ │ ├── drawable-land-xhdpi-screen.png │ │ ├── drawable-land-xxhdpi-screen.png │ │ ├── drawable-land-xxxhdpi-screen.png │ │ ├── drawable-port-hdpi-screen.png │ │ ├── drawable-port-ldpi-screen.png │ │ ├── drawable-port-mdpi-screen.png │ │ ├── drawable-port-xhdpi-screen.png │ │ ├── drawable-port-xxhdpi-screen.png │ │ └── drawable-port-xxxhdpi-screen.png ├── icon.png ├── icon.png.md5 ├── ios │ ├── icon │ │ ├── icon-40.png │ │ ├── icon-40@2x.png │ │ ├── icon-50.png │ │ ├── icon-50@2x.png │ │ ├── icon-60.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-72.png │ │ ├── icon-72@2x.png │ │ ├── icon-76.png │ │ ├── icon-76@2x.png │ │ ├── icon-small.png │ │ ├── icon-small@2x.png │ │ ├── icon-small@3x.png │ │ ├── icon.png │ │ └── icon@2x.png │ └── splash │ │ ├── Default-568h@2x~iphone.png │ │ ├── Default-667h.png │ │ ├── Default-736h.png │ │ ├── Default-Landscape-736h.png │ │ ├── Default-Landscape@2x~ipad.png │ │ ├── Default-Landscape~ipad.png │ │ ├── Default-Portrait@2x~ipad.png │ │ ├── Default-Portrait~ipad.png │ │ ├── Default@2x~iphone.png │ │ └── Default~iphone.png ├── splash.png └── splash.png.md5 ├── src ├── api │ ├── account.ts │ ├── currency-exchange-rate.ts │ ├── currency-exchange-service.ts │ ├── payment-request-handler.ts │ ├── payment-request.ts │ ├── payment-service.ts │ ├── transaction-filter.ts │ ├── transaction-service.ts │ └── transaction.ts ├── app │ ├── app.component.ts │ ├── app.html │ ├── app.module.ts │ ├── app.scss │ ├── main.prod.ts │ ├── main.ts │ └── scss │ │ ├── _buttons.scss │ │ ├── _content.scss │ │ ├── _logo.scss │ │ ├── _md.scss │ │ ├── _menu.scss │ │ ├── _numpad.scss │ │ ├── _payment.scss │ │ └── _utilities.scss ├── assets │ ├── fonts │ │ ├── Ubuntu-B.ttf │ │ ├── Ubuntu-C.ttf │ │ ├── Ubuntu-L.ttf │ │ ├── Ubuntu-M.ttf │ │ └── Ubuntu-R.ttf │ ├── i18n │ │ ├── de.json │ │ ├── en.json │ │ └── pl.json │ ├── icon │ │ └── favicon.ico │ ├── img │ │ ├── background.png │ │ ├── bitcoin-logo.png │ │ ├── bitpocket_appicon_1024x1024.png │ │ ├── bitpocket_appicon_white1024x1024.png │ │ ├── bitpocket_icon_135x135.svg │ │ ├── bitpocket_menu_header.jpg │ │ ├── noconnection.png │ │ ├── scanner.svg │ │ └── testnet-logo.png │ └── sound │ │ └── paid.mp3 ├── components │ ├── dynamic-font-size │ │ ├── dynamic-font-size.module.ts │ │ └── dynamic-font-size.ts │ └── logo │ │ ├── logo.module.ts │ │ └── logo.ts ├── declarations.d.ts ├── index.html ├── manifest.json ├── pages │ ├── account-creation │ │ ├── account-creation.html │ │ ├── account-creation.module.ts │ │ ├── account-creation.scss │ │ └── account-creation.ts │ ├── account-form │ │ ├── account-form.html │ │ ├── account-form.module.ts │ │ └── account-form.ts │ ├── account │ │ ├── account.html │ │ ├── account.module.ts │ │ └── account.ts │ ├── amount │ │ ├── amount.html │ │ ├── amount.module.ts │ │ └── amount.ts │ ├── export │ │ ├── export.html │ │ ├── export.module.ts │ │ └── export.ts │ ├── history │ │ ├── history.html │ │ ├── history.module.ts │ │ ├── history.scss │ │ └── history.ts │ ├── offline │ │ ├── offline.html │ │ ├── offline.module.ts │ │ ├── offline.scss │ │ └── offline.ts │ ├── payment-result │ │ ├── payment-result.html │ │ ├── payment-result.module.ts │ │ └── payment-result.ts │ ├── payment │ │ ├── payment.html │ │ ├── payment.module.ts │ │ └── payment.ts │ ├── pincode │ │ ├── pincode.html │ │ ├── pincode.module.ts │ │ ├── pincode.scss │ │ └── pincode.ts │ ├── qrscanner │ │ ├── qrscanner.html │ │ ├── qrscanner.module.ts │ │ ├── qrscanner.scss │ │ └── qrscanner.ts │ ├── settings │ │ ├── currency │ │ │ ├── currency.html │ │ │ ├── currency.module.ts │ │ │ ├── currency.scss │ │ │ └── currency.ts │ │ ├── general │ │ │ ├── general.html │ │ │ ├── general.module.ts │ │ │ └── general.ts │ │ ├── settings.html │ │ ├── settings.module.ts │ │ └── settings.ts │ └── transaction │ │ ├── transaction.html │ │ ├── transaction.module.ts │ │ ├── transaction.scss │ │ └── transaction.ts ├── pipes │ ├── bitpocket-currency │ │ ├── bitpocket-currency.module.ts │ │ └── bitpocket-currency.ts │ ├── bitpocket-fiat │ │ ├── bitpocket-fiat.module.ts │ │ └── bitpocket-fiat.ts │ └── bitpocket-unit │ │ ├── bitpocket-unit.module.ts │ │ └── bitpocket-unit.ts ├── providers │ ├── account │ │ ├── account-service.ts │ │ └── account-sync-service.ts │ ├── config.ts │ ├── currency │ │ ├── bitcoin-unit.spec.ts │ │ ├── bitcoin-unit.ts │ │ ├── bitcoinaverage-service.ts │ │ ├── cryptocurrency-service.spec.ts │ │ ├── cryptocurrency-service.ts │ │ ├── currency-service.spec.ts │ │ └── currency-service.ts │ ├── index.ts │ ├── payment │ │ ├── payment-request-handler │ │ │ └── insight-payment-request-handler.ts │ │ └── payment-service.ts │ ├── qrscanner │ │ └── qrscanner.ts │ ├── repository.ts │ └── transaction │ │ ├── blockchain-info-service │ │ ├── blockchain-info-data.ts │ │ ├── blockchain-info-service.spec.ts │ │ └── blockchain-info-service.ts │ │ ├── blockcypher-service │ │ ├── blockcypher-data.ts │ │ ├── blockcypher-service.spec.ts │ │ └── blockcypher-service.ts │ │ ├── insight-transaction-service │ │ ├── insight-transaction-data.ts │ │ ├── insight-transaction-service.spec.ts │ │ └── insight-transaction-service.ts │ │ ├── transaction-service-wrapper.ts │ │ └── transaction-storage-service.ts ├── service-worker.js └── theme │ ├── _font.scss │ └── variables.scss ├── tsconfig.json ├── tslint.json └── www └── index.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | 10 | # We recommend you to keep these unchanged 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | 15 | .idea/ 16 | .sass-cache/ 17 | .tmp/ 18 | .versions/ 19 | coverage/ 20 | dist/ 21 | node_modules/ 22 | tmp/ 23 | temp/ 24 | hooks/ 25 | platforms/ 26 | plugins/ 27 | plugins/android.json 28 | plugins/ios.json 29 | www/ 30 | $RECYCLE.BIN/ 31 | 32 | .DS_Store 33 | Thumbs.db 34 | UserInterfaceState.xcuserstate 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | dist: trusty 4 | node_js: 5 | - "8" 6 | 7 | install: 8 | - npm i ionic -g 9 | - npm i 10 | 11 | script: 12 | - npm test 13 | 14 | addons: 15 | apt: 16 | packages: 17 | - google-chrome-stable 18 | 19 | before_script: 20 | - ionic serve & 21 | - export DISPLAY=:99.0 22 | - sh -e /etc/init.d/xvfb start & 23 | - sleep 15 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, Stefan Huber 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/getbitpocket/bitpocket-mobile-app.svg?branch=master)](https://travis-ci.org/getbitpocket/bitpocket-mobile-app) [![Join the chat at https://gitter.im/getbitpocket/bitpocket-mobile-app](https://badges.gitter.im/getbitpocket/bitpocket-mobile-app.svg)](https://gitter.im/getbitpocket/bitpocket-mobile-app?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Dependency Status](https://david-dm.org/getbitpocket/bitpocket-mobile-app.svg)](https://david-dm.org/getbitpocket/bitpocket-mobile-app) 2 | 3 | 4 | # BitPocket 5 | 6 | Enabling Bitcoin Payments at the Point of Sale. 7 | 8 | ## Screenshots 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
BitPocket - Request PaymentBitPocket - Manage AccountsBitPocket - Transaction History
17 | 18 | ## Main features 19 | 20 | - Multiple accounts 21 | - BIP32 HD Keys 22 | - Testnet support 23 | - Support for different currencies 24 | - i18n support 25 | 26 | ## Development setup 27 | 28 | BitPocket is an App based on the Ionic/Cordova Frameworks. Therefore a couple of node modules are required to get started. In order to setup your environment the following global node modules need to be installed: `npm i cordova ionic -g` 29 | 30 | After successful installation of the required global modules, do a `npm i` inside the project folder to gather all local module dependencies. Additionally, some cordova plugins are required also. `ionic state reset --plugin` does the trick here. 31 | 32 | - `ionic serve` can be used to test the app locally in the browser, however not all networking features are available. 33 | - `ionic run (android|ios)` can be used to test the app on a connected device 34 | - `ionic emulate (android|ios)` can be used to test the app on a emulator/simulator 35 | 36 | ## Testing 37 | 38 | BitPocket is developed alongside unit and e2e testing. Test can be run: 39 | 40 | - `npm run test:unit` for unit tests 41 | - `npm run test:e2e` for e2e tests 42 | 43 | ## Translations 44 | 45 | - [English](https://github.com/getbitpocket/bitpocket-mobile-app/blob/master/src/assets/i18n/en.json) 46 | - [German](https://github.com/getbitpocket/bitpocket-mobile-app/blob/master/src/assets/i18n/de.json) 47 | - [Polish](https://github.com/getbitpocket/bitpocket-mobile-app/blob/master/src/assets/i18n/pl.json) -------------------------------------------------------------------------------- /config/karma-test-shim.js: -------------------------------------------------------------------------------- 1 | Error.stackTraceLimit = Infinity; 2 | 3 | require('core-js/es6'); 4 | require('core-js/es7/reflect'); 5 | 6 | require('zone.js/dist/zone'); 7 | require('zone.js/dist/long-stack-trace-zone'); 8 | require('zone.js/dist/proxy'); 9 | require('zone.js/dist/sync-test'); 10 | require('zone.js/dist/jasmine-patch'); 11 | require('zone.js/dist/async-test'); 12 | require('zone.js/dist/fake-async-test'); 13 | 14 | var appContext = require.context('../src', true, /\.spec\.ts/); 15 | 16 | appContext.keys().forEach(appContext); 17 | 18 | var testing = require('@angular/core/testing'); 19 | var browser = require('@angular/platform-browser-dynamic/testing'); 20 | 21 | testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting()); -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require('./webpack.test.js'); 2 | 3 | module.exports = function (config) { 4 | var _config = { 5 | basePath: '', 6 | 7 | frameworks: ['jasmine'], 8 | 9 | files: [ 10 | {pattern: './karma-test-shim.js', watched: true} 11 | ], 12 | 13 | preprocessors: { 14 | './karma-test-shim.js': ['webpack', 'sourcemap'] 15 | }, 16 | 17 | webpack: webpackConfig, 18 | 19 | webpackMiddleware: { 20 | stats: 'errors-only' 21 | }, 22 | 23 | webpackServer: { 24 | noInfo: true 25 | }, 26 | 27 | browserConsoleLogOptions: { 28 | level: 'log', 29 | format: '%b %T: %m', 30 | terminal: true 31 | }, 32 | 33 | reporters: ['kjhtml', 'dots'], 34 | port: 9876, 35 | colors: true, 36 | logLevel: config.LOG_INFO, 37 | autoWatch: true, 38 | browsers: ['Chrome'], 39 | singleRun: true 40 | }; 41 | 42 | config.set(_config); 43 | }; -------------------------------------------------------------------------------- /config/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | /*global jasmine */ 5 | var SpecReporter = require('jasmine-spec-reporter').SpecReporter; 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | '../e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:8100/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | useAllAngular2AppRoots: true, 24 | beforeLaunch: function() { 25 | require('ts-node').register({ 26 | project: 'e2e' 27 | }); 28 | }, 29 | onPrepare: function() { 30 | jasmine.getEnv().addReporter(new SpecReporter()); 31 | } 32 | }; -------------------------------------------------------------------------------- /config/uglifyjs.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 4 | 5 | /** 6 | * mangle: uglify 2's mangle option 7 | */ 8 | mangle: { 9 | reserved : ['BigInteger','ECPair','Point'] 10 | }, 11 | 12 | /** 13 | * compress: uglify 2's compress option 14 | */ 15 | compress: true, 16 | 17 | }; -------------------------------------------------------------------------------- /config/webpack.test.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | devtool: 'inline-source-map', 6 | 7 | resolve: { 8 | extensions: ['.ts', '.js'] 9 | }, 10 | 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.ts$/, 15 | loaders: [ 16 | { 17 | loader: 'ts-loader' 18 | } , 'angular2-template-loader' 19 | ] 20 | }, 21 | { 22 | test: /\.html$/, 23 | loader: 'html-loader' 24 | }, 25 | { 26 | test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/, 27 | loader: 'null-loader' 28 | } 29 | ] 30 | }, 31 | 32 | plugins: [ 33 | new webpack.ContextReplacementPlugin( 34 | // The (\\|\/) piece accounts for path separators in *nix and Windows 35 | /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/, 36 | root('./src'), // location of your src 37 | {} // a map of your routes 38 | ) 39 | ] 40 | }; 41 | 42 | function root(localPath) { 43 | return path.resolve(__dirname, localPath); 44 | } -------------------------------------------------------------------------------- /e2e/account-creation/account-creation.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { OnboardingPage } from './account-creation.po'; 2 | import { browser, element, by, promise } from 'protractor'; 3 | 4 | describe('Account Creation', function() { 5 | let page: OnboardingPage; 6 | 7 | beforeEach(() => { 8 | browser.restartSync(); 9 | page = new OnboardingPage(); 10 | }); 11 | 12 | describe('Create Account', () => { 13 | 14 | beforeEach(() => { 15 | browser.ignoreSynchronization = true; 16 | browser.get('/#/account-creation'); 17 | }); 18 | 19 | it('should create account and navigate to amount page', () => { 20 | page.setAccountCreationInput('n1RGwdbNTgNL868cRFCcimdCAQMft8HZKo') 21 | .then(() => { 22 | return page.submitAccountCreation(); 23 | }).then(() => { 24 | browser.sleep(4000); 25 | return page.isAmountPage(); 26 | }).then(amountPage => { 27 | expect(amountPage).toBeTruthy(); 28 | }); 29 | }); 30 | 31 | it('should create a dialog error message', () => { 32 | page.setAccountCreationInput('no-valid-input') 33 | .then(() => { 34 | return page.submitAccountCreation(); 35 | }).then(() => { 36 | return page.hasAlert(); 37 | }).then(() => { 38 | expect(true).toBeTruthy(); 39 | }); 40 | }); 41 | 42 | }); 43 | 44 | }); -------------------------------------------------------------------------------- /e2e/account-creation/account-creation.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | import { MainPage } from "./../main.po"; 3 | 4 | export class OnboardingPage extends MainPage { 5 | 6 | isAmountPage() { 7 | return browser.getCurrentUrl().then((url:string) => { 8 | return /\/#\/amount$/.test(url); 9 | }); 10 | } 11 | 12 | hasAlert() { 13 | browser.sleep(1000) 14 | .then(() => { 15 | return element(by.css('button[ion-button="alert-button"]')).click(); 16 | }); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /e2e/account/account.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AccountPage } from './account.po'; 2 | import { browser, element, by, promise } from 'protractor'; 3 | 4 | describe('Account', function() { 5 | let page: AccountPage; 6 | 7 | beforeEach(() => { 8 | browser.restartSync(); 9 | page = new AccountPage(); 10 | }); 11 | 12 | describe('Account Management', () => { 13 | 14 | beforeEach(() => { 15 | browser.ignoreSynchronization = true; 16 | page.createAccount('n1RGwdbNTgNL868cRFCcimdCAQMft8HZKo') 17 | .then(() => { 18 | browser.get('/#/account'); 19 | }); 20 | }); 21 | 22 | it('should create additional account', () => { 23 | page.navigateNewAccountPage() 24 | .then(() => { 25 | return page.setAccountData('testnet 2', 'mqdquKXQPfyn4Qzu2HHPgULTX5ay8g4wPg'); 26 | }).then(() => { 27 | return page.submitAccountData(); 28 | }).then(() => { 29 | return page.countAccounts(); 30 | }).then(count => { 31 | expect(count).toEqual(2); 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | }); -------------------------------------------------------------------------------- /e2e/account/account.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | import { MainPage } from './../main.po'; 3 | 4 | export class AccountPage extends MainPage { 5 | 6 | navigateNewAccountPage() { 7 | return browser.sleep(1000) 8 | .then(() => { 9 | return element(by.css('ion-fab button')).click() 10 | }).then(() => { 11 | return browser.sleep(500); 12 | }); 13 | } 14 | 15 | countAccounts() { 16 | return browser.sleep(1000) 17 | .then(() : any => { 18 | return element.all(by.css('.scroll-content ion-card')); 19 | }).then(items => { 20 | return items.length; 21 | }); 22 | } 23 | 24 | submitAccountData() { 25 | return element.all(by.css('ion-navbar .back-button')) 26 | .then(items => { 27 | return items[items.length - 1].click(); 28 | }); 29 | } 30 | 31 | setAccountData(name:string, account:string) { 32 | return element.all(by.css('ion-list ion-item input[type="text"]')) 33 | .then(items => { 34 | browser.sleep(100); 35 | 36 | items[0].sendKeys(name); 37 | items[1].sendKeys(account); 38 | 39 | return browser.sleep(300); 40 | }); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /e2e/amount/amount.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AmountPage } from './amount.po'; 2 | import { browser, element, by, promise } from 'protractor'; 3 | 4 | describe('Amount', function() { 5 | let page: AmountPage; 6 | 7 | beforeEach(() => { 8 | browser.restartSync(); 9 | page = new AmountPage(); 10 | }); 11 | 12 | describe('Amount', () => { 13 | 14 | beforeEach(() => { 15 | browser.ignoreSynchronization = true; 16 | page.createAccount('n1RGwdbNTgNL868cRFCcimdCAQMft8HZKo') 17 | .then(() => { 18 | browser.get('/#/amount'); 19 | }); 20 | }); 21 | 22 | it('should set amount correctly', () => { 23 | browser.sleep(1000); 24 | 25 | page.clickButton(0) 26 | .then(() => { 27 | return page.clickButton(1); 28 | }).then(() => { 29 | return page.clickButton(2); 30 | }).then(() => { 31 | return page.clickButton(9); 32 | }).then(() => { 33 | return page.clickButton(6); 34 | }).then(() => { 35 | return page.getActiveAmount(); 36 | }).then(amount => { 37 | expect(amount).toMatch(/123(,|\.)70/); 38 | }); 39 | }); 40 | 41 | }); 42 | 43 | }); -------------------------------------------------------------------------------- /e2e/amount/amount.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | import { MainPage } from './../main.po'; 3 | 4 | export class AmountPage extends MainPage { 5 | 6 | /** 7 | * 8 | * Click on of the 14 buttons 9 | * index: 0 - 13 10 | * @param buttonIndex 11 | */ 12 | clickButton(buttonIndex:number = 0) { 13 | return element.all(by.css('.numpad button')) 14 | .then(items => { 15 | if (items.length > buttonIndex) { 16 | return items[buttonIndex].click(); 17 | } 18 | }) 19 | } 20 | 21 | getActiveAmount() { 22 | return element(by.css('.payment-amount .active [dynamicfontsize]')).getText(); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /e2e/main.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class MainPage { 4 | 5 | navigateTo(destination) { 6 | return browser.get(destination); 7 | } 8 | 9 | getTitle() { 10 | return browser.getTitle(); 11 | } 12 | 13 | createAccount(input:string) { 14 | browser.ignoreSynchronization = true; 15 | 16 | return browser.sleep(2000) 17 | .then(() => { 18 | return browser.get('/#/account'); 19 | }).then(() => { 20 | return browser.sleep(2000); 21 | }).then(() => { 22 | return this.setAccountCreationInput(input); 23 | }).then(() => { 24 | return this.submitAccountCreation(); 25 | }).then(() => { 26 | return browser.sleep(5000); 27 | }); 28 | 29 | } 30 | 31 | setAccountCreationInput(input:string) { 32 | let el = element(by.css('#account-creation-input input[type="text"]')); 33 | browser.sleep(500); 34 | return el.sendKeys(input); 35 | } 36 | 37 | submitAccountCreation() { 38 | return element(by.css('button#account-creation-button')).click(); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "../dist/out-tsc-e2e", 10 | "sourceMap": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "../node_modules/@types" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitpocket-mobile-app", 3 | "app_id": "3cd21cf2", 4 | "type": "ionic-angular", 5 | "integrations": { 6 | "cordova": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitpocket-mobile-app", 3 | "author": "Stefan Huber", 4 | "version": "1.1.8", 5 | "license": "BSD-2-Clause", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/getbitpocket/bitpocket-mobile-app.git" 9 | }, 10 | "bugs": "https://github.com/getbitpocket/bitpocket-mobile-app/issues", 11 | "description": "Bitpocket mobile app", 12 | "homepage": "http://www.bitpocket.at/", 13 | "scripts": { 14 | "ionic:build": "ionic-app-scripts build", 15 | "ionic:serve": "ionic-app-scripts serve", 16 | "test": "npm run test:unit && npm run test:e2e", 17 | "test:e2e": "webdriver-manager update --standalone false --gecko false; protractor ./config/protractor.conf.js", 18 | "test:unit": "karma start ./config/karma.conf.js" 19 | }, 20 | "config": { 21 | "ionic_uglifyjs": "./config/uglifyjs.conf.js" 22 | }, 23 | "dependencies": { 24 | "@angular/common": "^5.0.2", 25 | "@angular/compiler": "^5.0.2", 26 | "@angular/compiler-cli": "^5.0.2", 27 | "@angular/core": "^5.0.2", 28 | "@angular/forms": "^5.0.2", 29 | "@angular/http": "^5.0.2", 30 | "@angular/platform-browser": "^5.0.2", 31 | "@angular/platform-browser-dynamic": "^5.0.2", 32 | "@ionic-native/core": "^4.4.2", 33 | "@ionic-native/file": "^4.4.2", 34 | "@ionic-native/file-opener": "^4.4.2", 35 | "@ionic-native/network": "^4.4.2", 36 | "@ionic-native/splash-screen": "^4.4.2", 37 | "@ionic-native/status-bar": "^4.4.2", 38 | "@ionic/storage": "^2.1.3", 39 | "@ngx-translate/core": "^8.0.0", 40 | "@ngx-translate/http-loader": "^2.0.0", 41 | "bip21": "^2.0.1", 42 | "bitcoinjs-lib": "^3.3.0", 43 | "cordova-android": "6.4.0", 44 | "cordova-plugin-file": "^5.0.0", 45 | "cordova-plugin-file-opener2": "^2.0.19", 46 | "cordova-plugin-inappbrowser": "^1.7.2", 47 | "cordova-plugin-ionic-webview": "^1.1.16", 48 | "cordova-plugin-network-information": "^1.3.4", 49 | "cordova-plugin-qrscanner": "~2.5.0", 50 | "cordova-plugin-splashscreen": "~4.1.0", 51 | "cordova-plugin-whitelist": "^1.3.3", 52 | "crypto-regex": "^0.3.0", 53 | "ionic-angular": "^3.9.2", 54 | "ionic-plugin-keyboard": "^2.2.1", 55 | "ionicons": "3.0.0", 56 | "pouchdb": "^6.3.4", 57 | "pouchdb-find": "^6.3.4", 58 | "pouchdb-upsert": "^2.1.0", 59 | "qrcode-generator": "^1.3.1", 60 | "rxjs": "^5.5.2", 61 | "socket.io": "^2.0.4", 62 | "sw-toolbox": "^3.6.0", 63 | "zone.js": "^0.8.18" 64 | }, 65 | "devDependencies": { 66 | "@ionic/app-scripts": "3.1.0", 67 | "@types/bitcoinjs-lib": "^3.0.5", 68 | "@types/cordova-plugin-qrscanner": "^1.0.31", 69 | "@types/jasmine": "^2.8.2", 70 | "@types/node": "^8.0.53", 71 | "@types/socket.io": "^1.4.31", 72 | "android-versions": "^1.2.1", 73 | "angular2-template-loader": "^0.6.2", 74 | "html-loader": "^0.5.1", 75 | "jasmine": "^2.8.0", 76 | "jasmine-spec-reporter": "^4.2.1", 77 | "karma": "^1.7.1", 78 | "karma-chrome-launcher": "^2.2.0", 79 | "karma-jasmine": "^1.1.0", 80 | "karma-jasmine-html-reporter": "^0.2.2", 81 | "karma-sourcemap-loader": "^0.3.7", 82 | "karma-webpack": "^2.0.6", 83 | "null-loader": "^0.1.1", 84 | "protractor": "^5.2.0", 85 | "ts-loader": "^2.3.7", 86 | "ts-node": "^3.3.0", 87 | "typescript": "2.4.2" 88 | }, 89 | "cordova": { 90 | "plugins": { 91 | "ionic-plugin-keyboard": {}, 92 | "cordova-plugin-wkwebview-engine": {}, 93 | "cordova-plugin-network-information": {}, 94 | "cordova-plugin-whitelist": {}, 95 | "cordova-plugin-splashscreen": {}, 96 | "cordova-plugin-inappbrowser": {}, 97 | "cordova-plugin-qrscanner": {}, 98 | "cordova-plugin-ionic-webview": {}, 99 | "cordova-plugin-file": {}, 100 | "cordova-plugin-file-opener2": {} 101 | }, 102 | "platforms": [ 103 | "android" 104 | ] 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /res/drawable-port-hdpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/drawable-port-hdpi/screen.png -------------------------------------------------------------------------------- /res/drawable-port-ldpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/drawable-port-ldpi/screen.png -------------------------------------------------------------------------------- /res/drawable-port-mdpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/drawable-port-mdpi/screen.png -------------------------------------------------------------------------------- /res/drawable-port-xhdpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/drawable-port-xhdpi/screen.png -------------------------------------------------------------------------------- /res/drawable-port-xxhdpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/drawable-port-xxhdpi/screen.png -------------------------------------------------------------------------------- /res/drawable-port-xxxhdpi/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/drawable-port-xxxhdpi/screen.png -------------------------------------------------------------------------------- /res/mipmap-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/mipmap-hdpi/icon.png -------------------------------------------------------------------------------- /res/mipmap-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/mipmap-ldpi/icon.png -------------------------------------------------------------------------------- /res/mipmap-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/mipmap-mdpi/icon.png -------------------------------------------------------------------------------- /res/mipmap-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/mipmap-xhdpi/icon.png -------------------------------------------------------------------------------- /res/mipmap-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/mipmap-xxhdpi/icon.png -------------------------------------------------------------------------------- /res/mipmap-xxxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/res/mipmap-xxxhdpi/icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-hdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/icon/drawable-hdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-ldpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/icon/drawable-ldpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-mdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/icon/drawable-mdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/icon/drawable-xhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/icon/drawable-xxhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/icon/drawable-xxxhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-land-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-land-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-land-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-land-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-land-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-land-xxxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-port-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-port-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-port-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-port-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-port-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/android/splash/drawable-port-xxxhdpi-screen.png -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/icon.png -------------------------------------------------------------------------------- /resources/icon.png.md5: -------------------------------------------------------------------------------- 1 | 172454b609fcc58c82c7e908d1276ad4 -------------------------------------------------------------------------------- /resources/ios/icon/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-40.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-40@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-50.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-50@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-60.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-60@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-60@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-72.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-72@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-76.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-76@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-small.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-small@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon-small@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon.png -------------------------------------------------------------------------------- /resources/ios/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/icon/icon@2x.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-568h@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/splash/Default-568h@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-667h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/splash/Default-667h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/splash/Default-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/splash/Default-Landscape-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/splash/Default-Landscape@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/splash/Default-Landscape~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/splash/Default-Portrait@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/splash/Default-Portrait~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/splash/Default@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/ios/splash/Default~iphone.png -------------------------------------------------------------------------------- /resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/resources/splash.png -------------------------------------------------------------------------------- /resources/splash.png.md5: -------------------------------------------------------------------------------- 1 | 793b26f03f71ba46b3ba5dc0dba80162 -------------------------------------------------------------------------------- /src/api/account.ts: -------------------------------------------------------------------------------- 1 | export interface Account { 2 | _id?:string; 3 | name?:string; 4 | type:string; 5 | data:any; 6 | default?:boolean; 7 | index?:number; 8 | lastConfirmedIndex?:number; 9 | } -------------------------------------------------------------------------------- /src/api/currency-exchange-rate.ts: -------------------------------------------------------------------------------- 1 | export interface CurrencyExchangeRate { 2 | code : string; 3 | rate? : number; 4 | } -------------------------------------------------------------------------------- /src/api/currency-exchange-service.ts: -------------------------------------------------------------------------------- 1 | import {CurrencyExchangeRate} from './currency-exchange-rate'; 2 | 3 | export interface CurrencyExchangeService { 4 | getAvailableCurrencies() : Promise>; 5 | getExchangeRates() : Promise>; 6 | getExchangeRate(code:string) : Promise; 7 | } -------------------------------------------------------------------------------- /src/api/payment-request-handler.ts: -------------------------------------------------------------------------------- 1 | export interface PaymentRequestHandler { 2 | 3 | cancel() : void; 4 | 5 | on(event:string, handler: (data:any) => void) : PaymentRequestHandler; 6 | 7 | once(event:string, handler: (data:any) => void) : PaymentRequestHandler; 8 | 9 | } -------------------------------------------------------------------------------- /src/api/payment-request.ts: -------------------------------------------------------------------------------- 1 | export const PAYMENT_STATUS_RECEIVED = 'received'; 2 | export const PAYMENT_STATUS_OVERPAID = 'overpaid'; 3 | export const PAYMENT_STATUS_PARTIAL_PAID = 'partial'; 4 | export const PAYMENT_STATUS_SERVICE_ERROR = 'service_error'; 5 | 6 | export interface PaymentRequest { 7 | 8 | currency: string; 9 | address: string; 10 | amount: number; 11 | 12 | txAmount?: number; // partial / over payment 13 | txid?:string; 14 | status?: string; 15 | startTime?: number; 16 | 17 | referenceAmount?: number; 18 | referenceCurrency?: string; 19 | } -------------------------------------------------------------------------------- /src/api/payment-service.ts: -------------------------------------------------------------------------------- 1 | import { PaymentRequestHandler } from './payment-request-handler'; 2 | import { PaymentRequest } from './payment-request'; 3 | 4 | export interface PaymentService { 5 | 6 | createPaymentRequestHandler(paymentRequest: PaymentRequest) : PaymentRequestHandler; 7 | 8 | } -------------------------------------------------------------------------------- /src/api/transaction-filter.ts: -------------------------------------------------------------------------------- 1 | import { Account } from './account'; 2 | 3 | export interface TransactionFilter { 4 | txid?: string , 5 | addresses?: string[] , 6 | account?:Account , 7 | from? : number , 8 | to? : number , 9 | startTime? : number , 10 | endTime? : number 11 | } -------------------------------------------------------------------------------- /src/api/transaction-service.ts: -------------------------------------------------------------------------------- 1 | import { TransactionFilter } from './transaction-filter'; 2 | import { Transaction } from './transaction'; 3 | 4 | export interface TransactionService { 5 | findTransactions(filter:TransactionFilter) : Promise; 6 | } -------------------------------------------------------------------------------- /src/api/transaction.ts: -------------------------------------------------------------------------------- 1 | export interface Transaction { 2 | _id: string; // txid 3 | currency: string; 4 | address: string; 5 | amount: number; 6 | incomming: boolean; 7 | timestamp?: number; 8 | confirmations?: number; 9 | account?: string; 10 | 11 | paymentReferenceAmount?: number; 12 | paymentReferenceCurrency?: string; 13 | paymentStatus?: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import * as ionic from 'ionic-angular'; 2 | import { Component, ViewChild } from '@angular/core'; 3 | import { StatusBar } from '@ionic-native/status-bar'; 4 | import { SplashScreen } from '@ionic-native/splash-screen'; 5 | import { Network } from '@ionic-native/network'; 6 | 7 | // Providers 8 | import { Repository, Config, CurrencyService, AccountService } from '../providers'; 9 | import { TranslateService } from '@ngx-translate/core'; 10 | 11 | @Component({ 12 | templateUrl: 'app.html' 13 | }) 14 | export class BitPocketApp { 15 | @ViewChild(ionic.Nav) nav: ionic.Nav; 16 | @ViewChild(ionic.Menu) menu: ionic.Menu; 17 | 18 | menuItems:Array<{name:string,icon:string,page:any}> = []; 19 | 20 | constructor( 21 | protected platform: ionic.Platform, 22 | protected statusBar: StatusBar, 23 | protected splashScreen: SplashScreen, 24 | protected network: Network, 25 | protected app:ionic.App, 26 | protected config:Config, 27 | protected ionicConfig:ionic.Config, 28 | protected currency:CurrencyService, 29 | protected repository:Repository, 30 | protected accountService:AccountService, 31 | protected translate: TranslateService) { 32 | 33 | platform.ready().then(() => { 34 | this.statusBar.styleDefault(); 35 | this.initLanguage(); 36 | this.initApp(); 37 | 38 | // watch network for a disconnect 39 | this.network.onDisconnect().subscribe(() => { 40 | this.nav.setRoot('offline'); 41 | }); 42 | 43 | // watch network for a connection 44 | this.network.onConnect().subscribe(() => { 45 | this.initNavState(); 46 | }); 47 | }); 48 | } 49 | 50 | isOnline() { 51 | if (this.network.type != 'none') { 52 | return true; 53 | } else { 54 | return false; 55 | } 56 | } 57 | 58 | triggerUpdateTask() { 59 | if (this.isOnline()) { 60 | this.currency.updateCurrencyRate(); 61 | } 62 | 63 | setTimeout(() => { 64 | this.triggerUpdateTask(); 65 | },1000 * 60 * 10); 66 | } 67 | 68 | hideSplashscreen() { 69 | setTimeout(() => { 70 | this.splashScreen.hide(); 71 | }, 2000); 72 | } 73 | 74 | initNavState() { 75 | if (this.isOnline()) { 76 | this.accountService.getAccounts() 77 | .then((accounts) => { 78 | if (accounts.length > 0) { 79 | this.nav.setRoot('amount'); 80 | } else { 81 | this.nav.setRoot('account-creation'); 82 | } 83 | this.hideSplashscreen(); 84 | }).catch(() => { 85 | this.nav.setRoot('account-creation'); 86 | this.hideSplashscreen(); 87 | }); 88 | } else { 89 | this.nav.setRoot('offline'); 90 | this.hideSplashscreen(); 91 | } 92 | } 93 | 94 | initLanguage() { 95 | this.translate.setDefaultLang('en'); 96 | let langs = ['de','en','pl']; 97 | let langIndex = langs.indexOf(this.translate.getBrowserLang()); 98 | 99 | if (langIndex == -1) { 100 | this.translate.use('en'); 101 | } else { 102 | this.translate.use(langs[langIndex]); 103 | } 104 | 105 | let languageIdentifiers = ['MENU.PAYMENT', 'MENU.ACCOUNTS', 'MENU.SETTINGS', 'BUTTON.BACK']; 106 | this.translate.get(languageIdentifiers) 107 | .subscribe((res:Array) => { 108 | this.menuItems[0] = { name:res[languageIdentifiers[0]], icon:'keypad' , page:'amount' }; 109 | this.menuItems[1] = { name:res[languageIdentifiers[1]], icon:'list', page:'account' }; 110 | this.menuItems[2] = { name:res[languageIdentifiers[2]], icon:'options', page:'settings' }; 111 | this.ionicConfig.set('ios', 'backButtonText', res[languageIdentifiers[3]]); 112 | }); 113 | } 114 | 115 | initApp() { 116 | Promise.all([ 117 | this.repository.init() , 118 | this.config.initConfig() 119 | ]).then(() => { 120 | this.triggerUpdateTask(); 121 | this.initNavState(); 122 | }); 123 | } 124 | 125 | openPage(page:any) { 126 | this.menu.close(); 127 | this.nav.setRoot(page); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ErrorHandler } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpClientModule, HttpClient } from '@angular/common/http'; 4 | import { IonicStorageModule, Storage } from '@ionic/storage'; 5 | import { IonicApp, IonicModule, IonicErrorHandler, ModalController } from 'ionic-angular'; 6 | 7 | // Pouch DB 8 | import PouchDB from 'pouchdb'; 9 | import pouchdbUpsert from 'pouchdb-upsert'; 10 | import pouchFind from 'pouchdb-find'; 11 | PouchDB.plugin(pouchdbUpsert); 12 | PouchDB.plugin(pouchFind); 13 | // PouchDB.debug.enable('pouchdb:find'); 14 | window['PouchDB'] = PouchDB; 15 | 16 | // Main Component 17 | import { BitPocketApp } from './app.component'; 18 | 19 | // Providers 20 | import { 21 | Config, 22 | Repository, 23 | QRScanner, 24 | CurrencyService, 25 | AccountService, 26 | TransactionServiceWrapper, 27 | CryptocurrencyService, 28 | PaymentService, 29 | TransactionStorageService, 30 | AccountSyncService, 31 | provideCurrencyService, 32 | provideAccountService, 33 | provideAccountSyncService, 34 | provideTransactionService, 35 | provideTransactionStorageService, 36 | providePaymentService, 37 | provideCryptocurrencyService, 38 | provideRepository, 39 | provideConfig, 40 | provideQRScanner, 41 | provideBitcoinAverageExchangeService 42 | } from '../providers/index'; 43 | 44 | // Ionic Native 45 | import { StatusBar } from '@ionic-native/status-bar'; 46 | import { Network } from '@ionic-native/network'; 47 | import { SplashScreen } from '@ionic-native/splash-screen'; 48 | import { File } from '@ionic-native/file'; 49 | import { FileOpener } from '@ionic-native/file-opener'; 50 | 51 | // Translations 52 | import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; 53 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 54 | 55 | // Exchange Services 56 | import { BitcoinAverageExchangeService } from '../providers/currency/bitcoinaverage-service'; 57 | 58 | // Pipes 59 | import { BitpocketCurrencyPipe } from '../pipes/currency'; 60 | import { BitpocketUnitPipe } from './../pipes/unit'; 61 | import { BitpocketFiatPipe } from './../pipes/fiat'; 62 | 63 | export function createTranslateLoader(http: HttpClient) { 64 | return new TranslateHttpLoader(http, './assets/i18n/', '.json'); 65 | } 66 | 67 | @NgModule({ 68 | declarations: [ 69 | BitPocketApp 70 | ], 71 | imports: [ 72 | BrowserModule, 73 | HttpClientModule, 74 | IonicModule.forRoot(BitPocketApp) , 75 | IonicStorageModule.forRoot(), 76 | TranslateModule.forRoot({ 77 | loader : { 78 | provide: TranslateLoader, 79 | useFactory: (createTranslateLoader), 80 | deps: [HttpClient] 81 | } 82 | }) 83 | ], 84 | bootstrap: [IonicApp], 85 | entryComponents: [ 86 | BitPocketApp 87 | ], 88 | providers: [ 89 | { provide: ErrorHandler, useClass: IonicErrorHandler } , 90 | { provide: CurrencyService, useFactory: provideCurrencyService, deps: [Config, BitcoinAverageExchangeService] } , 91 | { provide: AccountService, useFactory: provideAccountService, deps: [CryptocurrencyService, Config, Repository] } , 92 | { provide: AccountSyncService, useFactory: provideAccountSyncService, deps: [TransactionServiceWrapper, TransactionStorageService, AccountService, CryptocurrencyService] } , 93 | { provide: TransactionServiceWrapper, useFactory: provideTransactionService, deps: [HttpClient, CryptocurrencyService] } , 94 | { provide: TransactionStorageService, useFactory: provideTransactionStorageService, deps: [Repository] } , 95 | { provide: PaymentService, useFactory: providePaymentService, deps: [TransactionServiceWrapper, CryptocurrencyService] } , 96 | { provide: CryptocurrencyService, useFactory: provideCryptocurrencyService } , 97 | { provide: Repository, useFactory:provideRepository } , 98 | { provide:Config, useFactory:provideConfig, deps:[Storage] } , 99 | { provide:QRScanner, useFactory:provideQRScanner, deps:[ModalController] }, 100 | { provide:BitcoinAverageExchangeService, useFactory:provideBitcoinAverageExchangeService, deps:[HttpClient, Config] }, 101 | SplashScreen, 102 | Network, 103 | StatusBar, 104 | File, 105 | FileOpener 106 | ] 107 | }) 108 | export class AppModule {} 109 | -------------------------------------------------------------------------------- /src/app/app.scss: -------------------------------------------------------------------------------- 1 | @import "scss/buttons"; 2 | @import "scss/content"; 3 | @import "scss/logo"; 4 | @import "scss/md"; 5 | @import "scss/menu"; 6 | @import "scss/payment"; 7 | @import "scss/utilities"; 8 | @import "scss/numpad"; 9 | 10 | .toolbar-title-md { 11 | text-align:left !important; 12 | } 13 | 14 | .toolbar-title-ios { 15 | padding-top:12px; 16 | } 17 | 18 | ion-app { 19 | &.md, 20 | &.ios { 21 | background-color: transparent !important; 22 | .ion-page.show-page ~ .nav-decor { 23 | background-color: transparent !important; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/main.prod.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowser } from '@angular/platform-browser'; 2 | import { enableProdMode } from '@angular/core'; 3 | 4 | import { AppModuleNgFactory } from './app.module.ngfactory'; 5 | 6 | enableProdMode(); 7 | platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); 8 | -------------------------------------------------------------------------------- /src/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app.module'; 4 | 5 | platformBrowserDynamic().bootstrapModule(AppModule); 6 | -------------------------------------------------------------------------------- /src/app/scss/_buttons.scss: -------------------------------------------------------------------------------- 1 | .button-qr-scanner { 2 | &.button-md { 3 | margin:0 !important; 4 | padding: 20px 9px !important; 5 | } 6 | &.button-ios { 7 | margin:0 !important; 8 | padding: 20px 9px !important; 9 | } 10 | } 11 | 12 | .button-large { 13 | width:100%; 14 | line-height: $font-size-x-large; 15 | height: $font-size-x-large; 16 | border-radius: $button-border-radius; 17 | } 18 | 19 | .button-bottom { 20 | width:80%; 21 | position: absolute; 22 | bottom: 8vh; 23 | left: 50%; 24 | transform: translateX(-50%); 25 | text-align:center; 26 | 27 | .fab { 28 | display:inline-block; 29 | margin:0 10px; 30 | } 31 | 32 | } 33 | 34 | button { 35 | ion-spinner { 36 | margin-right:10px; 37 | width:20px; 38 | height:20px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/scss/_content.scss: -------------------------------------------------------------------------------- 1 | ion-nav ion-content, ion-modal ion-content { 2 | background: url(../assets/img/background.png) no-repeat center center; 3 | background-size: cover; 4 | } 5 | 6 | [no-scroll] { 7 | .scroll-content { 8 | overflow: hidden; 9 | } 10 | ::-webkit-scrollbar, 11 | *::-webkit-scrollbar { 12 | display: none; 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/scss/_logo.scss: -------------------------------------------------------------------------------- 1 | bitpocket-logo { 2 | display:block; 3 | position:absolute; 4 | bottom:10px; 5 | right:0px; 6 | 7 | img { 8 | max-width:2rem; 9 | max-height:2rem; 10 | 11 | &.ios { 12 | margin-right:16px; 13 | } 14 | 15 | &.md { 16 | margin-bottom:6px; 17 | margin-right:18px; 18 | } 19 | } 20 | 21 | &[loading] > img { 22 | -webkit-animation:spin 2s linear infinite; 23 | -moz-animation:spin 2s linear infinite; 24 | animation:spin 2s linear infinite; 25 | @-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } 26 | @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } 27 | @keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/scss/_md.scss: -------------------------------------------------------------------------------- 1 | .list-header-md { 2 | padding-top:13px; 3 | background:none; 4 | border:none; 5 | } -------------------------------------------------------------------------------- /src/app/scss/_menu.scss: -------------------------------------------------------------------------------- 1 | .menu-header { 2 | background-color: #434954; 3 | padding: 16px; 4 | width: 100%; 5 | min-height: 150px; 6 | text-align: center; 7 | } 8 | 9 | .menu-header img { 10 | max-width: 200px; 11 | min-height: 115px; 12 | margin-left: -15px; 13 | padding: 25px 0 20px 0; 14 | } 15 | 16 | .bar-button-close ion-icon { 17 | padding: 0 14px; 18 | font-size: 2.4rem; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/scss/_numpad.scss: -------------------------------------------------------------------------------- 1 | 2 | .numpad { 3 | display: flex; 4 | flex-flow: column; 5 | justify-content: center; 6 | 7 | .button-number-backspaceinput { 8 | font-size: $font-size-x-small; 9 | } 10 | } 11 | 12 | .numpad-row { 13 | display:flex; 14 | flex-flow: row nowrap; 15 | justify-content: center; 16 | align-content: center; 17 | align-items: center; 18 | } 19 | 20 | .numpad-button-row { 21 | display:flex; 22 | justify-content: space-between; 23 | margin-top:20px; 24 | flex: 0 1 232px; 25 | @media(min-width: $bitpocket-xs-min) { 26 | flex: 0 1 244px; 27 | } 28 | @media(min-width: $bitpocket-sm-min) { 29 | flex: 0 1 280px; 30 | } 31 | @media(min-width: $bitpocket-md-min) { 32 | flex: 0 1 300px; 33 | } 34 | } 35 | 36 | .numpad-primary-button { 37 | font-size: $font-size-base; 38 | border-radius: $button-border-radius; 39 | flex: 1 1 45%; 40 | } 41 | 42 | .numpad-primary-button + .numpad-primary-button { 43 | margin-left:15px; 44 | } 45 | 46 | .numpad-button { 47 | flex: 0 1 60px; 48 | height: 60px; 49 | margin: 5px 13px; 50 | padding:0; 51 | font-size: $font-size-x-medium; 52 | border-radius: 100%; 53 | 54 | @media(min-width: $bitpocket-xs-min) { 55 | flex: 0 1 64px; 56 | height: 64px; 57 | } 58 | @media(min-width: $bitpocket-sm-min) { 59 | flex: 0 1 70px; 60 | height: 70px; 61 | margin:10px 15px; 62 | } 63 | @media(min-width: $bitpocket-md-min) { 64 | flex: 0 1 80px; 65 | height: 80px; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/scss/_payment.scss: -------------------------------------------------------------------------------- 1 | .payment-specification { 2 | display:flex; 3 | flex-flow:row wrap; 4 | color: white; 5 | margin: 2vh 8vw 0 8vw; 6 | 7 | .payment-amount { 8 | line-height: 0; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | flex-basis:65%; 13 | text-align: right; 14 | font-size: $font-size-x-small; 15 | overflow: hidden; 16 | white-space: nowrap; 17 | 18 | .active { 19 | font-size: $font-size-x-large; 20 | } 21 | } 22 | 23 | .payment-currency { 24 | flex-basis:35%; 25 | font-size: $font-size-x-small; 26 | overflow: hidden; 27 | min-height: 50px; 28 | 29 | .checkbox-disabled { 30 | opacity: 1 !important; 31 | } 32 | 33 | &.vertical-align { 34 | line-height:50px; 35 | } 36 | 37 | ion-label { 38 | font-size: $font-size-small; 39 | margin-left:3px; 40 | } 41 | 42 | ion-checkbox, ion-label { 43 | display: inline-block; 44 | vertical-align: middle; 45 | } 46 | } 47 | } 48 | 49 | .payment-qr { 50 | padding:0 6%; 51 | text-align: center; 52 | height: 35%; 53 | 54 | div { 55 | display:inline-block; 56 | background:white; 57 | img { 58 | margin:20px; 59 | } 60 | } 61 | } 62 | 63 | .payment-info { 64 | color: white; 65 | margin-top:95px; 66 | text-align: center; 67 | font-size: 1.2em; 68 | 69 | p { 70 | font-size:12px; 71 | } 72 | 73 | .payment-text { 74 | margin:0; 75 | } 76 | } 77 | 78 | .payment-result-box { 79 | color:white; 80 | height: 70%; 81 | margin:0 10%; 82 | text-align: center; 83 | 84 | .payment-result-icon { 85 | padding: 20% 10% 5% 10%; 86 | font-size: 8.0rem; 87 | } 88 | 89 | .payment-result-text { 90 | font-size: $font-size-small; 91 | } 92 | 93 | } 94 | 95 | .payment-result-history { 96 | position: absolute; 97 | bottom: 5%; 98 | left: 50%; 99 | transform: translateX(-50%); 100 | .button-show-history { 101 | border-radius: $button-border-radius; 102 | } 103 | } 104 | 105 | .transaction-failed .payment-result-history { 106 | display:none; 107 | } 108 | -------------------------------------------------------------------------------- /src/app/scss/_utilities.scss: -------------------------------------------------------------------------------- 1 | .divider-box { 2 | margin: 1.6vh 0 4vh 0; 3 | position: relative; 4 | .divider { 5 | position: relative; 6 | top: 50%; 7 | transform: translateY(-50%); 8 | border: 0; 9 | height: 2px; 10 | background-image: linear-gradient(to right, rgba(255,255,255, 0), rgba(255,255,255, 0.75), rgba(255,255,255, 0)); 11 | &.black { 12 | background-image: linear-gradient(to right, rgba(0,0,0, 0), rgba(0,0,0, 0.75), rgba(0,0,0, 0)) 13 | } 14 | } 15 | } 16 | 17 | .invisible { 18 | visibility: hidden; 19 | } 20 | 21 | [dynamicFontSize] { 22 | display:inline-block; 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/assets/fonts/Ubuntu-B.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/fonts/Ubuntu-B.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Ubuntu-C.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/fonts/Ubuntu-C.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Ubuntu-L.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/fonts/Ubuntu-L.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Ubuntu-M.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/fonts/Ubuntu-M.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Ubuntu-R.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/fonts/Ubuntu-R.ttf -------------------------------------------------------------------------------- /src/assets/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/icon/favicon.ico -------------------------------------------------------------------------------- /src/assets/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/img/background.png -------------------------------------------------------------------------------- /src/assets/img/bitcoin-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/img/bitcoin-logo.png -------------------------------------------------------------------------------- /src/assets/img/bitpocket_appicon_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/img/bitpocket_appicon_1024x1024.png -------------------------------------------------------------------------------- /src/assets/img/bitpocket_appicon_white1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/img/bitpocket_appicon_white1024x1024.png -------------------------------------------------------------------------------- /src/assets/img/bitpocket_menu_header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/img/bitpocket_menu_header.jpg -------------------------------------------------------------------------------- /src/assets/img/noconnection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/img/noconnection.png -------------------------------------------------------------------------------- /src/assets/img/scanner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 56 | 61 | 66 | 71 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/assets/img/testnet-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/img/testnet-logo.png -------------------------------------------------------------------------------- /src/assets/sound/paid.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/assets/sound/paid.mp3 -------------------------------------------------------------------------------- /src/components/dynamic-font-size/dynamic-font-size.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicModule } from 'ionic-angular'; 3 | import { DynamicFontSize } from './dynamic-font-size'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | DynamicFontSize, 8 | ], 9 | imports: [ 10 | IonicModule , 11 | 12 | ], 13 | exports: [ 14 | DynamicFontSize 15 | ] 16 | }) 17 | export class DynamicFontSizeModule {} -------------------------------------------------------------------------------- /src/components/dynamic-font-size/dynamic-font-size.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Input, Renderer } from '@angular/core'; 2 | 3 | @Directive({ selector: '[dynamicFontSize]' }) 4 | export class DynamicFontSize { 5 | 6 | @Input() maxFontSize:any; 7 | @Input() minFontSize:any; 8 | @Input() content:string = ""; 9 | 10 | constructor(private el: ElementRef, private renderer: Renderer) { 11 | this.el.nativeElement.style.fontSize = this.maxFontSize + "px"; 12 | } 13 | 14 | calculateFontSize() { 15 | let childWidth = this.el.nativeElement.clientWidth; 16 | let parentWidth = this.el.nativeElement.parentElement.clientWidth; 17 | let fontSize = parseInt(this.el.nativeElement.style.fontSize) || parseInt(this.maxFontSize); 18 | let calculatedSize = (fontSize * ((parentWidth/childWidth)-0.1)); 19 | 20 | if (calculatedSize < parseInt(this.maxFontSize) && calculatedSize > parseInt(this.minFontSize)) { 21 | this.el.nativeElement.style.fontSize = calculatedSize + "px"; 22 | childWidth = this.el.nativeElement.clientWidth + 20; 23 | 24 | if (childWidth >= parentWidth) { 25 | setTimeout(() => { 26 | this.calculateFontSize(); 27 | },0); 28 | } 29 | } 30 | } 31 | 32 | ngAfterViewInit() { 33 | setTimeout(() => { 34 | this.calculateFontSize(); 35 | },50); 36 | } 37 | 38 | ngOnChanges() { 39 | this.calculateFontSize(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/logo/logo.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicModule } from 'ionic-angular'; 3 | import { Logo } from './logo'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | Logo 8 | ], 9 | imports: [ 10 | IonicModule 11 | ], 12 | exports: [ 13 | Logo 14 | ] 15 | }) 16 | export class LogoModule {} -------------------------------------------------------------------------------- /src/components/logo/logo.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NavController, Config } from 'ionic-angular'; 3 | 4 | @Component({ 5 | selector: 'bitpocket-logo' , 6 | template: '' 7 | }) 8 | export class Logo { 9 | 10 | modeClass:string = ""; 11 | 12 | constructor( 13 | protected config:Config, 14 | protected navigation:NavController) { 15 | this.modeClass = config.get('mode', 'md'); 16 | } 17 | 18 | click(ev: UIEvent) { 19 | this.navigation.setRoot('amount'); 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Declaration files are how the Typescript compiler knows about the type information(or shape) of an object. 3 | They're what make intellisense work and make Typescript know all about your code. 4 | 5 | A wildcard module is declared below to allow third party libraries to be used in an app even if they don't 6 | provide their own type declarations. 7 | 8 | To learn more about using third party libraries in an Ionic app, check out the docs here: 9 | http://ionicframework.com/docs/v2/resources/third-party-libs/ 10 | 11 | For more info on type definition files, check out the Typescript docs here: 12 | https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html 13 | */ 14 | declare module '*'; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BitPocket 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ionic", 3 | "short_name": "Ionic", 4 | "start_url": "index.html", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "assets/imgs/logo.png", 8 | "sizes": "512x512", 9 | "type": "image/png" 10 | }], 11 | "background_color": "#4e8ef7", 12 | "theme_color": "#4e8ef7" 13 | } -------------------------------------------------------------------------------- /src/pages/account-creation/account-creation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 |

{{ 'TITLE.ONBOARDING' | translate }}

9 |
10 |
11 | 12 | 13 | 14 | 15 |

16 |
17 |
18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 |
38 | 39 |
-------------------------------------------------------------------------------- /src/pages/account-creation/account-creation.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from '../../components/logo/logo.module'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { AccountCreationPage } from './account-creation'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | AccountCreationPage, 10 | ], 11 | imports: [ 12 | IonicPageModule.forChild(AccountCreationPage), 13 | LogoModule , 14 | TranslateModule 15 | ], 16 | exports: [ 17 | AccountCreationPage 18 | ] 19 | }) 20 | export class AccountCreationPageModule {} -------------------------------------------------------------------------------- /src/pages/account-creation/account-creation.scss: -------------------------------------------------------------------------------- 1 | .logo { 2 | margin-top:10vh; 3 | text-align:center; 4 | img { 5 | display: inline-block; 6 | height: 5rem; 7 | } 8 | } 9 | 10 | .title { 11 | color: white; 12 | font-size: $font-size-x-medium; 13 | font-weight: 300; 14 | display: block; 15 | text-align:center; 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/account-creation/account-creation.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NavController, AlertController, MenuController, IonicPage } from 'ionic-angular'; 3 | import { TranslateService } from '@ngx-translate/core' 4 | import { Config, AccountService, QRScanner, AccountSyncService } from '../../providers/index'; 5 | import 'rxjs/add/operator/toPromise'; 6 | 7 | @IonicPage({ 8 | name : 'account-creation' 9 | }) 10 | @Component({ 11 | templateUrl : 'account-creation.html' 12 | }) 13 | export class AccountCreationPage { 14 | 15 | accountInput:string = ""; 16 | account:any = null; 17 | loading:boolean = false; 18 | 19 | constructor( 20 | private accountSyncService: AccountSyncService , 21 | private qrscanner:QRScanner, 22 | private config: Config, 23 | private accountService: AccountService, 24 | private nav:NavController, 25 | private alertController: AlertController, 26 | private menuController: MenuController, 27 | private translate: TranslateService) { 28 | } 29 | 30 | ionViewWillEnter() { 31 | this.loading = false; 32 | this.menuController.enable(false); 33 | } 34 | 35 | ionViewWillLeave() { 36 | this.menuController.enable(true); 37 | } 38 | 39 | parseAccount() { 40 | let account = this.accountService.parseAccountInput(this.accountInput); 41 | account.name = "Bitcoin"; 42 | return account; 43 | } 44 | 45 | triggerAlert() { 46 | Promise.all([ 47 | this.translate.get('TITLE.INVALID_INPUT').toPromise() , 48 | this.translate.get('TEXT.INVALID_ACCOUNT_INPUT').toPromise() , 49 | this.translate.get('BUTTON.OK').toPromise() 50 | ]).then((texts) => { 51 | let alert = this.alertController.create({ 52 | title: texts[0], 53 | subTitle: texts[1], 54 | buttons: [texts[2]] 55 | }); 56 | alert.present(); 57 | this.loading = false; 58 | }); 59 | } 60 | 61 | start() { 62 | try { 63 | this.loading = true; 64 | 65 | if (!this.account) { 66 | this.account = this.parseAccount(); 67 | } 68 | 69 | console.log(this.account); 70 | this.accountService.addAccount(this.account) 71 | .then(account => { 72 | this.account = account; 73 | return this.config.set(Config.CONFIG_KEY_DEFAULT_ACCOUNT, account._id); 74 | }).then(() => { 75 | return this.accountSyncService.syncAccount(this.account); 76 | }).then(() => { 77 | this.nav.setRoot('amount'); 78 | }).catch(e => { 79 | this.triggerAlert(); 80 | console.error(e); 81 | }); 82 | } catch (e) { 83 | this.account = ''; 84 | this.triggerAlert(); 85 | } 86 | } 87 | 88 | scan() { 89 | this.qrscanner.scan((text) => { 90 | this.accountInput = text; 91 | let account = this.parseAccount(); 92 | 93 | if (account) { 94 | this.account = account; 95 | return true; 96 | } else { 97 | return false; 98 | } 99 | }); 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /src/pages/account-form/account-form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ 'TITLE.ACCOUNTS' | translate }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{ 'TEXT.NAME' | translate }} 13 | 14 | 15 | 16 | 17 | {{ 'TEXT.' + account.type | translate }} 18 | 19 | 20 | 21 | 22 | 23 | 24 | {{ 'TEXT.DEFAULT_ACCOUNT' | translate }} 25 | 26 | 27 | 28 | 29 | 30 | 31 |

{{ 'TEXT.REMOVE_ACCOUNT' | translate }}

32 | 33 |
34 |
35 | 36 |
-------------------------------------------------------------------------------- /src/pages/account-form/account-form.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from '../../components/logo/logo.module'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { AccountFormPage } from './account-form'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | AccountFormPage, 10 | ], 11 | imports: [ 12 | IonicPageModule.forChild(AccountFormPage), 13 | LogoModule , 14 | TranslateModule 15 | ], 16 | exports: [ 17 | AccountFormPage 18 | ] 19 | }) 20 | export class AccountFormPageModule {} -------------------------------------------------------------------------------- /src/pages/account/account.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ 'TITLE.ACCOUNTS' | translate }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

{{ account.name }}

17 |

{{ 'TEXT.' + account.type | translate }}

18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 |
-------------------------------------------------------------------------------- /src/pages/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from '../../components/logo/logo.module'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { AccountPage } from './account'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | AccountPage, 10 | ], 11 | imports: [ 12 | IonicPageModule.forChild(AccountPage), 13 | LogoModule , 14 | TranslateModule 15 | ], 16 | exports: [ 17 | AccountPage 18 | ] 19 | }) 20 | export class AccountPageModule {} -------------------------------------------------------------------------------- /src/pages/account/account.ts: -------------------------------------------------------------------------------- 1 | import { NavController, IonicPage } from 'ionic-angular'; 2 | import { Component } from '@angular/core'; 3 | import { Config, AccountService } from './../../providers/index'; 4 | 5 | @IonicPage({ 6 | name : 'account' , 7 | defaultHistory: ['amount'] 8 | }) 9 | @Component({ 10 | templateUrl : 'account.html' 11 | }) 12 | export class AccountPage { 13 | 14 | accounts:any[]; 15 | defaultAccount:string = ""; 16 | 17 | constructor( 18 | protected config:Config, 19 | protected navController:NavController, 20 | protected accountService:AccountService) {} 21 | 22 | openAccountEditForm(i:number) { 23 | this.navController.push('account-form', { id : this.accounts[i]._id }); 24 | } 25 | 26 | openAccountCreateForm() { 27 | this.navController.push('account-form'); 28 | } 29 | 30 | openAccountHistory(i:number) { 31 | this.navController.push('history', { accountId : this.accounts[i]._id }); 32 | } 33 | 34 | ionViewWillEnter() { 35 | this.accounts = []; 36 | 37 | Promise.all([ 38 | this.accountService.getAccounts() , 39 | this.config.get(Config.CONFIG_KEY_DEFAULT_ACCOUNT) 40 | ]).then(promised => { 41 | for (let account of promised[0]) { 42 | if (/bitcoin/.test(account.type)) { 43 | account['icon'] = 'bitcoin'; 44 | } 45 | if (/testnet/.test(account.type)) { 46 | account['icon'] = 'testnet'; 47 | } 48 | this.defaultAccount = promised[1]; 49 | this.accounts.push(account); 50 | } 51 | }); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/pages/amount/amount.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ 'TITLE.REQUEST_AMOUNT' | translate }} 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 | {{ currency }} 13 |
14 |
15 |
16 | 17 | {{ digits }}{{ separator }}{{ decimals }} 18 | 19 | {{ exchangedAmount }} 20 |
21 |
22 |
23 | 24 | {{ bitcoinUnit }} 25 |
26 |
27 |
28 | 29 | {{ digits }}{{ separator }}{{ decimals }} 30 | 31 | {{ exchangedAmount }} 32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 | 40 |
41 |
42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 |
56 |
57 | 58 | 59 | 60 |
61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 |
69 | 70 |
-------------------------------------------------------------------------------- /src/pages/amount/amount.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from '../../components/logo/logo.module'; 4 | import { DynamicFontSizeModule } from '../../components/dynamic-font-size/dynamic-font-size.module'; 5 | import { AmountPage } from './amount'; 6 | import { TranslateModule } from '@ngx-translate/core'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | AmountPage, 11 | ], 12 | imports: [ 13 | IonicPageModule.forChild(AmountPage), 14 | TranslateModule , 15 | DynamicFontSizeModule , 16 | LogoModule 17 | ], 18 | exports: [ 19 | AmountPage 20 | ] 21 | }) 22 | export class AmountPageModule {} -------------------------------------------------------------------------------- /src/pages/export/export.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ 'TITLE.EXPORT' | translate }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{ 'TEXT.START_DATE' | translate }} 14 | 15 | 16 | 17 | {{ 'TEXT.END_DATE' | translate }} 18 | 19 | 20 | 21 | 22 | {{ 'TEXT.ONLY_INCOMMING' | translate }} 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/pages/export/export.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { ExportPage } from './export'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | ExportPage, 9 | ], 10 | imports: [ 11 | IonicPageModule.forChild(ExportPage), 12 | TranslateModule 13 | ], 14 | }) 15 | export class ExportPageModule {} 16 | -------------------------------------------------------------------------------- /src/pages/history/history.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ 'TITLE.HISTORY' | translate }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

{{ currencySymbol }} {{ transaction.amount | bitpocketUnit:'BTC':currencySymbol | bitpocketCurrency:currencySeparator:currencyThousandsPoint:currencyPrecision }} ({{ referenceCurrencySymbol }} {{ transaction.amount | bitpocketFiat:'BTC':referenceCurrencyRate | bitpocketCurrency:currencySeparator:currencyThousandsPoint }})

15 |

{{ (transaction.timestamp * 1000) | date:dateTimeFormat }}

16 |
17 | 18 | 21 | 24 | 25 |
26 |
27 | 28 |
{{ 'TEXT.NO_TRANSACTIONS' | translate }}
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
-------------------------------------------------------------------------------- /src/pages/history/history.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from '../../components/logo/logo.module'; 4 | import { BitpocketCurrencyPipeModule } from '../../pipes/bitpocket-currency/bitpocket-currency.module'; 5 | import { BitpocketUnitPipeModule } from '../../pipes/bitpocket-unit/bitpocket-unit.module'; 6 | import { BitpocketFiatPipeModule } from '../../pipes/bitpocket-fiat/bitpocket-fiat.module'; 7 | import { TranslateModule } from '@ngx-translate/core'; 8 | import { HistoryPage } from './history'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | HistoryPage, 13 | ], 14 | imports: [ 15 | IonicPageModule.forChild(HistoryPage), 16 | LogoModule , 17 | TranslateModule , 18 | BitpocketCurrencyPipeModule , 19 | BitpocketUnitPipeModule , 20 | BitpocketFiatPipeModule 21 | ], 22 | exports: [ 23 | HistoryPage 24 | ] 25 | }) 26 | export class HistoryPageModule {} -------------------------------------------------------------------------------- /src/pages/history/history.scss: -------------------------------------------------------------------------------- 1 | .history { 2 | .no-transactions { 3 | color: white; 4 | margin-top:15px; 5 | text-align: center; 6 | font-size: 1.2em; 7 | } 8 | h2 span { 9 | color: lightgrey; 10 | } 11 | } 12 | 13 | .icon-unconfirmed { 14 | color: yellow; 15 | } 16 | .icon-confirmed { 17 | color: green; 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/offline/offline.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |

{{ 'TEXT.NO_INTERNET' | translate }}

8 | 9 | 12 |
13 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /src/pages/offline/offline.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from '../../components/logo/logo.module'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { OfflinePage } from './offline'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | OfflinePage, 10 | ], 11 | imports: [ 12 | IonicPageModule.forChild(OfflinePage), 13 | LogoModule , 14 | TranslateModule 15 | ], 16 | exports: [ 17 | OfflinePage 18 | ] 19 | }) 20 | export class OfflinePageModule {} -------------------------------------------------------------------------------- /src/pages/offline/offline.scss: -------------------------------------------------------------------------------- 1 | 2 | .no-internet { 3 | 4 | div { 5 | text-align:center; 6 | } 7 | 8 | img { 9 | display:inline-block; 10 | width:50vw; 11 | margin-top:10vh; 12 | } 13 | 14 | p { 15 | color: white; 16 | font-size: $font-size-small; 17 | text-transform: uppercase; 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/pages/offline/offline.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MenuController, IonicPage } from 'ionic-angular'; 3 | import { SplashScreen } from '@ionic-native/splash-screen'; 4 | 5 | declare var document:any; 6 | declare var navigator:any; 7 | 8 | @IonicPage({ 9 | name : 'offline' 10 | }) 11 | @Component({ 12 | templateUrl: 'offline.html', 13 | }) 14 | export class OfflinePage { 15 | 16 | constructor( 17 | protected splashscreen: SplashScreen, 18 | protected menuController:MenuController) { 19 | } 20 | 21 | ionViewWillEnter() { 22 | this.menuController.enable(false); 23 | } 24 | 25 | ionViewWillLeave() { 26 | this.menuController.enable(true); 27 | } 28 | 29 | retry() { 30 | this.splashscreen.show(); 31 | window.location.reload(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/payment-result/payment-result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ 'TITLE.PAYMENT_RESULT' | translate }} 4 | 5 | 6 | 7 | 8 | 9 |
10 |
{{ referenceCurrency }}
11 |
12 |
13 | {{ referenceAmount }} 14 |
15 |
16 |
{{ currency }}
17 |
18 |
19 | {{ amount }} 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
{{ resultText }}
33 |
34 | 35 |
36 | 37 |
38 |
-------------------------------------------------------------------------------- /src/pages/payment-result/payment-result.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from '../../components/logo/logo.module'; 4 | import { DynamicFontSizeModule } from '../../components/dynamic-font-size/dynamic-font-size.module'; 5 | import { PaymentResultPage } from './payment-result'; 6 | import { TranslateModule } from '@ngx-translate/core'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | PaymentResultPage 11 | ], 12 | imports: [ 13 | IonicPageModule.forChild(PaymentResultPage), 14 | TranslateModule , 15 | DynamicFontSizeModule , 16 | LogoModule 17 | ], 18 | exports: [ 19 | PaymentResultPage 20 | ] 21 | }) 22 | export class PaymentResultPageModule {} -------------------------------------------------------------------------------- /src/pages/payment-result/payment-result.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NavParams, NavController, IonicPage } from 'ionic-angular'; 3 | import { CryptocurrencyService, CurrencyService, Config, AccountService, BitcoinUnit, AccountSyncService } from '../../providers/index'; 4 | import { TranslateService } from '@ngx-translate/core'; 5 | import { PaymentRequest, PAYMENT_STATUS_RECEIVED, PAYMENT_STATUS_OVERPAID, PAYMENT_STATUS_PARTIAL_PAID } from './../../api/payment-request'; 6 | import { Account } from './../../api/account'; 7 | import 'rxjs/add/operator/toPromise'; 8 | 9 | @IonicPage({ 10 | name : 'payment-result' , 11 | defaultHistory : ['amount'] 12 | }) 13 | @Component({ 14 | templateUrl : 'payment-result.html' 15 | }) 16 | export class PaymentResultPage { 17 | 18 | resultSuccess : boolean = false; 19 | resultIcon : string = ""; 20 | resultText : string = ""; 21 | paymentRequest:PaymentRequest; 22 | account:Account = null; 23 | 24 | currency:string = ""; 25 | amount:string = ""; 26 | referenceCurrency = ""; 27 | referenceAmount = ""; 28 | 29 | waiting:boolean = true; 30 | 31 | getResultClasses() { 32 | if (this.resultSuccess) { 33 | return { "transaction-success" : true , "transaction-failed" : false }; 34 | } else { 35 | return { "transaction-success" : false , "transaction-failed" : true }; 36 | } 37 | } 38 | 39 | ionViewWillLeave() { 40 | this.waiting = false; 41 | } 42 | 43 | ionViewWillEnter() { 44 | this.paymentRequest = this.params.data.paymentRequest; 45 | 46 | if (this.paymentRequest.status == PAYMENT_STATUS_RECEIVED) { 47 | this.resultSuccess = true; 48 | this.resultIcon = "checkmark-circle"; 49 | } else if (this.paymentRequest.status == PAYMENT_STATUS_OVERPAID || 50 | this.paymentRequest.status == PAYMENT_STATUS_PARTIAL_PAID) { 51 | // change referenceAmount in accordance 52 | this.paymentRequest.referenceAmount = this.paymentRequest.referenceAmount * (this.paymentRequest.txAmount / this.paymentRequest.amount); 53 | this.resultSuccess = true; 54 | this.resultIcon = "alert"; 55 | } else { 56 | this.resultIcon = "close-circle"; 57 | } 58 | 59 | Promise.all([ 60 | this.config.get(Config.CONFIG_KEY_BITCOIN_UNIT) , 61 | this.translate.get('FORMAT.CURRENCY_S').toPromise() , 62 | this.translate.get('PAYMENT_STATUS.' + this.paymentRequest.status).toPromise() , 63 | this.accountService.getDefaultAccount() 64 | ]).then(promised => { 65 | this.account = promised[3]; 66 | this.currency = promised[0]; 67 | this.referenceCurrency = this.paymentRequest.referenceCurrency; 68 | this.amount = this.currencyService.formatNumber(BitcoinUnit.from(this.paymentRequest.txAmount,'BTC').to(this.currency), promised[1], BitcoinUnit.decimalsCount(this.currency)); 69 | this.referenceAmount = this.currencyService.formatNumber(this.paymentRequest.referenceAmount, promised[1]); 70 | this.resultText = promised[2]; 71 | 72 | if (this.resultSuccess) { 73 | this.accountSyncService.storeTransaction({ 74 | _id : this.paymentRequest.txid , 75 | amount : this.paymentRequest.txAmount , 76 | currency : this.cryptocurrencyService.parseInput(this.account.data).currency , 77 | address : this.paymentRequest.address , 78 | incomming : true , 79 | confirmations : 0 , 80 | paymentReferenceAmount : this.paymentRequest.referenceAmount , 81 | paymentReferenceCurrency : this.paymentRequest.referenceCurrency , 82 | paymentStatus : this.paymentRequest.status 83 | }, this.account._id).then(() => { 84 | // TODO: is a sync required at this moment? 85 | // index needs to be updated 86 | this.accountSyncService.syncAccount(this.account); 87 | }); 88 | } 89 | }); 90 | } 91 | 92 | showHistory() { 93 | this.nav.setRoot('history', { accountId : this.account._id }); 94 | } 95 | 96 | constructor( 97 | protected cryptocurrencyService: CryptocurrencyService , 98 | protected accountSyncService: AccountSyncService, 99 | protected accountService: AccountService, 100 | protected translate: TranslateService, 101 | protected currencyService: CurrencyService, 102 | protected config: Config, 103 | protected params: NavParams, 104 | protected nav: NavController) {} 105 | 106 | } -------------------------------------------------------------------------------- /src/pages/payment/payment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ 'TITLE.PAYMENT_REQUEST' | translate }} 4 | 5 | 6 | 7 | 8 | 9 |
10 |
{{ currency }}
11 |
12 |
13 | {{ fiatAmount }} 14 |
15 |
16 |
{{ bitcoinUnit }}
17 |
18 |
19 | {{ bitcoinAmount }} 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 |

{{ address }}

34 | {{ 'TEXT.WAITING_FOR_PAYMENT' | translate }} 35 |
36 | 37 |
-------------------------------------------------------------------------------- /src/pages/payment/payment.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from '../../components/logo/logo.module'; 4 | import { DynamicFontSizeModule } from '../../components/dynamic-font-size/dynamic-font-size.module'; 5 | import { PaymentPage } from './payment'; 6 | import { TranslateModule } from '@ngx-translate/core'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | PaymentPage, 11 | ], 12 | imports: [ 13 | IonicPageModule.forChild(PaymentPage), 14 | TranslateModule , 15 | DynamicFontSizeModule , 16 | LogoModule 17 | ], 18 | exports: [ 19 | PaymentPage 20 | ] 21 | }) 22 | export class PaymentPageModule {} -------------------------------------------------------------------------------- /src/pages/pincode/pincode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ title }} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | 40 |
41 |
42 | 43 |
-------------------------------------------------------------------------------- /src/pages/pincode/pincode.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from './../../components/logo/logo.module'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { PincodePage } from './pincode'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | PincodePage 10 | ], 11 | imports: [ 12 | IonicPageModule.forChild(PincodePage), 13 | LogoModule , 14 | TranslateModule 15 | ], 16 | exports: [ 17 | PincodePage 18 | ] 19 | }) 20 | export class PincodePageModule {} -------------------------------------------------------------------------------- /src/pages/pincode/pincode.scss: -------------------------------------------------------------------------------- 1 | .pin { 2 | padding:4vh 0 3vh 0; 3 | text-align:center; 4 | 5 | .pin-position { 6 | margin:3vw; 7 | display:inline-block; 8 | width:10vw; 9 | height:10vw; 10 | border:1px solid white; 11 | border-radius: 100%; 12 | } 13 | 14 | .filled { 15 | background: $basic-color-transparency; 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/pages/pincode/pincode.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ToastController, ViewController, NavParams, MenuController, IonicPage } from 'ionic-angular'; 3 | import * as bitcoin from 'bitcoinjs-lib'; 4 | import { TranslateService } from '@ngx-translate/core'; 5 | 6 | @IonicPage({ 7 | name : 'pincode' 8 | }) 9 | @Component({ 10 | templateUrl : 'pincode.html' 11 | }) 12 | export class PincodePage { 13 | 14 | pin:Array = ["","","",""]; 15 | position:number = 0; 16 | length:number = 4; 17 | title:string = ""; 18 | token:string = ""; 19 | closeable: boolean = false; 20 | confirm: boolean = false; 21 | texts:Array = []; 22 | textIdentifiers = ['TITLE.VERIFY_PIN','TITLE.ENTER_PIN','TITLE.CONFIRM_PIN','TEXT.PIN_MISMATCH']; 23 | 24 | constructor(private toastController: ToastController, private viewController: ViewController, private navParams: NavParams, private menuController: MenuController, private translate: TranslateService) { 25 | translate.get(this.textIdentifiers) 26 | .subscribe((res:Array) => { 27 | this.texts = res; 28 | this.title = this.texts[this.textIdentifiers[0]]; 29 | 30 | if (navParams.get('closable') === true) { 31 | this.closeable = true; 32 | } 33 | if (navParams.get('token')) { 34 | this.token = navParams.get('token'); 35 | } 36 | if (navParams.get('change') === true) { 37 | this.confirm = true; 38 | this.title = this.texts[this.textIdentifiers[1]]; 39 | } 40 | }); 41 | 42 | } 43 | 44 | ionViewWillEnter() { 45 | this.position = 0; 46 | this.menuController.enable(false); 47 | } 48 | 49 | ionViewWillLeave() { 50 | this.menuController.enable(true); 51 | } 52 | 53 | close(success:boolean = false) { 54 | this.viewController.dismiss({ 55 | success : success , 56 | pin : this.hashedPin() 57 | }); 58 | } 59 | 60 | numberInput(input:string) { 61 | if (this.position >= 0 && this.position < this.length) { 62 | this.pin[this.position] = input; 63 | this.position++; 64 | } 65 | 66 | let fullLength = this.position >= this.length; 67 | 68 | if (fullLength && this.confirm) { 69 | this.token = this.hashedPin(); 70 | this.title = this.texts[this.textIdentifiers[2]]; 71 | this.confirm = false; 72 | this.reset(); 73 | } else if (fullLength && this.verify(this.token)) { 74 | this.close(true); 75 | } else if (fullLength) { 76 | this.toastController.create({ 77 | message : this.texts[this.textIdentifiers[3]] , 78 | duration : 3000 79 | }).present(); 80 | this.reset(); 81 | } 82 | } 83 | 84 | backspaceInput() { 85 | if (this.position > 0) { 86 | this.pin[this.position-1] = ""; 87 | this.position--; 88 | } 89 | } 90 | 91 | reset() { 92 | this.position = 0; 93 | this.pin = ["","","",""]; 94 | } 95 | 96 | hashedPin() : string { 97 | return bitcoin.crypto.sha256(new Buffer(this.pin.join(''))).toString('hex'); 98 | } 99 | 100 | verify(token:string) : boolean { 101 | return this.hashedPin() === token; 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /src/pages/qrscanner/qrscanner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ 'TITLE.SCANNING' | translate }} 7 | 8 | 9 | 10 |
11 |
12 |
13 | 16 | 19 |
20 |
-------------------------------------------------------------------------------- /src/pages/qrscanner/qrscanner.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from '../../components/logo/logo.module'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { QRScannerPage } from './qrscanner'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | QRScannerPage 10 | ], 11 | imports: [ 12 | IonicPageModule.forChild(QRScannerPage), 13 | LogoModule , 14 | TranslateModule 15 | ], 16 | exports: [ 17 | QRScannerPage 18 | ] 19 | }) 20 | export class QRScannerPageModule {} -------------------------------------------------------------------------------- /src/pages/qrscanner/qrscanner.scss: -------------------------------------------------------------------------------- 1 | .qrscanner { 2 | background:none; 3 | 4 | &-area { 5 | width:100%; 6 | height:85%; 7 | background: url(../assets/img/scanner.svg) no-repeat center center; 8 | background-size: contain; 9 | } 10 | } -------------------------------------------------------------------------------- /src/pages/qrscanner/qrscanner.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ToastController, Toast, ViewController, NavParams, IonicPage } from 'ionic-angular'; 3 | 4 | @IonicPage({ 5 | name : 'qrscanner' 6 | }) 7 | @Component({ 8 | templateUrl : 'qrscanner.html' 9 | }) 10 | export class QRScannerPage { 11 | 12 | protected light: boolean = false; 13 | protected frontCamera: boolean = false; 14 | protected scanningError: Toast; 15 | protected validate:any; 16 | 17 | constructor(private toastController: ToastController, private viewController: ViewController, navParams: NavParams) { 18 | this.scanningError = toastController.create({ 19 | message : 'There was a scanning error, please try again!' , 20 | duration: 2000 21 | }); 22 | 23 | this.validate = navParams.get('validate'); 24 | } 25 | 26 | close(data:any = null) { 27 | this.viewController.dismiss(data); 28 | } 29 | 30 | toggleCamera() { 31 | this.frontCamera = !this.frontCamera; 32 | 33 | if (this.frontCamera) { 34 | window['QRScanner'].useFrontCamera(); 35 | } else { 36 | window['QRScanner'].useBackCamera(); 37 | } 38 | } 39 | 40 | toggleLight() { 41 | this.light = !this.light; 42 | 43 | if (this.light) { 44 | window['QRScanner'].enableLight(); 45 | } else { 46 | window['QRScanner'].disableLight(); 47 | } 48 | } 49 | 50 | setupScanner() { 51 | window['QRScanner'].scan((error, text) => { 52 | if (error || !this.validate(text)) { 53 | this.scanningError.present(); 54 | this.close(); 55 | } else { 56 | this.close({ 57 | 'text' : this.validate(text) 58 | }); 59 | } 60 | }); 61 | } 62 | 63 | ionViewWillEnter() { 64 | this.setupScanner(); 65 | window['QRScanner'].show(); 66 | window.document.querySelector('ion-app > .app-root').classList.add('hide'); 67 | } 68 | 69 | ionViewWillLeave() { 70 | window.document.querySelector('ion-app > .app-root').classList.remove('hide'); 71 | window['QRScanner'].destroy(); 72 | } 73 | 74 | ionViewCanEnter() { 75 | if (window['QRScanner']) { 76 | return true; 77 | } else { 78 | return false; 79 | } 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /src/pages/settings/currency/currency.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ 'TITLE.CURRENCY' | translate }} 4 | 5 | 6 | 7 | 8 | 9 | {{ 'LIST_HEADING.EXCHANGE_RATE_FEE' | translate }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {{ 'LIST_TEXT.EXCHANGE_RATE_FEE' | translate }} 18 | {{ (rateFee / 100) | bitpocketCurrency:currencySeparator }} % 19 | 20 | {{ 'LIST_HEADING.AVAILABLE_CURRENCIES' | translate }} 21 | 22 | {{ currency.code }} 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/pages/settings/currency/currency.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from './../../../components/logo/logo.module'; 4 | import { BitpocketCurrencyPipeModule } from '../../../pipes/bitpocket-currency/bitpocket-currency.module'; 5 | import { TranslateModule } from '@ngx-translate/core'; 6 | import { CurrencyPage } from './currency'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | CurrencyPage 11 | ], 12 | imports: [ 13 | IonicPageModule.forChild(CurrencyPage), 14 | LogoModule , 15 | BitpocketCurrencyPipeModule , 16 | TranslateModule 17 | ], 18 | exports: [ 19 | CurrencyPage 20 | ] 21 | }) 22 | export class CurrencyPageModule {} -------------------------------------------------------------------------------- /src/pages/settings/currency/currency.scss: -------------------------------------------------------------------------------- 1 | .text-small { 2 | font-size:12px; 3 | } -------------------------------------------------------------------------------- /src/pages/settings/currency/currency.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Loading, NavController, LoadingController, IonicPage } from 'ionic-angular'; 3 | import { CurrencyService } from '../../../providers/index'; 4 | import { TranslateService } from '@ngx-translate/core' 5 | import 'rxjs/add/operator/toPromise'; 6 | 7 | @IonicPage({ 8 | name : 'currency' , 9 | defaultHistory : ['settings'] 10 | }) 11 | @Component({ 12 | templateUrl : 'currency.html' 13 | }) 14 | export class CurrencyPage { 15 | 16 | currencies:Array<{code:string}>; 17 | selectedCurrency:string; 18 | availableCurrencies:Array; 19 | loader: Loading; 20 | rateFee: number = 0; 21 | currencySeparator: string = ""; 22 | 23 | startLoading() { 24 | this.translateService.get("TEXT.LOADING_CURRENCIES").toPromise() 25 | .then((text) => { 26 | this.loader = this.loadingController.create({ 27 | content : text 28 | }); 29 | this.loader.present(); 30 | }); 31 | } 32 | 33 | stopLoading() { 34 | this.loader.dismiss(); 35 | } 36 | 37 | constructor( 38 | protected translateService:TranslateService, 39 | protected currencyService:CurrencyService, 40 | protected nav:NavController, 41 | protected loadingController:LoadingController) { 42 | } 43 | 44 | increaseFee() { 45 | this.rateFee += 5; 46 | } 47 | 48 | decreaseFee() { 49 | this.rateFee -= 5; 50 | } 51 | 52 | ionViewWillEnter() { 53 | this.startLoading(); 54 | 55 | Promise.all([ 56 | this.currencyService.getSelectedCurrency() , 57 | this.currencyService.getAvailableCurrencies() , 58 | this.translateService.get('FORMAT.CURRENCY_S').toPromise() , 59 | this.currencyService.getFeePercentage() 60 | ]).then(selections => { 61 | this.selectedCurrency = selections[0]; 62 | this.availableCurrencies = selections[1]; 63 | this.currencySeparator = selections[2]; 64 | this.rateFee = selections[3] * 100; 65 | this.stopLoading(); 66 | }); 67 | } 68 | 69 | ionViewWillLeave() { 70 | this.currencyService.setFeePercentage(this.rateFee / 100); 71 | this.currencyService.setSelectedCurrency(this.selectedCurrency); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/pages/settings/general/general.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ 'TITLE.GENERAL' | translate }} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{ 'LIST_HEADING.MERCHANT_INFORMATION' | translate }} 12 | 13 | {{ 'LIST_TEXT.MERCHANT_NAME' | translate }} 14 | 15 | 16 | 17 | 18 | 19 | {{ 'LIST_HEADING.BITCOIN_UNIT' | translate }} 20 | 21 | {{ 'LIST_TEXT.BITCOIN' | translate }} 22 | 23 | 24 | 25 | {{ 'LIST_TEXT.MILLI_BITCOIN' | translate }} 26 | 27 | 28 | 29 | {{ 'LIST_TEXT.BITS' | translate }} 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/pages/settings/general/general.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from './../../../components/logo/logo.module'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { GeneralPage } from './general'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | GeneralPage 10 | ], 11 | imports: [ 12 | IonicPageModule.forChild(GeneralPage), 13 | LogoModule , 14 | TranslateModule 15 | ], 16 | exports: [ 17 | GeneralPage 18 | ] 19 | }) 20 | export class GeneralPageModule {} -------------------------------------------------------------------------------- /src/pages/settings/general/general.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicPage } from 'ionic-angular'; 3 | import { Config } from '../../../providers/index'; 4 | 5 | @IonicPage({ 6 | name : 'general' , 7 | defaultHistory : ['settings'] 8 | }) 9 | @Component({ 10 | templateUrl : 'general.html' 11 | }) 12 | export class GeneralPage { 13 | 14 | selectedUnit: string = 'mBTC'; 15 | selectedExplorer: string = 'blockchaininfo'; 16 | paymentRequestLabel: string = ""; 17 | 18 | constructor(private config: Config) { 19 | } 20 | 21 | ionViewWillEnter() { 22 | Promise.all([ 23 | this.config.get(Config.CONFIG_KEY_BITCOIN_UNIT) , 24 | this.config.get(Config.CONFIG_KEY_PAYMENT_REQUEST_LABEL) 25 | ]).then(promised => { 26 | this.selectedUnit = promised[0]; 27 | this.paymentRequestLabel = promised[1]; 28 | }); 29 | } 30 | 31 | ionViewWillLeave() { 32 | this.config.set(Config.CONFIG_KEY_BITCOIN_UNIT, this.selectedUnit); 33 | this.config.set(Config.CONFIG_KEY_PAYMENT_REQUEST_LABEL, this.paymentRequestLabel); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/pages/settings/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ 'TITLE.SETTINGS' | translate }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 |

{{ 'LIST_HEADING.PIN' | translate }}

22 |

{{ 'LIST_TEXT.PIN' | translate }}

23 |
24 |
25 |
26 |
-------------------------------------------------------------------------------- /src/pages/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { LogoModule } from '../../components/logo/logo.module'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { SettingsPage } from './settings'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | SettingsPage 10 | ], 11 | imports: [ 12 | IonicPageModule.forChild(SettingsPage), 13 | LogoModule, 14 | TranslateModule 15 | ], 16 | exports: [ 17 | SettingsPage 18 | ] 19 | }) 20 | export class SettingsPageModule {} -------------------------------------------------------------------------------- /src/pages/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NavController, ModalController, Modal, IonicPage } from 'ionic-angular'; 3 | import { Config } from '../../providers/index'; 4 | 5 | @IonicPage({ 6 | name : 'settings' 7 | }) 8 | @Component({ 9 | templateUrl : 'settings.html' , 10 | }) 11 | export class SettingsPage { 12 | 13 | pages = ['general', 'currency']; 14 | 15 | constructor( 16 | private config:Config, 17 | private modalController: 18 | ModalController, 19 | private navigation:NavController) { 20 | } 21 | 22 | ionViewCanEnter() : Promise { 23 | return new Promise((resolve, reject) => { 24 | 25 | // if the SettingsPage was already navigated, don't ask again for PIN 26 | for (let i = 0; i < this.navigation.length(); i++) { 27 | if (SettingsPage === this.navigation.getByIndex(i).component) { 28 | resolve(); 29 | return; 30 | } 31 | } 32 | 33 | this.config.get(Config.CONFIG_KEY_PIN).then(value => { 34 | if (value === '') { 35 | resolve(); 36 | } else { 37 | let modal:Modal = this.modalController.create('pincode', { token : value, closable : true }); 38 | 39 | modal.present(); 40 | modal.onDidDismiss(data => { 41 | if (data && data.success) { 42 | resolve(); 43 | } else { 44 | reject(); 45 | } 46 | }) 47 | } 48 | }).catch(e => {console.error(e);}); 49 | }); 50 | } 51 | 52 | changePin() { 53 | let modal:Modal = this.modalController.create('pincode', { change : true }); 54 | modal.present(); 55 | 56 | modal.onDidDismiss(data => { 57 | if (data && data.success) { 58 | this.config.set(Config.CONFIG_KEY_PIN, data.pin); 59 | } 60 | }); 61 | 62 | } 63 | 64 | openPage(index:number) { 65 | this.navigation.push(this.pages[index]); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/pages/transaction/transaction.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ 'TITLE.TRANSACTION' | translate }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{ 'LIST_HEADING.PAYMENT' | translate }} 14 | 15 | {{ transaction.paymentReferenceCurrency }} {{ transaction.paymentReferenceAmount | bitpocketCurrency:currencySeparator:currencyThousandsPoint:2 }} 16 | {{ transaction.paymentStatus | translate }} 17 | 18 | 19 | {{ 'LIST_HEADING.TRANSACTION' | translate }} 20 | 21 | {{ currencySymbol }} {{ transaction.amount | bitpocketUnit:'BTC':currencySymbol | bitpocketCurrency:currencySeparator:currencyThousandsPoint:currencyPrecision }} 22 | {{ referenceCurrencySymbol }} {{ transaction.amount | bitpocketFiat:'BTC':referenceCurrencyRate | bitpocketCurrency:currencySeparator:currencyThousandsPoint }} 23 | {{ (transaction.timestamp * 1000) | date:dateTimeFormat }} 24 | 25 | 26 | {{ 'LIST_TEXT.UNCONFIRMED' | translate }} 27 | 28 | 29 | {{ 'LIST_TEXT.CONFIRMED' | translate }} 30 | 31 | 32 | 33 | 34 | {{ 'LIST_TEXT.INCOMMING' | translate }} 35 | 36 | 37 | {{ 'LIST_TEXT.OUTOING' | translate }} 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/pages/transaction/transaction.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { BitpocketCurrencyPipeModule } from '../../pipes/bitpocket-currency/bitpocket-currency.module'; 4 | import { BitpocketUnitPipeModule } from '../../pipes/bitpocket-unit/bitpocket-unit.module'; 5 | import { BitpocketFiatPipeModule } from '../../pipes/bitpocket-fiat/bitpocket-fiat.module'; 6 | import { TranslateModule } from '@ngx-translate/core'; 7 | import { TransactionPage } from './transaction'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | TransactionPage, 12 | ], 13 | imports: [ 14 | IonicPageModule.forChild(TransactionPage), 15 | TranslateModule , 16 | BitpocketCurrencyPipeModule , 17 | BitpocketUnitPipeModule , 18 | BitpocketFiatPipeModule 19 | ], 20 | }) 21 | export class TransactionPageModule {} 22 | -------------------------------------------------------------------------------- /src/pages/transaction/transaction.scss: -------------------------------------------------------------------------------- 1 | page-transaction { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/transaction/transaction.ts: -------------------------------------------------------------------------------- 1 | import { TransactionStorageService, Config, CurrencyService, BitcoinUnit } from './../../providers/index'; 2 | import { Transaction } from './../../api/transaction'; 3 | import { Component } from '@angular/core'; 4 | import { IonicPage, NavController, NavParams, ViewController } from 'ionic-angular'; 5 | import { TranslateService } from '@ngx-translate/core' 6 | import 'rxjs/add/operator/toPromise'; 7 | 8 | @IonicPage({ 9 | name : 'transaction' , 10 | segment : 'transaction/:txid' 11 | }) 12 | @Component({ 13 | templateUrl: 'transaction.html', 14 | }) 15 | export class TransactionPage { 16 | 17 | txid:string; 18 | transaction:any = {}; 19 | 20 | currencyThousandsPoint: string = ""; 21 | currencySeparator: string = ""; 22 | currencyPrecision: number = 2; 23 | currencySymbol:string = "BTC"; 24 | dateTimeFormat: any; 25 | referenceCurrencySymbol:string = ""; 26 | referenceCurrencyRate:number = 0; 27 | 28 | constructor( 29 | protected currencyService: CurrencyService, 30 | protected config: Config, 31 | protected translation: TranslateService , 32 | protected transactionStorageService: TransactionStorageService , 33 | protected viewController: ViewController , 34 | protected navParams: NavParams) { 35 | this.txid = navParams.get('txid'); 36 | } 37 | 38 | ionViewDidEnter() { 39 | Promise.all([ 40 | this.translation.get('FORMAT.CURRENCY_T').toPromise() , 41 | this.translation.get('FORMAT.CURRENCY_S').toPromise() , 42 | this.translation.get('FORMAT.DATETIME').toPromise() , 43 | this.translation.get('TEXT.LOADING_TRANSACTIONS').toPromise() , 44 | this.config.get(Config.CONFIG_KEY_BITCOIN_UNIT) , 45 | this.currencyService.getSelectedCurrency() , 46 | this.currencyService.getSelectedCurrencyRate() , 47 | this.transactionStorageService.retrieveTransaction(this.txid) 48 | ]).then(promised => { 49 | this.currencyThousandsPoint = promised[0]; 50 | this.currencySeparator = promised[1]; 51 | this.dateTimeFormat = promised[2]; 52 | this.currencySymbol = promised[4]; 53 | this.currencyPrecision = BitcoinUnit.decimalsCount(promised[4]); 54 | this.referenceCurrencySymbol = promised[5]; 55 | this.referenceCurrencyRate = promised[6]; 56 | this.transaction = promised[7]; 57 | }); 58 | } 59 | 60 | close() { 61 | this.viewController.dismiss(); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/pipes/bitpocket-currency/bitpocket-currency.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BitpocketCurrencyPipe } from './bitpocket-currency'; 3 | 4 | @NgModule({ 5 | declarations: [BitpocketCurrencyPipe], 6 | exports: [BitpocketCurrencyPipe] 7 | }) 8 | export class BitpocketCurrencyPipeModule {} -------------------------------------------------------------------------------- /src/pipes/bitpocket-currency/bitpocket-currency.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'bitpocketCurrency' 5 | }) 6 | export class BitpocketCurrencyPipe implements PipeTransform { 7 | transform(value, separator=".", thousandsDelimiter=",", precision=2, symbol="") : string { 8 | 9 | symbol = symbol === "" ? "" : (symbol + " "); 10 | 11 | let n:any = value, 12 | c = isNaN(precision = Math.abs(precision)) ? 2 : precision, 13 | d = separator == undefined ? "." : separator, 14 | t = thousandsDelimiter == undefined ? "," : thousandsDelimiter, 15 | s = value < 0 ? "-" : "", 16 | i:any = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "", 17 | j = (i.length) > 3 ? i.length % 3 : 0; 18 | 19 | return symbol + s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : ""); 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /src/pipes/bitpocket-fiat/bitpocket-fiat.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BitpocketFiatPipe } from './bitpocket-fiat'; 3 | 4 | @NgModule({ 5 | declarations: [BitpocketFiatPipe], 6 | exports: [BitpocketFiatPipe] 7 | }) 8 | export class BitpocketFiatPipeModule {} -------------------------------------------------------------------------------- /src/pipes/bitpocket-fiat/bitpocket-fiat.ts: -------------------------------------------------------------------------------- 1 | import { BitcoinUnit } from './../../providers/index'; 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | 4 | @Pipe({ 5 | name: 'bitpocketFiat' 6 | }) 7 | export class BitpocketFiatPipe implements PipeTransform { 8 | transform(value:any, from="BTC", rate:number=0) : number { 9 | return BitcoinUnit.from(parseFloat(value), from).toFiat(rate); 10 | } 11 | } -------------------------------------------------------------------------------- /src/pipes/bitpocket-unit/bitpocket-unit.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BitpocketUnitPipe } from './bitpocket-unit'; 3 | 4 | @NgModule({ 5 | declarations: [BitpocketUnitPipe], 6 | exports: [BitpocketUnitPipe] 7 | }) 8 | export class BitpocketUnitPipeModule {} -------------------------------------------------------------------------------- /src/pipes/bitpocket-unit/bitpocket-unit.ts: -------------------------------------------------------------------------------- 1 | import { BitcoinUnit } from './../../providers/index'; 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | 4 | @Pipe({ 5 | name: 'bitpocketUnit' 6 | }) 7 | export class BitpocketUnitPipe implements PipeTransform { 8 | transform(value:any, from="BTC", to="BTC") : number { 9 | return BitcoinUnit.from(parseFloat(value), from).to(to); 10 | } 11 | } -------------------------------------------------------------------------------- /src/providers/config.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Storage} from '@ionic/storage'; 3 | 4 | @Injectable() 5 | export class Config { 6 | 7 | constructor( 8 | protected storage: Storage 9 | ) {} 10 | 11 | static CONFIG_KEY_PAYMENT_REQUEST_LABEL = 'payment-request-label'; 12 | static CONFIG_KEY_CURRENCY = 'currency'; 13 | static CONFIG_KEY_BITCOIN_UNIT = 'bitcoin-unit'; 14 | static CONFIG_KEY_EXCHANGE_RATE = 'rate'; 15 | static CONFIG_KEY_PIN = 'pin'; 16 | static CONFIG_KEY_DEFAULT_ACCOUNT = 'default-account'; 17 | static CONFIG_KEY_CURRENCY_CACHE = 'currency-cache'; 18 | static CONFIG_KEY_FEE_PERCENTAGE = 'fee-percentage'; 19 | 20 | initConfig() : Promise { 21 | return new Promise((resolve,reject) => { 22 | Promise.all([ 23 | this.initialize(Config.CONFIG_KEY_EXCHANGE_RATE,-1), 24 | this.initialize(Config.CONFIG_KEY_CURRENCY,'EUR'), 25 | this.initialize(Config.CONFIG_KEY_BITCOIN_UNIT,'mBTC'), 26 | this.initialize(Config.CONFIG_KEY_PIN,''), 27 | this.initialize(Config.CONFIG_KEY_FEE_PERCENTAGE,0), 28 | this.initialize(Config.CONFIG_KEY_CURRENCY_CACHE, [{ 29 | code : 'EUR', rate : 0 30 | }, { 31 | code : 'USD' , rate : 0 32 | }]) 33 | ]).then(() => { 34 | resolve(true); 35 | }).catch(() => { 36 | resolve(false); 37 | }); 38 | }); 39 | } 40 | 41 | isSet(key:string) : Promise { 42 | return new Promise((resolve,reject) => { 43 | this.storage.get(key).then(storedValue => { 44 | if (storedValue === null || storedValue === undefined) { 45 | resolve(false); 46 | } 47 | resolve(true); 48 | }); 49 | }); 50 | } 51 | 52 | /** 53 | * init the key with the given value, only! 54 | * if there is no value set already 55 | */ 56 | initialize(key:string, value:any) : Promise { 57 | return new Promise((resolve,reject) => { 58 | this.isSet(key).then(status => { 59 | if (!status) { 60 | this.storage.set(key,value); 61 | } 62 | resolve(true); 63 | }).catch(() => { 64 | resolve(false); 65 | }); 66 | }); 67 | } 68 | 69 | set(key:string, value:any) : Promise { 70 | return this.storage.set(key, value); 71 | } 72 | 73 | get(key:string) : Promise { 74 | return this.storage.get(key); 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/providers/currency/bitcoin-unit.spec.ts: -------------------------------------------------------------------------------- 1 | import {BitcoinUnit} from './bitcoin-unit' 2 | 3 | describe('Bitcoin Unit', () => { 4 | 5 | it('from btc input with many decimals', () => { 6 | let bU = BitcoinUnit.from(12.12345678, 'BTC'); 7 | expect(bU.to('satoshis')).toEqual(1212345678); 8 | }); 9 | 10 | it('from satoshi input to different units', () => { 11 | // 100.000.000 satoshi is 1 BTC 12 | let bU = BitcoinUnit.from(100000000); 13 | 14 | expect(bU.to('BTC')).toEqual(1); 15 | expect(bU.to('mBTC')).toEqual(1000); 16 | expect(bU.to('bits')).toEqual(1000000); 17 | expect(bU.to('satoshis')).toEqual(100000000); 18 | }); 19 | 20 | it('from fiat to mBTC', () => { 21 | // 400 Fiat is 1 BTC 22 | let bU = BitcoinUnit.fromFiat(100,400); 23 | expect(bU.to('mBTC')).toEqual(250); 24 | }); 25 | 26 | it('from mBTC to Fiat', () => { 27 | // 420.50 Fiat is 1 BTC 28 | let bU = BitcoinUnit.from(350,'mBTC'); 29 | expect(bU.toFiat(420.5,3)).toEqual(147.175); 30 | }); 31 | 32 | }); -------------------------------------------------------------------------------- /src/providers/currency/bitcoin-unit.ts: -------------------------------------------------------------------------------- 1 | const BITCOIN_UNITS = { 2 | 'BTC' : [1e8,8] , 3 | 'mBTC' : [1e5,5] , 4 | 'bits' : [1e2,2] , 5 | 'satoshis' : [1,0] 6 | }; 7 | 8 | export class BitcoinUnit { 9 | // internal representation is in satoshis 10 | private satoshis: number = 0; 11 | 12 | set value(value: number) { 13 | this.satoshis = value; 14 | } 15 | 16 | constructor(value: number) { 17 | this.satoshis = value; 18 | } 19 | 20 | static decimalsCount(bitcoinUnit) : number { 21 | return BitcoinUnit.getUnitSpecification(bitcoinUnit)[1]; 22 | } 23 | 24 | static getUnitSpecification(bitcoinUnit: string) : Array { 25 | if (!BITCOIN_UNITS.hasOwnProperty(bitcoinUnit)) { // fallback to satoshis... 26 | bitcoinUnit = 'satoshis'; 27 | } 28 | return BITCOIN_UNITS[bitcoinUnit]; 29 | } 30 | 31 | static from(value: number, bitcoinUnit: string = 'satoshis') : BitcoinUnit { 32 | let unitSpec = BitcoinUnit.getUnitSpecification(bitcoinUnit); 33 | return new BitcoinUnit( parseInt((value * unitSpec[0]).toFixed(0)) ); 34 | } 35 | 36 | static fromFiat(fiatValue: number, exchangeRate: number) : BitcoinUnit { 37 | let valueInBTC:number = fiatValue / exchangeRate; 38 | return BitcoinUnit.from(valueInBTC, 'BTC'); 39 | } 40 | 41 | to(bitcoinUnit: string = 'satoshis') : number { 42 | let unitSpec = BitcoinUnit.getUnitSpecification(bitcoinUnit); 43 | return parseFloat( (this.satoshis / unitSpec[0]).toFixed(unitSpec[1]) ); 44 | } 45 | 46 | toFiat(exchangeRate: number, decimals: number = 2) : number { 47 | let btcValue = this.to('BTC'); 48 | return parseFloat((btcValue * exchangeRate).toFixed(decimals)); 49 | } 50 | 51 | toString() { 52 | return this.satoshis.toFixed(0); 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/providers/currency/bitcoinaverage-service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { CurrencyExchangeRate } from '../../api/currency-exchange-rate'; 4 | import { CurrencyExchangeService } from '../../api/currency-exchange-service'; 5 | import { Config } from '../../providers/index'; 6 | 7 | @Injectable() 8 | export class BitcoinAverageExchangeService implements CurrencyExchangeService { 9 | 10 | constructor(protected http:HttpClient, protected config:Config) { 11 | } 12 | 13 | prepareOne(code, json:any) : CurrencyExchangeRate { 14 | return { 15 | code : code , 16 | rate : json.last 17 | }; 18 | } 19 | 20 | prepareAll(json:any) : Array { 21 | let output = []; 22 | 23 | for (let code in json) { 24 | if (json.hasOwnProperty(code)) { 25 | output.push(this.prepareOne(code.slice(-3),json[code])) 26 | } 27 | } 28 | 29 | return output; 30 | } 31 | 32 | getAvailableCurrencies() : Promise> { 33 | return this.getExchangeRates(); 34 | } 35 | 36 | getExchangeRates() : Promise> { 37 | return new Promise((resolve, reject) => { 38 | try { 39 | this.http.get('https://apiv2.bitcoinaverage.com/indices/global/ticker/short?crypto=BTC') 40 | .subscribe( 41 | response => { 42 | let currencies = this.prepareAll(response); 43 | this.config.set(Config.CONFIG_KEY_CURRENCY_CACHE, currencies); 44 | resolve(currencies); 45 | } , 46 | error => { 47 | resolve(this.config.get(Config.CONFIG_KEY_CURRENCY_CACHE)); 48 | } 49 | ); 50 | } catch(e) { 51 | console.error(e); 52 | resolve(this.config.get(Config.CONFIG_KEY_CURRENCY_CACHE)); 53 | } 54 | }); 55 | } 56 | 57 | getExchangeRate(code:string) : Promise { 58 | return new Promise((resolve, reject) => { 59 | try { 60 | this.http.get('https://apiv2.bitcoinaverage.com/indices/global/ticker/short?crypto=BTC&fiats=' + code) 61 | .subscribe( 62 | response => { 63 | resolve(this.prepareOne(code, response['BTC' + code])); 64 | } , 65 | error => { 66 | reject(); 67 | } 68 | ); 69 | } catch(e) { 70 | console.error(e); 71 | reject(); 72 | } 73 | }); 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /src/providers/currency/cryptocurrency-service.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CryptocurrencyService, 3 | BITCOIN_ADDRESS, 4 | BITCOIN_XPUB_KEY, 5 | TESTNET_ADDRESS, 6 | TESTNET_TPUB_KEY 7 | } from './cryptocurrency-service'; 8 | 9 | describe('Bitcoin', () => { 10 | 11 | let service = new CryptocurrencyService(); 12 | 13 | it('should correctly parse account address input', () => { 14 | let address = "15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma"; 15 | let result = service.parseInput(" " + address + " "); 16 | expect(result.data).toEqual(address); 17 | expect(result.type).toEqual(BITCOIN_ADDRESS); 18 | }); 19 | 20 | it('should correctly parse account uri input', () => { 21 | let address = "15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma"; 22 | let result = service.parseInput("bitcoin:" + address + "?amount=100.0000&text=something"); 23 | expect(result.data).toEqual(address); 24 | expect(result.type).toEqual(BITCOIN_ADDRESS); 25 | }); 26 | 27 | it('should correctly parse account xpub input', () => { 28 | let key = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"; 29 | let result = service.parseInput(" " + key + " "); 30 | expect(result.data).toEqual(key); 31 | expect(result.type).toEqual(BITCOIN_XPUB_KEY); 32 | }); 33 | 34 | }); 35 | 36 | describe('Testnet', () => { 37 | 38 | let service = new CryptocurrencyService(); 39 | 40 | it('should correctly parse account address input', () => { 41 | let address = "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn"; 42 | let result = service.parseInput(" " + address + " "); 43 | expect(result.data).toEqual(address); 44 | expect(result.type).toEqual(TESTNET_ADDRESS); 45 | }); 46 | 47 | it('should correctly parse account uri input', () => { 48 | let address = "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn"; 49 | let result = service.parseInput("bitcoin:" + address + "?amount=100.0000&text=something"); 50 | expect(result.data).toEqual(address); 51 | expect(result.type).toEqual(TESTNET_ADDRESS); 52 | }); 53 | 54 | it('should correctly parse account tpub input', () => { 55 | let key = "tpubD6NzVbkrYhZ4WUbpsyRwqJ8C2f5T7vXZVaiZkgsBCeNAT3gQxKAaFnqdM3jUzVJDAQsnnMBHvcwKUqt6NesoZRuW1ED8xEGoV5HDR3vad1j"; 56 | let result = service.parseInput(" " + key + " "); 57 | expect(result.data).toEqual(key); 58 | expect(result.type).toEqual(TESTNET_TPUB_KEY); 59 | }); 60 | 61 | it('should parse tpub input from android wallet', () => { 62 | let input = "tpubD8dWq6wHd9sXLeDZ8hiYixYvqApcRBgiYhtKVhEozbhMZBA4qgYtTXtfPWqWKdFXB2uKxptSZiR9LQ4kjNSBNmWfxPFhEByzJx3X9ry4mWw?c=1490794392&h=bip32"; 63 | let key = "tpubD8dWq6wHd9sXLeDZ8hiYixYvqApcRBgiYhtKVhEozbhMZBA4qgYtTXtfPWqWKdFXB2uKxptSZiR9LQ4kjNSBNmWfxPFhEByzJx3X9ry4mWw"; 64 | let result = service.parseInput(" " + input + " "); 65 | expect(result.data).toEqual(key); 66 | expect(result.type).toEqual(TESTNET_TPUB_KEY); 67 | }); 68 | 69 | }); -------------------------------------------------------------------------------- /src/providers/currency/cryptocurrency-service.ts: -------------------------------------------------------------------------------- 1 | import { Transaction } from './../../api/transaction'; 2 | import {Injectable} from '@angular/core'; 3 | import * as regex from 'crypto-regex'; 4 | import * as bitcoin from 'bitcoinjs-lib'; 5 | 6 | export const BITCOIN = "bitcoin"; 7 | export const TESTNET = "testnet"; 8 | 9 | export const BITCOIN_ADDRESS = "bitcoin-static-address"; 10 | export const BITCOIN_XPUB_KEY = "bitcoin-xpub-key"; 11 | export const TESTNET_ADDRESS = "testnet-static-address"; 12 | export const TESTNET_TPUB_KEY = "testnet-tpub-key"; 13 | 14 | export const REGEX_BITCOIN_ADDRESS = new RegExp("^(" + regex['bitcoin-address'] + ")$"); 15 | export const REGEX_BITCOIN_URI = new RegExp("^" + regex['bitcoin-uri']); 16 | export const REGEX_XPUB_KEY = new RegExp("^(" + regex['bitcoin-xpub-key'] + ")$"); 17 | export const REGEX_TESTNET_ADDRESS = new RegExp("^(" + regex['testnet-address'] + ")$"); 18 | export const REGEX_TESTNET_URI = new RegExp("^" + regex['testnet-uri']); 19 | export const REGEX_TPUB_KEY = new RegExp("^(" + regex['testnet-tpub-key'] + ")$"); 20 | 21 | @Injectable() 22 | export class CryptocurrencyService { 23 | 24 | deriveAddress(key:string, index:number, change:boolean = false) : string { 25 | try { 26 | let data = this.parseXpubKeyInput(key); 27 | let path = this.getDerivationPath(index, change); 28 | 29 | if (data.currency == BITCOIN) { 30 | return bitcoin.HDNode.fromBase58(key).derivePath(path).getAddress(); 31 | } else { 32 | return bitcoin.HDNode.fromBase58(key, [bitcoin.networks.testnet]).derivePath(path).getAddress(); 33 | } 34 | } catch (e) { 35 | throw new Error('Could not derive index from key' + e); 36 | } 37 | } 38 | 39 | getDerivationPath(index:number = 0, change:boolean = false) { 40 | return (change ? 1 : 0) + '/' + index; 41 | } 42 | 43 | parseXpubKeyInput(input: string) { 44 | try { 45 | if (REGEX_XPUB_KEY.test(input)) { 46 | input = input.match(REGEX_XPUB_KEY)[2]; 47 | if (bitcoin.HDNode.fromBase58(input).toBase58() === input) { 48 | return { 49 | type : BITCOIN_XPUB_KEY , 50 | currency : BITCOIN , 51 | data : input.match(REGEX_XPUB_KEY)[1] 52 | }; 53 | } 54 | } 55 | if (REGEX_TPUB_KEY.test(input)) { 56 | input = input.match(REGEX_TPUB_KEY)[2]; 57 | if ((bitcoin.HDNode.fromBase58(input, [bitcoin.networks.testnet]).toBase58() === input)) { 58 | return { 59 | type : TESTNET_TPUB_KEY , 60 | currency : TESTNET , 61 | data : input.match(REGEX_TPUB_KEY)[1] 62 | } 63 | } 64 | } 65 | } catch(e) { 66 | console.error("error", e); 67 | } 68 | 69 | return null; 70 | } 71 | 72 | parseAddressInput(input: string) { 73 | let addressRegexs = [REGEX_BITCOIN_ADDRESS, REGEX_BITCOIN_URI, REGEX_TESTNET_ADDRESS, REGEX_TESTNET_URI]; 74 | for(let i = 0; i < addressRegexs.length; i++) { 75 | if (addressRegexs[i].test(input)) { 76 | return { 77 | type : i > 1 ? TESTNET_ADDRESS : BITCOIN_ADDRESS , 78 | currency : i > 1 ? TESTNET : BITCOIN , 79 | data : input.match(addressRegexs[i])[1] 80 | }; 81 | } 82 | } 83 | return null; 84 | } 85 | 86 | /** 87 | * Parses account input information 88 | * 89 | * @param input account information 90 | * @return Account 91 | * @throws Error 92 | * 93 | */ 94 | parseInput(input:string) { 95 | input = input.trim(); 96 | let address = this.parseAddressInput(input); 97 | if (address) { 98 | return address; 99 | } 100 | 101 | let key = this.parseXpubKeyInput(input); 102 | if (key) { 103 | return key; 104 | } 105 | 106 | throw new Error('Could not parse input information'); 107 | } 108 | 109 | isConfirmed(transaction:Transaction) : boolean { 110 | if (transaction.confirmations >= 6) { 111 | return true; 112 | } 113 | return false; 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /src/providers/currency/currency-service.spec.ts: -------------------------------------------------------------------------------- 1 | import {CurrencyService} from './currency-service'; 2 | 3 | describe('Currency', () => { 4 | 5 | let currencyService: CurrencyService = new CurrencyService(null,null); 6 | 7 | it('format Currency', () => { 8 | expect(currencyService.formatNumber(10,',')).toEqual('10,00'); 9 | expect(currencyService.formatNumber(10.2501,',',6,2)).toEqual('10,2501'); 10 | expect(currencyService.formatNumber(10.250,',',3,3)).toEqual('10,250'); 11 | }); 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /src/providers/currency/currency-service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {CurrencyExchangeService} from '../../api/currency-exchange-service'; 3 | import {CurrencyExchangeRate} from '../../api/currency-exchange-rate'; 4 | import {Config, BitcoinAverageExchangeService} from './../index'; 5 | 6 | @Injectable() 7 | export class CurrencyService { 8 | 9 | constructor(protected config: Config, protected exchangeService: BitcoinAverageExchangeService) { 10 | } 11 | 12 | getFeePercentage() : Promise { 13 | return this.config.get(Config.CONFIG_KEY_FEE_PERCENTAGE); 14 | } 15 | 16 | getExchangeService() : CurrencyExchangeService { 17 | return this.exchangeService; 18 | } 19 | 20 | getSelectedCurrency() : Promise { 21 | return this.config.get(Config.CONFIG_KEY_CURRENCY); 22 | } 23 | 24 | getSelectedCurrencyRate() : Promise { 25 | return this.config.get(Config.CONFIG_KEY_EXCHANGE_RATE).then(rate => { 26 | return parseFloat(rate); 27 | }); 28 | } 29 | 30 | getCalculatedCurrencyRate() : Promise { 31 | return new Promise ((resolve, reject) => { 32 | Promise.all([ 33 | this.config.get(Config.CONFIG_KEY_EXCHANGE_RATE) , 34 | this.config.get(Config.CONFIG_KEY_FEE_PERCENTAGE) 35 | ]).then(values => { 36 | resolve(parseFloat(values[0]) * ((100.0 - parseFloat(values[1]))/100.0)); 37 | }); 38 | }); 39 | } 40 | 41 | getAvailableCurrencies() : Promise { 42 | return new Promise((resolve, reject) => { 43 | resolve(this.getExchangeService().getAvailableCurrencies()); 44 | }); 45 | } 46 | 47 | setSelectedCurrency(code:string) : CurrencyService { 48 | this.config.set(Config.CONFIG_KEY_CURRENCY, code).then(() => { 49 | this.updateCurrencyRate(); 50 | }); 51 | return this; 52 | } 53 | 54 | setFeePercentage(feePercentage:number) : CurrencyService { 55 | this.config.set(Config.CONFIG_KEY_FEE_PERCENTAGE, feePercentage); 56 | return this; 57 | } 58 | 59 | updateCurrencyRate() : Promise { 60 | return new Promise((resolve,reject) => { 61 | Promise.all([ 62 | this.getExchangeService() , 63 | this.getSelectedCurrency() 64 | ]).then(promised => { 65 | promised[0].getExchangeRate(promised[1]).then(data => { 66 | this.config.set(Config.CONFIG_KEY_EXCHANGE_RATE, data.rate); 67 | 68 | resolve({ 69 | 'code' : promised[1] , 70 | 'rate' : data.rate 71 | }); 72 | }).catch(reject); 73 | }).catch(reject); 74 | }); 75 | } 76 | 77 | /** 78 | * Format a number, fillup with 0 until minDecimals is reached, cut at maxDecimals 79 | */ 80 | formatNumber(value: number, separator: string, maxDecimals: number = 2, minDecimals: number = 2) : string { 81 | let formattedNumber = value.toFixed(maxDecimals).replace(/\./,separator); 82 | 83 | if (minDecimals >= maxDecimals) { 84 | return formattedNumber; 85 | } else { 86 | let startLength = (formattedNumber.indexOf(separator) + minDecimals); 87 | let endIndex = formattedNumber.length - 1; 88 | while (startLength < endIndex) { 89 | if (formattedNumber[endIndex] != "0") { 90 | break; 91 | } else { 92 | formattedNumber = formattedNumber.substr(0,formattedNumber.length-1); 93 | } 94 | 95 | endIndex--; 96 | } 97 | } 98 | 99 | return formattedNumber; 100 | } 101 | 102 | } 103 | 104 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { TransactionServiceWrapper } from './transaction/transaction-service-wrapper'; 2 | import { Storage } from '@ionic/storage'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import { ModalController } from 'ionic-angular'; 5 | import { TransactionStorageService } from './transaction/transaction-storage-service'; 6 | import { InsightTransactionService } from './transaction/insight-transaction-service/insight-transaction-service'; 7 | import { BlockchainInfoService } from './transaction/blockchain-info-service/blockchain-info-service'; 8 | import { QRScanner } from './qrscanner/qrscanner'; 9 | import { PaymentService } from './payment/payment-service'; 10 | import { InsightPaymentRequestHandler } from './payment/payment-request-handler/insight-payment-request-handler'; 11 | import { BitcoinAverageExchangeService } from './currency/bitcoinaverage-service'; 12 | import { BitcoinUnit } from './currency/bitcoin-unit'; 13 | import { CurrencyService } from './currency/currency-service'; 14 | import { AccountService, ACCOUNT_TYPE_BITCOIN_ADDRESS, ACCOUNT_TYPE_BITCOIN_XPUB_KEY, ACCOUNT_TYPE_TESTNET_ADDRESS, ACCOUNT_TYPE_TESTNET_TPUB_KEY } from './account/account-service'; 15 | import { AccountSyncService } from './account/account-sync-service'; 16 | import { Repository } from './repository'; 17 | import { Config } from './config'; 18 | import { 19 | CryptocurrencyService , 20 | BITCOIN, 21 | TESTNET, 22 | BITCOIN_ADDRESS, 23 | BITCOIN_XPUB_KEY, 24 | TESTNET_ADDRESS, 25 | TESTNET_TPUB_KEY, 26 | REGEX_BITCOIN_ADDRESS, 27 | REGEX_BITCOIN_URI, 28 | REGEX_TESTNET_ADDRESS, 29 | REGEX_TESTNET_URI, 30 | REGEX_XPUB_KEY, 31 | REGEX_TPUB_KEY 32 | } from './currency/cryptocurrency-service'; 33 | 34 | export function provideCurrencyService(config:Config, exchangeService:BitcoinAverageExchangeService) { 35 | return new CurrencyService(config, exchangeService); 36 | } 37 | 38 | export function provideAccountService(cryptocurrencyService:CryptocurrencyService, config:Config, repository:Repository) { 39 | return new AccountService(cryptocurrencyService, config, repository); 40 | } 41 | 42 | export function provideAccountSyncService(transactionService:TransactionServiceWrapper, transactionStorageService:TransactionStorageService, accountService:AccountService, cryptocurrencyService:CryptocurrencyService) { 43 | return new AccountSyncService(transactionService, transactionStorageService, accountService, cryptocurrencyService); 44 | } 45 | 46 | export function provideTransactionService(http:HttpClient, cryptocurrencyService:CryptocurrencyService) { 47 | return new TransactionServiceWrapper(http, cryptocurrencyService); 48 | } 49 | 50 | export function provideTransactionStorageService(repository:Repository) { 51 | return new TransactionStorageService(repository); 52 | } 53 | 54 | export function providePaymentService(transactionService:TransactionServiceWrapper, cryptocurrencyService:CryptocurrencyService) { 55 | return new PaymentService(transactionService, cryptocurrencyService); 56 | } 57 | 58 | export function provideCryptocurrencyService() { 59 | return new CryptocurrencyService(); 60 | } 61 | 62 | export function provideRepository() { 63 | return new Repository(); 64 | } 65 | 66 | export function provideConfig(storage:Storage) { 67 | return new Config(storage); 68 | } 69 | 70 | export function provideQRScanner(modalController: ModalController) { 71 | return new QRScanner(modalController); 72 | } 73 | 74 | export function provideBitcoinAverageExchangeService(http:HttpClient, config:Config) { 75 | return new BitcoinAverageExchangeService(http, config); 76 | } 77 | 78 | export { 79 | Config , 80 | Repository , 81 | BitcoinUnit , 82 | AccountService , 83 | AccountSyncService, 84 | CurrencyService , 85 | CryptocurrencyService , 86 | BitcoinAverageExchangeService, 87 | InsightPaymentRequestHandler, 88 | PaymentService, 89 | QRScanner, 90 | TransactionStorageService, 91 | InsightTransactionService, 92 | BlockchainInfoService, 93 | TransactionServiceWrapper, 94 | BITCOIN, 95 | TESTNET, 96 | BITCOIN_ADDRESS, 97 | BITCOIN_XPUB_KEY, 98 | TESTNET_ADDRESS, 99 | TESTNET_TPUB_KEY, 100 | REGEX_BITCOIN_ADDRESS, 101 | REGEX_BITCOIN_URI, 102 | REGEX_TESTNET_ADDRESS, 103 | REGEX_TESTNET_URI, 104 | REGEX_XPUB_KEY, 105 | REGEX_TPUB_KEY, 106 | ACCOUNT_TYPE_BITCOIN_ADDRESS, 107 | ACCOUNT_TYPE_BITCOIN_XPUB_KEY, 108 | ACCOUNT_TYPE_TESTNET_ADDRESS, 109 | ACCOUNT_TYPE_TESTNET_TPUB_KEY 110 | }; 111 | -------------------------------------------------------------------------------- /src/providers/payment/payment-request-handler/insight-payment-request-handler.ts: -------------------------------------------------------------------------------- 1 | import { PaymentRequestHandler } from './../../../api/payment-request-handler'; 2 | import { TransactionService } from './../../../api/transaction-service'; 3 | import { PaymentRequest, PAYMENT_STATUS_SERVICE_ERROR, PAYMENT_STATUS_RECEIVED, PAYMENT_STATUS_PARTIAL_PAID, PAYMENT_STATUS_OVERPAID } from './../../../api/payment-request'; 4 | import { EventEmitter } from 'events'; 5 | import * as io from 'socket.io-client'; 6 | 7 | export class InsightPaymentRequestHandler extends EventEmitter implements PaymentRequestHandler { 8 | 9 | protected _paymentRequest: PaymentRequest; 10 | protected socket: SocketIO.Socket; 11 | protected transactionService: TransactionService; 12 | protected serviceUrl: string; 13 | 14 | get paymentRequest() : PaymentRequest { 15 | return this._paymentRequest; 16 | } 17 | 18 | constructor() { 19 | super(); 20 | } 21 | 22 | static createPaymentRequestHandler(paymentRequest: PaymentRequest, transactionService: TransactionService, serviceUrl:string) { 23 | let handler = new InsightPaymentRequestHandler(); 24 | handler._paymentRequest = paymentRequest; 25 | handler.transactionService = transactionService; 26 | handler.serviceUrl = serviceUrl; 27 | handler.init(); 28 | return handler; 29 | } 30 | 31 | init() { 32 | try { 33 | this.socket = io(this.serviceUrl); 34 | this.socket.on('error', () => { 35 | this.emit("payment-status:" + PAYMENT_STATUS_SERVICE_ERROR); 36 | }).on('bitcoind/addresstxid', data => { 37 | this.triggerStatusUpdate(data.txid) 38 | .then((close:boolean) => { 39 | if (close && this && this.cancel) { 40 | this.cancel(); 41 | } 42 | }); 43 | }).on('connect', () => { 44 | this.socket.emit('subscribe', 'bitcoind/addresstxid', [ this.paymentRequest.address ]); 45 | }); 46 | } catch (e) { 47 | this.emit("payment-status:" + PAYMENT_STATUS_SERVICE_ERROR); 48 | } 49 | } 50 | 51 | triggerStatusUpdate(txid:string) : Promise { 52 | return new Promise ((resolve, reject) => { 53 | this.transactionService.findTransactions({ 54 | addresses : [this.paymentRequest.address] , 55 | txid : txid 56 | }).then(transactions => { 57 | if (transactions.length > 0) { 58 | let event = { 59 | txid : txid , 60 | address : this.paymentRequest.address , 61 | amount : transactions[0].amount 62 | }; 63 | 64 | if (transactions[0].amount < this.paymentRequest.amount) { 65 | this.emit("payment-status:" + PAYMENT_STATUS_PARTIAL_PAID, event); 66 | } else if (transactions[0].amount > this.paymentRequest.amount) { 67 | this.emit("payment-status:" + PAYMENT_STATUS_OVERPAID, event); 68 | } else { 69 | this.emit("payment-status:" + PAYMENT_STATUS_RECEIVED, event); 70 | } 71 | 72 | resolve(true); 73 | } else { 74 | resolve(false); 75 | } 76 | }); 77 | }); 78 | } 79 | 80 | cancel() { 81 | this.socket.disconnect(); 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /src/providers/payment/payment-service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import { TransactionServiceWrapper, InsightPaymentRequestHandler, CryptocurrencyService, BITCOIN, TESTNET } from '../index'; 3 | import { PaymentRequest } from './../../api/payment-request'; 4 | import { PaymentRequestHandler } from './../../api/payment-request-handler'; 5 | import * as payment from '../../api/payment-service'; 6 | 7 | @Injectable() 8 | export class PaymentService implements payment.PaymentService { 9 | 10 | constructor( 11 | protected transactionService: TransactionServiceWrapper , 12 | protected cryptocurrencyService: CryptocurrencyService 13 | ) {} 14 | 15 | createPaymentRequestHandler(paymentRequest: PaymentRequest) : PaymentRequestHandler { 16 | let currency = this.cryptocurrencyService.parseInput(paymentRequest.address).currency; 17 | 18 | if (currency == BITCOIN) { 19 | return InsightPaymentRequestHandler.createPaymentRequestHandler(paymentRequest, this.transactionService, 'https://insight.bitpay.com'); 20 | } else if (currency == TESTNET) { 21 | return InsightPaymentRequestHandler.createPaymentRequestHandler(paymentRequest, this.transactionService, 'https://test-insight.bitpay.com'); 22 | } 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/providers/qrscanner/qrscanner.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ModalController, Modal } from 'ionic-angular'; 3 | 4 | @Injectable() 5 | export class QRScanner { 6 | 7 | protected modal:Modal; 8 | 9 | constructor(protected modalController:ModalController) { 10 | } 11 | 12 | scan(validate: (text:any) => any = (text) => { return text; }) : Promise { 13 | this.modal = this.modalController.create('qrscanner', { validate: validate}); 14 | this.modal.present(); 15 | 16 | return new Promise((resolve, reject) => { 17 | this.modal.onDidDismiss(data => { 18 | if (data && data.text) { 19 | resolve(data.text); 20 | } else { 21 | reject(); 22 | } 23 | }); 24 | }); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/providers/repository.ts: -------------------------------------------------------------------------------- 1 | import PouchDB from 'pouchdb'; 2 | import {Injectable} from '@angular/core'; 3 | 4 | declare var emit:any; 5 | 6 | @Injectable() 7 | export class Repository { 8 | 9 | protected _db:any; 10 | protected _dbname = "bitpocket-pouchdb"; 11 | 12 | get db() : any { 13 | return this._db; 14 | } 15 | 16 | addOrEditDocument(doc:any) : Promise { 17 | if (doc._id) { 18 | return this.db.upsert(doc._id, (oldDoc) => { 19 | return doc; 20 | }); 21 | } else { 22 | return this.addDocument(doc); 23 | } 24 | } 25 | 26 | addDocumentIfNotExists(doc:any) : Promise { 27 | return this.db.putIfNotExists(doc); 28 | } 29 | 30 | addDocument(doc:any) : Promise { 31 | return this.db.post(doc); 32 | } 33 | 34 | editDocument(doc:any) : Promise { 35 | return this.db.put(doc); 36 | } 37 | 38 | removeDocument(id:string) : Promise { 39 | return this.db.get(id).then((doc) => { 40 | return this.db.remove(doc); 41 | }); 42 | } 43 | 44 | findById(id:string) : Promise { 45 | return this.db.get(id); 46 | } 47 | 48 | findByDocumentType(doctype:string) : Promise> { 49 | return new Promise>((resolve, reject) => { 50 | this._db 51 | .find({ 52 | selector : { 53 | doctype : doctype 54 | } 55 | }) 56 | .then((res) => { 57 | resolve(this.prepareDocuments(res)); 58 | }).catch(error => { 59 | reject(error); 60 | }); 61 | }); 62 | } 63 | 64 | findDocuments(query) : Promise> { 65 | return new Promise> ((resolve, reject) => { 66 | this.db.find(query).then((res) => { 67 | resolve(this.prepareDocuments(res)); 68 | }).catch(error => { 69 | reject(error); 70 | }); 71 | }); 72 | } 73 | 74 | init() : Promise { 75 | 76 | this._db = new PouchDB(this._dbname); 77 | 78 | return Promise.all([ 79 | this.db.createIndex({ 80 | index : { 81 | fields : ['doctype'] 82 | } 83 | }) , 84 | this.db.createIndex({ 85 | index : { 86 | fields : ['timestamp','account'] 87 | } 88 | }) , 89 | this.db.createIndex({ 90 | index : { 91 | fields : ['timestamp','address'] 92 | } 93 | }) 94 | ]); 95 | } 96 | 97 | prepareDocuments(res) : Array { 98 | let docs = []; 99 | 100 | if (res.docs && res.docs.length > 0) { 101 | docs = res.docs; 102 | } 103 | 104 | return docs; 105 | } 106 | 107 | } -------------------------------------------------------------------------------- /src/providers/transaction/blockchain-info-service/blockchain-info-service.spec.ts: -------------------------------------------------------------------------------- 1 | import { BlockchainInfoService } from './../../index'; 2 | import {data} from './blockchain-info-data'; 3 | 4 | describe('Transaction Service', () => { 5 | 6 | let transactionService = new BlockchainInfoService(null); 7 | 8 | it('should build correct urls', () => { 9 | expect(transactionService.buildUrl({ 10 | addresses : ['152f1muMCNa7goXYhYAQC61hxEgGacmncB','12ni9ddt4WHfEQriVN7DajDBd1JbKL9yUZ','1FS4FF2SYdHf3PGfSbjpdYcUiUYxiVLy73'] , 11 | to : 15 , 12 | from : 5 13 | })).toEqual('https://blockchain.info/multiaddr?active=152f1muMCNa7goXYhYAQC61hxEgGacmncB|12ni9ddt4WHfEQriVN7DajDBd1JbKL9yUZ|1FS4FF2SYdHf3PGfSbjpdYcUiUYxiVLy73&limit=10&offset=5&cors=true'); 14 | }); 15 | 16 | it('should parse transactions correctly', () => { 17 | let txs = transactionService.parseTransactions(['19L9LhCArs9SUNu3ZzaqJ1ys3dGgvJmi5T'], data); 18 | let incomming = [true,false,true,false,true,false,true,false,true,false,false,true,true]; 19 | 20 | for (let i = 0; i < incomming.length; i++) { 21 | expect(txs[i].incomming).toBe(incomming[i]); 22 | } 23 | }); 24 | 25 | }); -------------------------------------------------------------------------------- /src/providers/transaction/blockchain-info-service/blockchain-info-service.ts: -------------------------------------------------------------------------------- 1 | import { TransactionService } from './../../../api/transaction-service'; 2 | import { TransactionFilter } from './../../../api/transaction-filter'; 3 | import { BITCOIN, BitcoinUnit } from './../../index'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { Transaction } from './../../../api/transaction'; 6 | 7 | const BLOCKCHAIN_INFO_BASE_URL = "https://blockchain.info/"; 8 | 9 | export class BlockchainInfoService implements TransactionService { 10 | 11 | protected lastKnownBlockchainHeight = 0; 12 | 13 | set blockchainHeight(height:number) { 14 | this.lastKnownBlockchainHeight = height; 15 | } 16 | 17 | constructor(protected http:HttpClient) {} 18 | 19 | findTransactions(filter: TransactionFilter) : Promise { 20 | return new Promise((resolve, reject) => { 21 | this.getBlockHeight() 22 | .then(() => { 23 | this.http.get(this.buildUrl(filter)) 24 | .subscribe(response => { 25 | resolve(this.parseTransactions(filter.addresses, response)); 26 | }, () => { reject(); }) 27 | }) 28 | }); 29 | } 30 | 31 | getBlockHeight() : Promise { 32 | return new Promise ((resolve, reject) => { 33 | this.http.get(BLOCKCHAIN_INFO_BASE_URL + 'q/getblockcount?cors=true') 34 | .subscribe((response:any) => { 35 | if (response) { 36 | this.lastKnownBlockchainHeight = response; 37 | } 38 | resolve(this.lastKnownBlockchainHeight); 39 | }, () => { resolve(this.lastKnownBlockchainHeight); }); 40 | }); 41 | } 42 | 43 | parseTransactions(addresses:string[], json:any) { 44 | let txs:Transaction[] = []; 45 | let items = []; 46 | 47 | if (json && json.txs && Array.isArray(json.txs)) { 48 | items = json.txs; 49 | } else if (json && json.inputs && json.out) { 50 | items = [json]; 51 | } 52 | 53 | for (let item of items) { 54 | let tx:any = this.parseTransactionInputs(addresses, item.inputs); 55 | if (!tx) { 56 | tx = this.parseTransactionOutputs(addresses, item.out); 57 | } 58 | 59 | if (tx) { 60 | tx.currency = "BTC"; 61 | tx.confirmations = this.lastKnownBlockchainHeight - item['block_height']; 62 | tx._id = item.hash; 63 | tx.timestamp = parseInt(item.time); 64 | txs.push(tx); 65 | } 66 | } 67 | 68 | return txs; 69 | } 70 | 71 | parseTransactionOutputs(addresses:string[], outputs:any[]) { 72 | if (outputs && Array.isArray(outputs)) { 73 | for (let output of outputs) { 74 | if (output.addr) { 75 | let index = addresses.indexOf(output.addr); 76 | if (index >= 0) { 77 | return { 78 | address : addresses[index] , 79 | incomming : true , 80 | amount : BitcoinUnit.from(output.value).to('BTC') 81 | }; 82 | } 83 | } 84 | } 85 | } 86 | return null; 87 | } 88 | 89 | parseTransactionInputs(addresses:string[], inputs:any[]) { 90 | if (inputs && Array.isArray(inputs)) { 91 | for (let input of inputs) { 92 | if (input.prev_out && input.prev_out.addr && input.prev_out.addr) { 93 | let index = addresses.indexOf(input.prev_out.addr); 94 | if (index >= 0) { 95 | return { 96 | address : addresses[index] , 97 | incomming : false , 98 | amount : BitcoinUnit.from(input.prev_out.value).to('BTC') 99 | }; 100 | } 101 | } 102 | } 103 | } 104 | return null; 105 | } 106 | 107 | buildUrl(filter:TransactionFilter = {}) : string { 108 | let url = ""; 109 | 110 | if (filter.txid) { 111 | url = BLOCKCHAIN_INFO_BASE_URL + "rawtx/" + filter.txid + "?"; 112 | } else if (filter.addresses && filter.addresses.length > 0) { 113 | url = BLOCKCHAIN_INFO_BASE_URL + "multiaddr?active=" + filter.addresses.join('|') + "&"; 114 | } 115 | 116 | if (filter.from >= 0 && filter.to > 0) { 117 | let limit = filter.to - filter.from; 118 | url += 'limit=' + limit + '&offset=' + filter.from; 119 | } else { 120 | url += 'limit=50&offset=0'; 121 | } 122 | 123 | return url + '&cors=true'; 124 | } 125 | 126 | } -------------------------------------------------------------------------------- /src/providers/transaction/blockcypher-service/blockcypher-data.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/providers/transaction/blockcypher-service/blockcypher-data.ts -------------------------------------------------------------------------------- /src/providers/transaction/blockcypher-service/blockcypher-service.spec.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getbitpocket/bitpocket-mobile-app/907cf005fcd2c33bd9c2aa2d2a9a2281bc1a6376/src/providers/transaction/blockcypher-service/blockcypher-service.spec.ts -------------------------------------------------------------------------------- /src/providers/transaction/blockcypher-service/blockcypher-service.ts: -------------------------------------------------------------------------------- 1 | import { TransactionService } from './../../../api/transaction-service'; 2 | import { TransactionFilter } from './../../../api/transaction-filter'; 3 | import { BITCOIN, TESTNET } from './../../index'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { Transaction } from './../../../api/transaction'; 6 | 7 | // NOT YET IMPLEMENTED 8 | 9 | export class BlockcypherService implements TransactionService { 10 | 11 | constructor(protected http:HttpClient, protected cryptocurrency:string) {} 12 | 13 | findTransactions(filter: TransactionFilter) : Promise { 14 | return new Promise((resolve, reject) => { 15 | 16 | }); 17 | } 18 | 19 | parseTransactions(addresses:string[], json:any) { 20 | 21 | } 22 | 23 | buildUrl(filter:TransactionFilter = {}) : string { 24 | let url = ""; 25 | 26 | if (this.cryptocurrency == TESTNET) { 27 | url += "https://api.blockcypher.com/v1/btc/test3/"; 28 | } else { 29 | url += "https://api.blockcypher.com/v1/btc/main/"; 30 | } 31 | 32 | if (filter.txid) { 33 | } else if (filter.addresses && filter.addresses.length > 0) { 34 | } 35 | 36 | if (filter.from >= 0 && filter.to > 0) { 37 | let limit = filter.to - filter.from; 38 | url += 'limit=' + limit + '&offset=' + filter.from; 39 | } else { 40 | url += 'limit=50&offset=0'; 41 | } 42 | 43 | return url; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/providers/transaction/insight-transaction-service/insight-transaction-service.spec.ts: -------------------------------------------------------------------------------- 1 | import { InsightTransactionService } from './../../index'; 2 | import {data} from './insight-transaction-data'; 3 | 4 | describe('Transaction Service', () => { 5 | 6 | let transactionService = new InsightTransactionService(null, "https://insight.bitpay.com"); 7 | 8 | it('should build correct urls', () => { 9 | expect(transactionService.buildUrl({ 10 | addresses : ['152f1muMCNa7goXYhYAQC61hxEgGacmncB','12ni9ddt4WHfEQriVN7DajDBd1JbKL9yUZ','1FS4FF2SYdHf3PGfSbjpdYcUiUYxiVLy73'] , 11 | to : 15 , 12 | from : 5 13 | })).toEqual('https://insight.bitpay.com/api/addrs/152f1muMCNa7goXYhYAQC61hxEgGacmncB,12ni9ddt4WHfEQriVN7DajDBd1JbKL9yUZ,1FS4FF2SYdHf3PGfSbjpdYcUiUYxiVLy73/txs?from=5&to=15'); 14 | }); 15 | 16 | it('should find address input', () => { 17 | let index = transactionService.addressIndex( 18 | ['ad1','ad2','ad3','ad4'] , 19 | ['ad0','ad3'] 20 | ); 21 | expect(index).toEqual(1); 22 | }); 23 | 24 | it('should parse transactions correctly', () => { 25 | let txs = transactionService.parseTransactions(['19L9LhCArs9SUNu3ZzaqJ1ys3dGgvJmi5T'], data); 26 | let incomming = [false, true, false, true, false, false, true, true, false, true]; 27 | for (let i = 0; i < incomming.length; i++) { 28 | expect(txs[i].incomming).toBe(incomming[i]); 29 | } 30 | }); 31 | 32 | }); -------------------------------------------------------------------------------- /src/providers/transaction/insight-transaction-service/insight-transaction-service.ts: -------------------------------------------------------------------------------- 1 | import { TransactionService } from './../../../api/transaction-service'; 2 | import { TransactionFilter } from './../../../api/transaction-filter'; 3 | import { CryptocurrencyService, TESTNET, BITCOIN } from './../../index'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { Transaction } from './../../../api/transaction'; 6 | 7 | export class InsightTransactionService implements TransactionService { 8 | 9 | constructor( 10 | protected http:HttpClient, 11 | protected serviceUrl:string, 12 | protected cryptocurrency:string = BITCOIN) {} 13 | 14 | findTransactions(filter:TransactionFilter) : Promise { 15 | return new Promise((resolve, reject) => { 16 | this.http.get(this.buildUrl(filter)) 17 | .subscribe(response => { 18 | resolve(this.parseTransactions(filter.addresses, response)); 19 | },() => { reject(); }); 20 | }); 21 | } 22 | 23 | parseTransactionInputs(vins:any[], addresses:string[]) { 24 | if (vins && vins.length > 0) { 25 | for (let vin of vins) { 26 | let addressIndex = addresses.indexOf(vin.addr); 27 | if (addressIndex >= 0) { 28 | // TODO: same address in vin multiple times? 29 | return { 30 | amount : parseFloat(vin.value) , 31 | incomming : false , 32 | address : addresses[addressIndex] 33 | }; 34 | } 35 | } 36 | } 37 | return null; 38 | } 39 | 40 | parseTransactionOutputs(vouts:any[], addresses:string[]) { 41 | if (vouts && vouts.length > 0) { 42 | for (let vout of vouts) { 43 | if (vout.scriptPubKey && (vout.scriptPubKey.type == 'pubkeyhash' || vout.scriptPubKey.type == 'scripthash')) { 44 | let index = this.addressIndex(vout.scriptPubKey.addresses, addresses); 45 | 46 | if (index >= 0) { 47 | // TODO: same address in vout multiple times? 48 | // http://bitcoin.stackexchange.com/questions/4475/can-the-same-target-address-appear-more-than-once-on-transaction-output 49 | 50 | return { 51 | amount : parseFloat(vout.value) , 52 | incomming : true , 53 | address : addresses[index] 54 | }; 55 | } 56 | } 57 | } 58 | } 59 | 60 | return null; 61 | } 62 | 63 | parseTransactions(addresses:string[], json:any) : Transaction[] { 64 | let output = []; 65 | let items = []; 66 | 67 | if (json.items && json.items.length) { 68 | items = json.items; 69 | } else { 70 | items.push(json); 71 | } 72 | 73 | for (let item of items) { 74 | 75 | let tx:any = this.parseTransactionInputs(item.vin, addresses); 76 | if (!tx) { 77 | tx = this.parseTransactionOutputs(item.vout, addresses); 78 | } 79 | 80 | if (tx) { 81 | tx.currency = "BTC"; 82 | tx._id = item.txid; 83 | tx.confirmations = parseInt(item.confirmations); 84 | tx.timestamp = parseInt(item.time); 85 | output.push(tx); 86 | } 87 | } 88 | 89 | return output; 90 | } 91 | 92 | /** 93 | * returns index of addressesAsSearchInput array, which is found inside 94 | * addressesToSearchFor array 95 | */ 96 | addressIndex(addressesToSearchFor:string[], addressesAsSearchInput:string[]) : number { 97 | for (let i = 0; i < addressesAsSearchInput.length; i++) { 98 | if (addressesToSearchFor.indexOf(addressesAsSearchInput[i]) >= 0) { 99 | return i; 100 | } 101 | } 102 | return -1; 103 | } 104 | 105 | buildUrl(filter:any = {}) : string { 106 | let url = ""; 107 | 108 | if (filter.addresses && filter.addresses.length > 0) { 109 | url = this.serviceUrl + "/api"; 110 | 111 | if (filter.txid) { 112 | url += '/tx/' + filter.txid; 113 | } else { 114 | url += '/addrs/' + filter.addresses.join(',') + '/txs'; 115 | } 116 | } 117 | 118 | if (filter.from >= 0 && filter.to > 0) { 119 | url += '?from=' + filter.from + '&to=' + filter.to; 120 | } 121 | 122 | return url; 123 | } 124 | 125 | } -------------------------------------------------------------------------------- /src/providers/transaction/transaction-service-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { TransactionService } from './../../api/transaction-service'; 2 | import { TransactionFilter } from './../../api/transaction-filter'; 3 | import { CryptocurrencyService, BlockchainInfoService, InsightTransactionService, BITCOIN, TESTNET } from './../index'; 4 | import { Injectable } from '@angular/core'; 5 | import { HttpClient } from '@angular/common/http'; 6 | import { Transaction } from './../../api/transaction'; 7 | 8 | @Injectable() 9 | export class TransactionServiceWrapper implements TransactionService { 10 | 11 | protected instances:any = {}; 12 | 13 | constructor(protected httpClient:HttpClient, protected cryptocurrencyService:CryptocurrencyService) {} 14 | 15 | init() { 16 | if (!this.instances[BITCOIN]) { 17 | this.instances[BITCOIN] = []; 18 | this.instances[TESTNET] = []; 19 | this.instances[BITCOIN].push(new BlockchainInfoService(this.httpClient)); 20 | this.instances[BITCOIN].push(new InsightTransactionService(this.httpClient, 'https://insight.bitpay.com')); 21 | this.instances[TESTNET].push(new InsightTransactionService(this.httpClient, 'https://test-insight.bitpay.com', TESTNET)); 22 | } 23 | } 24 | 25 | findTransactions(filter: TransactionFilter): Promise { 26 | this.init(); 27 | return this.triggerRandomInstance(filter, this.checkCryptocurrency(filter)); 28 | } 29 | 30 | triggerRandomInstance(filter:TransactionFilter, cryptocurrency:string) { 31 | return new Promise ((resolve, reject) => { 32 | this.getInstance(cryptocurrency).findTransactions(filter) 33 | .then(result => { 34 | resolve(result); 35 | }).catch(() => { 36 | resolve(this.triggerRandomInstance(filter, cryptocurrency)); 37 | }); 38 | }); 39 | } 40 | 41 | getInstance(cryptocurrency:string) : TransactionService { 42 | let index = Math.floor(Math.random() * this.instances[cryptocurrency].length); 43 | return this.instances[cryptocurrency][index]; 44 | } 45 | 46 | checkCryptocurrency(filter: TransactionFilter) : string { 47 | if (filter.addresses && filter.addresses.length > 0) { 48 | return this.cryptocurrencyService.parseInput(filter.addresses[0]).currency; 49 | } else { 50 | return BITCOIN; 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/providers/transaction/transaction-storage-service.ts: -------------------------------------------------------------------------------- 1 | import { TransactionFilter } from './../../api/transaction-filter'; 2 | import { Repository } from './../repository'; 3 | import { Transaction } from './../../api/transaction'; 4 | import { Injectable } from '@angular/core'; 5 | 6 | @Injectable() 7 | export class TransactionStorageService { 8 | 9 | constructor( 10 | protected repository: Repository 11 | ) {} 12 | 13 | storeTransaction(transaction:Transaction) : Promise { 14 | return new Promise((resolve, reject) => { 15 | this.repository.addOrEditDocument(transaction) 16 | .then(() => { resolve(transaction); }) 17 | .catch(() => { reject(); }); 18 | }); 19 | } 20 | 21 | retrieveTransaction(txid: string) { 22 | return this.repository.findById(txid); 23 | } 24 | 25 | retrieveTransactions(transactionFilter: TransactionFilter) : Promise { 26 | let selector:any = null, limit = true; 27 | 28 | if (transactionFilter.startTime > 0 && transactionFilter.endTime > 0) { 29 | limit = false; 30 | selector = { 31 | '$and' : [ 32 | { 'timestamp' : { '$gte':transactionFilter.startTime } } , 33 | { 'timestamp' : { '$lte':transactionFilter.endTime } } 34 | ] 35 | }; 36 | } else { 37 | selector = { 38 | '$and' : [ 39 | { 'timestamp' : { '$gt':null } } , 40 | ] 41 | }; 42 | } 43 | 44 | if (transactionFilter.account && /static-address/.test(transactionFilter.account.type)) { 45 | selector.$and.push({ 'address' : { '$eq':transactionFilter.account.data }}); 46 | } else if (transactionFilter.account && /pub-key/.test(transactionFilter.account.type)) { 47 | selector.$and.push({ 'account' : { '$eq': transactionFilter.account._id }}); 48 | } else if (transactionFilter.addresses) { 49 | selector.$and.push({ 'address' : { '$in':transactionFilter.addresses }}); 50 | } 51 | 52 | return new Promise((resolve, reject) => { 53 | let query = { 54 | selector : selector, 55 | sort : [{ timestamp : 'desc' }] 56 | }; 57 | 58 | if (limit) { 59 | query['limit'] = transactionFilter.to - transactionFilter.from; 60 | query['skip'] = transactionFilter.from > 0 ? transactionFilter.from : 0; 61 | } 62 | 63 | resolve(this.repository.findDocuments(query)); 64 | }); 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | // tick this to make the cache invalidate and update 2 | const CACHE_VERSION = 1; 3 | const CURRENT_CACHES = { 4 | 'read-through': 'read-through-cache-v' + CACHE_VERSION 5 | }; 6 | 7 | self.addEventListener('activate', (event) => { 8 | // Delete all caches that aren't named in CURRENT_CACHES. 9 | // While there is only one cache in this example, the same logic will handle the case where 10 | // there are multiple versioned caches. 11 | const expectedCacheNames = Object.keys(CURRENT_CACHES).map((key) => { 12 | return CURRENT_CACHES[key]; 13 | }); 14 | 15 | event.waitUntil( 16 | caches.keys().then((cacheNames) => { 17 | return Promise.all( 18 | cacheNames.map((cacheName) => { 19 | if (expectedCacheNames.indexOf(cacheName) === -1) { 20 | // If this cache name isn't present in the array of "expected" cache names, then delete it. 21 | console.log('Deleting out of date cache:', cacheName); 22 | return caches.delete(cacheName); 23 | } 24 | }) 25 | ); 26 | }) 27 | ); 28 | }); 29 | 30 | // This sample illustrates an aggressive approach to caching, in which every valid response is 31 | // cached and every request is first checked against the cache. 32 | // This may not be an appropriate approach if your web application makes requests for 33 | // arbitrary URLs as part of its normal operation (e.g. a RSS client or a news aggregator), 34 | // as the cache could end up containing large responses that might not end up ever being accessed. 35 | // Other approaches, like selectively caching based on response headers or only caching 36 | // responses served from a specific domain, might be more appropriate for those use cases. 37 | self.addEventListener('fetch', (event) => { 38 | 39 | event.respondWith( 40 | caches.open(CURRENT_CACHES['read-through']).then((cache) => { 41 | return cache.match(event.request).then((response) => { 42 | if (response) { 43 | // If there is an entry in the cache for event.request, then response will be defined 44 | // and we can just return it. 45 | 46 | return response; 47 | } 48 | 49 | // Otherwise, if there is no entry in the cache for event.request, response will be 50 | // undefined, and we need to fetch() the resource. 51 | console.log(' No response for %s found in cache. ' + 52 | 'About to fetch from network...', event.request.url); 53 | 54 | // We call .clone() on the request since we might use it in the call to cache.put() later on. 55 | // Both fetch() and cache.put() "consume" the request, so we need to make a copy. 56 | // (see https://fetch.spec.whatwg.org/#dom-request-clone) 57 | return fetch(event.request.clone()).then((response) => { 58 | 59 | // Optional: add in extra conditions here, e.g. response.type == 'basic' to only cache 60 | // responses from the same domain. See https://fetch.spec.whatwg.org/#concept-response-type 61 | if (response.status < 400 && response.type === 'basic') { 62 | // We need to call .clone() on the response object to save a copy of it to the cache. 63 | // (https://fetch.spec.whatwg.org/#dom-request-clone) 64 | cache.put(event.request, response.clone()); 65 | } 66 | 67 | // Return the original response object, which will be used to fulfill the resource request. 68 | return response; 69 | }); 70 | }).catch((error) => { 71 | // This catch() will handle exceptions that arise from the match() or fetch() operations. 72 | // Note that a HTTP error response (e.g. 404) will NOT trigger an exception. 73 | // It will return a normal response object that has the appropriate error code set. 74 | console.error(' Read-through caching failed:', error); 75 | 76 | throw error; 77 | }); 78 | }) 79 | ); 80 | }); -------------------------------------------------------------------------------- /src/theme/_font.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Ubuntu"; 3 | font-style: normal; 4 | font-weight: normal; 5 | src: url("#{$font-path}/Ubuntu-R.ttf") format("truetype"); 6 | } 7 | @font-face { 8 | font-family: "Ubuntu"; 9 | font-style: normal; 10 | font-weight: 300; 11 | src: url("#{$font-path}/Ubuntu-L.ttf") format("truetype"); 12 | } 13 | @font-face { 14 | font-family: "Ubuntu"; 15 | font-style: normal; 16 | font-weight: 500; 17 | src: url("#{$font-path}/Ubuntu-M.ttf") format("truetype"); 18 | } 19 | @font-face { 20 | font-family: "Ubuntu"; 21 | font-style: normal; 22 | font-weight: 700; 23 | src: url("#{$font-path}/Ubuntu-B.ttf") format("truetype"); 24 | } 25 | @font-face { 26 | font-family: "Ubuntu Condensed"; 27 | font-style: normal; 28 | font-weight: normal; 29 | src: url("#{$font-path}/Ubuntu-C.ttf") format("truetype"); 30 | } -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/v2/theming/ 3 | $font-path: "../assets/fonts"; 4 | 5 | @import "ionic.globals"; 6 | 7 | 8 | // Shared Variables 9 | // -------------------------------------------------- 10 | // To customize the look and feel of this app, you can override 11 | // the Sass variables found in Ionic's source scss files. 12 | // To view all the possible Ionic variables, see: 13 | // http://ionicframework.com/docs/v2/theming/overriding-ionic-variables/ 14 | 15 | $font-family-base: 'Ubuntu', sans-serif; 16 | 17 | $button-border-radius: 6px; 18 | 19 | // font sizes 20 | $font-size-base: 1.5rem; // 21 | $font-size-small: 2.0rem; // 22 | $font-size-x-small: 2.5rem; // 23 | $font-size-medium: 3.0rem; // 24 | $font-size-x-medium: 3.5rem; // 25 | $font-size-large: 4.0rem; // 26 | $font-size-x-large: 4.5rem; // 27 | 28 | // sizes 29 | $bitpocket-xs-min: 360px; 30 | $bitpocket-sm-min: 576px; 31 | $bitpocket-md-min: 768px; 32 | 33 | 34 | // Named Color Variables 35 | // -------------------------------------------------- 36 | // Named colors makes it easy to reuse colors on various components. 37 | // It's highly recommended to change the default colors 38 | // to match your app's branding. Ionic uses a Sass map of 39 | // colors so you can add, rename and remove colors as needed. 40 | // The "primary" color is the only required color in the map. 41 | 42 | $basic-color: rgba(106,109,122,1); 43 | $basic-color-transparency: rgba(255,255,255,0.2); 44 | 45 | $colors: ( 46 | primary: $basic-color, 47 | secondary: #32db64, 48 | danger: #f53d3d, 49 | light: #f4f4f4, 50 | dark: #222, 51 | menu-item: ( 52 | base: $basic-color, 53 | contrast: $basic-color 54 | ), 55 | menutoggle: ( 56 | base: $basic-color, 57 | contrast: $basic-color 58 | ), 59 | number: ( 60 | base: $basic-color, 61 | contrast: white 62 | ), 63 | number-decimals: ( 64 | base: $basic-color-transparency, 65 | contrast: white 66 | ), 67 | number-backspaceinput: ( 68 | base: $basic-color-transparency, 69 | contrast: white 70 | ), 71 | numpad-reset: ( 72 | base: $basic-color-transparency, 73 | contrast: white 74 | ), 75 | numpad-request: ( 76 | base: white, 77 | contrast: $basic-color 78 | ), 79 | show-history: ( 80 | base: white, 81 | contrast: $basic-color 82 | ), 83 | checkbox-style: ( 84 | base: white, 85 | contrast: $basic-color 86 | ) 87 | ); 88 | 89 | $text-color: map-get($colors , primary); 90 | 91 | 92 | // App iOS Variables 93 | // -------------------------------------------------- 94 | // iOS only Sass variables can go here 95 | 96 | $toolbar-ios-title-text-color: map-get($colors , primary); 97 | 98 | 99 | // App Material Design Variables 100 | // -------------------------------------------------- 101 | // Material Design only Sass variables can go here 102 | 103 | $font-family-md-base: $font-family-base; 104 | 105 | $toolbar-md-button-color: map-get($colors , primary); 106 | $toolbar-md-title-text-color: map-get($colors , primary); 107 | 108 | $list-md-header-color: white; 109 | $list-md-header-font-size: $font-size-small; 110 | $list-md-text-color: map-get($colors , primary); 111 | 112 | $card-md-text-color: map-get($colors , primary); 113 | $card-md-title-text-color: map-get($colors , primary); 114 | 115 | $checkbox-md-icon-background-color-on: rgba(map-get($colors , primary),0.5); 116 | $checkbox-md-icon-background-color-off: rgba(map-get($colors , primary),0.5); 117 | $checkbox-md-icon-border-color-on: map-get($colors , primary); 118 | $checkbox-md-icon-border-color-off: map-get($colors , primary); 119 | 120 | // App Windows Variables 121 | // -------------------------------------------------- 122 | // Windows only Sass variables can go here 123 | 124 | 125 | 126 | 127 | // App Theme 128 | // -------------------------------------------------- 129 | // Ionic apps can have different themes applied, which can 130 | // then be future customized. This import comes last 131 | // so that the above variables are used and Ionic's 132 | // default are overridden. 133 | 134 | @import "ionic.theme.default"; 135 | 136 | 137 | // Ionicons 138 | // -------------------------------------------------- 139 | // The premium icon font for Ionic. For more info, please see: 140 | // http://ionicframework.com/docs/v2/ionicons/ 141 | 142 | @import "ionic.ionicons"; 143 | 144 | 145 | // Fonts 146 | // -------------------------------------------------- 147 | 148 | @import "font"; 149 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "dom", 9 | "es2015" 10 | ], 11 | "module": "es2015", 12 | "moduleResolution": "node", 13 | "sourceMap": true, 14 | "target": "es5" 15 | }, 16 | "include": [ 17 | "src/**/*.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules", 21 | "src/**/*.spec.ts" 22 | ], 23 | "compileOnSave": false, 24 | "atom": { 25 | "rewriteTsconfig": false 26 | } 27 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-duplicate-variable": true, 4 | "no-unused-variable": [ 5 | true 6 | ] 7 | }, 8 | "rulesDirectory": [ 9 | "node_modules/tslint-eslint-rules/dist/rules" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | BitPocket 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | --------------------------------------------------------------------------------