├── .watchmanconfig ├── .gitattributes ├── android ├── local.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ └── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ ├── assets │ │ │ │ └── fonts │ │ │ │ │ ├── Entypo.ttf │ │ │ │ │ ├── Zocial.ttf │ │ │ │ │ ├── Feather.ttf │ │ │ │ │ ├── Fontisto.ttf │ │ │ │ │ ├── Ionicons.ttf │ │ │ │ │ ├── Octicons.ttf │ │ │ │ │ ├── AntDesign.ttf │ │ │ │ │ ├── EvilIcons.ttf │ │ │ │ │ ├── FontAwesome.ttf │ │ │ │ │ ├── Foundation.ttf │ │ │ │ │ ├── MaterialIcons.ttf │ │ │ │ │ ├── SimpleLineIcons.ttf │ │ │ │ │ ├── FontAwesome5_Solid.ttf │ │ │ │ │ ├── FontAwesome5_Brands.ttf │ │ │ │ │ ├── FontAwesome5_Regular.ttf │ │ │ │ │ └── MaterialCommunityIcons.ttf │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── partnr │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MainApplication.java │ │ │ └── AndroidManifest.xml │ │ └── debug │ │ │ └── AndroidManifest.xml │ ├── .project │ ├── build_defs.bzl │ ├── proguard-rules.pro │ └── BUCK ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── keystores │ ├── debug.keystore.properties │ └── BUCK ├── .project ├── gradle.properties ├── build.gradle ├── settings.gradle └── gradlew.bat ├── app.json ├── src ├── common │ ├── constants │ │ ├── recents.js │ │ ├── wallet.js │ │ ├── storage.js │ │ ├── index.js │ │ └── url.js │ ├── styles │ │ ├── index.js │ │ ├── measures.js │ │ └── colors.js │ ├── utils │ │ ├── index.js │ │ ├── image.js │ │ ├── __tests__ │ │ │ ├── image.js │ │ │ └── shims.js │ │ ├── transaction.js │ │ ├── shims.js │ │ └── wallet.js │ ├── stores │ │ ├── index.js │ │ ├── recents.js │ │ ├── prices.js │ │ ├── wallets.js │ │ ├── wallet.js │ │ └── __tests__ │ │ │ ├── recents.js │ │ │ ├── wallets.js │ │ │ └── prices.js │ ├── services │ │ ├── index.js │ │ ├── api.js │ │ ├── storage.js │ │ ├── recents.js │ │ ├── wallets.js │ │ ├── transactions.js │ │ └── __tests__ │ │ │ ├── api.js │ │ │ └── transaction.js │ └── actions │ │ ├── index.js │ │ ├── prices.js │ │ ├── __tests__ │ │ ├── price.js │ │ ├── general.js │ │ ├── recents.js │ │ ├── transactions.js │ │ └── wallets.js │ │ ├── recents.js │ │ ├── transactions.js │ │ ├── general.js │ │ └── wallets.js ├── assets │ └── ethereum-logo.png ├── setupTests.js ├── components │ ├── widgets │ │ ├── index.js │ │ ├── TextBullet │ │ │ ├── test.js │ │ │ └── index.js │ │ ├── HeaderIcon │ │ │ ├── test.js │ │ │ └── index.js │ │ ├── InputWithIcon │ │ │ ├── test.js │ │ │ └── index.js │ │ ├── Button │ │ │ ├── test.js │ │ │ └── index.js │ │ ├── TabView │ │ │ ├── index.js │ │ │ ├── TabBarIcon.js │ │ │ └── TabBar.js │ │ ├── Icon │ │ │ ├── test.js │ │ │ └── index.js │ │ ├── Calculator │ │ │ ├── index.js │ │ │ └── Panel.js │ │ ├── NumberGrid │ │ │ └── index.js │ │ └── Camera │ │ │ └── index.js │ └── views │ │ ├── SelectDestination │ │ ├── NoRecents.js │ │ ├── index.js │ │ └── Recents.js │ │ ├── Settings │ │ ├── ListItem.js │ │ └── index.js │ │ ├── ChangeCurrency │ │ ├── ListItem.js │ │ └── index.js │ │ ├── WalletSettings │ │ ├── ListItem.js │ │ └── index.js │ │ ├── WalletExtract │ │ ├── NoTransactions.js │ │ ├── Balance.js │ │ ├── index.js │ │ └── TransactionCard.js │ │ ├── index.js │ │ ├── SendCoins │ │ └── index.js │ │ ├── WalletDetails │ │ └── index.js │ │ ├── ConfirmTransaction │ │ ├── ErrorMessage.js │ │ ├── SuccessMessage.js │ │ └── index.js │ │ ├── ShowPrivateKey │ │ └── index.js │ │ ├── LoadWallet │ │ └── index.js │ │ ├── WalletsOverview │ │ ├── NoWallets.js │ │ ├── TotalBalance.js │ │ ├── index.js │ │ └── WalletCard.js │ │ ├── NewWallet │ │ └── index.js │ │ ├── CreateWallet │ │ └── index.js │ │ ├── ConfirmMnemonics │ │ ├── index.js │ │ └── ConfirmBox.js │ │ ├── ReceiveCoins │ │ └── index.js │ │ ├── CreateMnemonics │ │ └── index.js │ │ ├── NewWalletName │ │ └── index.js │ │ ├── LoadPrivateKey │ │ └── index.js │ │ └── LoadMnemonics │ │ └── index.js ├── index.js ├── folderStructure.txt └── Router.js ├── ios ├── Partnr │ ├── Images.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ ├── AppDelegate.m │ ├── Info.plist │ └── Base.lproj │ │ └── LaunchScreen.xib ├── PartnrTests │ ├── Info.plist │ └── PartnrTests.m ├── Partnr-tvOSTests │ └── Info.plist └── Partnr-tvOS │ └── Info.plist ├── tsconfig.json ├── .buckconfig ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── rn-cli.config.js ├── __mocks__ ├── react-native-snackbar.js └── react-native-camera.js ├── config └── jest.config.js ├── metro.config.js ├── index.js ├── babel.config.js ├── contracts ├── Migrations.sol ├── MultiSigWalletFactory.sol ├── Factory.sol ├── Escrow.sol └── Votes.sol ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .gitignore ├── .circleci └── config.yml ├── .flowconfig ├── package.json └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /android/local.properties: -------------------------------------------------------------------------------- 1 | sdk.dir = /Users/carlos/Library/Android/sdk 2 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Partnr", 3 | "displayName": "Partnr" 4 | } -------------------------------------------------------------------------------- /src/common/constants/recents.js: -------------------------------------------------------------------------------- 1 | export const STORAGE_KEY = '@Storage/Recents'; -------------------------------------------------------------------------------- /src/common/constants/wallet.js: -------------------------------------------------------------------------------- 1 | export const STORAGE_KEY = '@Storage/Wallet'; -------------------------------------------------------------------------------- /src/assets/ethereum-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/src/assets/ethereum-logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Partnr 3 | 4 | -------------------------------------------------------------------------------- /ios/Partnr/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "allowJs": true 5 | } 6 | } -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Entypo.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Zocial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Zocial.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Feather.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Fontisto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Fontisto.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Ionicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Octicons.ttf -------------------------------------------------------------------------------- /src/common/styles/index.js: -------------------------------------------------------------------------------- 1 | import * as colors from './colors'; 2 | import * as measures from './measures'; 3 | 4 | export { colors, measures }; -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/AntDesign.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/AntDesign.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/EvilIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/EvilIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Foundation.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/Foundation.ttf -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | configure({ adapter: new Adapter() }); -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/SimpleLineIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/SimpleLineIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/partnr/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /src/common/constants/storage.js: -------------------------------------------------------------------------------- 1 | export const CONFIG = { 2 | sharedPreferencesName: '@BitWalletPrefs-712638173', 3 | keychainService: '@BitWalletKeychain-513961259' 4 | }; -------------------------------------------------------------------------------- /src/common/utils/index.js: -------------------------------------------------------------------------------- 1 | import * as Image from './image'; 2 | import * as Transaction from './transaction'; 3 | import * as Wallet from './wallet'; 4 | 5 | export { Image, Transaction, Wallet }; -------------------------------------------------------------------------------- /android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = "debug", 3 | properties = "debug.keystore.properties", 4 | store = "debug.keystore", 5 | visibility = [ 6 | "PUBLIC", 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /rn-cli.config.js: -------------------------------------------------------------------------------- 1 | const blacklist = require('metro/src/blacklist') 2 | module.exports = { 3 | getBlacklistRE () { 4 | return blacklist([/react-native\/local-cli\/core\/__fixtures__.*/]) 5 | }, 6 | } -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const MultisigWalletFactory = artifacts.require('MultiSigWalletWithDailyLimitFactory.sol') 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(MultisigWalletFactory) 5 | } 6 | -------------------------------------------------------------------------------- /src/common/stores/index.js: -------------------------------------------------------------------------------- 1 | import prices from './prices'; 2 | import recents from './recents'; 3 | import wallet from './wallet'; 4 | import wallets from './wallets'; 5 | 6 | export { prices, recents, wallet, wallets }; 7 | -------------------------------------------------------------------------------- /src/common/constants/index.js: -------------------------------------------------------------------------------- 1 | import * as Recents from './recents'; 2 | import * as Storage from './storage'; 3 | import * as Url from './url'; 4 | import * as Wallet from './wallet'; 5 | 6 | export { Recents, Storage, Url, Wallet }; 7 | -------------------------------------------------------------------------------- /src/common/constants/url.js: -------------------------------------------------------------------------------- 1 | export const CRYPTO_COMPARE = 'https://min-api.cryptocompare.com'; 2 | 3 | export const ETHERSCAN = (process.env.NODE_ENV === 'production') ? 'https://api.etherscan.io/api' : 'https://rinkeby.etherscan.io/api'; -------------------------------------------------------------------------------- /__mocks__/react-native-snackbar.js: -------------------------------------------------------------------------------- 1 | export const LENGTH_LONG = 'LENGTH_LONG'; 2 | export const LENGTH_INDEFINITE = 'LENGTH_INDEFINITE'; 3 | export const LENGTH_SHORT = 'LENGTH_SHORT'; 4 | 5 | 6 | export function show({ title, duration }) {} -------------------------------------------------------------------------------- /src/common/utils/image.js: -------------------------------------------------------------------------------- 1 | import Identicon from 'identicon.js'; 2 | 3 | export function generateAvatar(hash) { 4 | const options = { size: 500 }; 5 | const avatar = new Identicon(hash, options).toString(); 6 | return `data:image/png;base64,${avatar}`; 7 | } -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /config/jest.config.js: -------------------------------------------------------------------------------- 1 | require("react-native-mock-render/mock"); 2 | const enzyme = require('enzyme'); 3 | const Adapter = require('enzyme-adapter-react-16'); 4 | const { JSDOM } = require("jsdom"); 5 | 6 | global = new JSDOM(""); 7 | enzyme.configure({ adapter: new Adapter() }); -------------------------------------------------------------------------------- /src/common/services/index.js: -------------------------------------------------------------------------------- 1 | import * as Api from './api'; 2 | import * as Recents from './recents'; 3 | import * as Storage from './storage'; 4 | import * as Transactions from './transactions'; 5 | import * as Wallets from './wallets'; 6 | 7 | export { Api, Recents, Storage, Transactions, Wallets }; -------------------------------------------------------------------------------- /src/common/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as General from './general'; 2 | import * as Prices from './prices'; 3 | import * as Recents from './recents'; 4 | import * as Transactions from './transactions'; 5 | import * as Wallets from './wallets'; 6 | 7 | export { General, Prices, Recents, Transactions, Wallets }; -------------------------------------------------------------------------------- /src/components/widgets/index.js: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | export * from './Calculator'; 3 | export * from './Camera'; 4 | export * from './HeaderIcon'; 5 | export * from './Icon'; 6 | export * from './InputWithIcon'; 7 | export * from './NumberGrid'; 8 | export * from './TabView'; 9 | export * from './TextBullet'; -------------------------------------------------------------------------------- /__mocks__/react-native-camera.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | 4 | export class RNCamera extends React.Component { 5 | 6 | static Constants = { 7 | BarCodeType: 'BarCodeType' 8 | }; 9 | 10 | render() { 11 | return ( 12 | 13 | ); 14 | } 15 | } -------------------------------------------------------------------------------- /src/common/utils/__tests__/image.js: -------------------------------------------------------------------------------- 1 | import * as ImageUtils from '../image'; 2 | 3 | describe('Image utils', () => { 4 | 5 | it('`generateAvatar` function should return a base64 encoded SVG', () => { 6 | const result = ImageUtils.generateAvatar('123456789012345'); 7 | expect(typeof result).toEqual('string'); 8 | }); 9 | }); -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/common/styles/measures.js: -------------------------------------------------------------------------------- 1 | export const defaultMargin = 8; 2 | export const defaultPadding = 8; 3 | 4 | export const fontSizeMedium = 18; 5 | export const fontSizeSmall = fontSizeMedium * 0.6; 6 | export const fontSizeLarge = fontSizeMedium * 1.5; 7 | 8 | export const iconSizeMedium = 24; 9 | export const iconSizeSmall = iconSizeMedium * 0.6; 10 | export const iconSizeLarge = iconSizeMedium * 1.5; -------------------------------------------------------------------------------- /src/common/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Url } from '@common/constants'; 3 | 4 | export function getPrice() { 5 | return axios.get(`${Url.CRYPTO_COMPARE}/data/price?fsym=ETH&tsyms=USD,EUR,JMD`); 6 | } 7 | 8 | export function getHistory(address) { 9 | return axios.get(`${Url.ETHERSCAN}?module=account&action=txlist&address=${address}&startblock=0&endblock=99999999&sort=asc`); 10 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/common/utils/__tests__/shims.js: -------------------------------------------------------------------------------- 1 | import '../shims'; 2 | 3 | describe('Shims', () => { 4 | 5 | it('should add the method `slice` to `Uint8Array` prototype', () => { 6 | expect(Uint8Array.prototype.slice).toBeInstanceOf(Function); 7 | }); 8 | 9 | it('should add the method `equals` to `Array` prototype', () => { 10 | expect(Array.prototype.equals).toBeInstanceOf(Function); 11 | }); 12 | }); -------------------------------------------------------------------------------- /src/common/actions/prices.js: -------------------------------------------------------------------------------- 1 | import { Api as ApiService } from '@common/services'; 2 | import { prices as PricesStore } from '@common/stores'; 3 | 4 | export async function getPrice() { 5 | PricesStore.isLoading(true); 6 | const { data } = await ApiService.getPrice(); 7 | PricesStore.setUSDRate(data.USD); 8 | PricesStore.setEURRate(data.EUR); 9 | PricesStore.setJMDRate(data.JMD); 10 | PricesStore.isLoading(false); 11 | } -------------------------------------------------------------------------------- /src/components/widgets/TextBullet/test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native'; 3 | import { shallow } from 'enzyme'; 4 | import { TextBullet } from './index'; 5 | 6 | describe('', () => { 7 | 8 | it('should have a label `label`', () => { 9 | const wrapper = shallow(label); 10 | expect(wrapper.find(Text).children().text()).toEqual('label'); 11 | }); 12 | }); -------------------------------------------------------------------------------- /android/app/src/main/java/com/partnr/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.partnr; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. 9 | * This is used to schedule rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "Partnr"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry, YellowBox } from 'react-native'; 2 | import App from './src'; 3 | import './src/common/utils/shims'; 4 | import { name as appName } from './app.json'; 5 | 6 | YellowBox.ignoreWarnings([ 7 | 'Warning: componentWillMount', 8 | 'Warning: componentWillReceiveProps', 9 | 'Module RCTImageLoader', 10 | 'Class RCTCxxModule was not exported', 11 | 'Remote debugger' 12 | ]); 13 | 14 | AppRegistry.registerComponent(appName, () => App); 15 | -------------------------------------------------------------------------------- /ios/Partnr/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (nonatomic, strong) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Partnr/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/widgets/HeaderIcon/test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { HeaderIcon } from './index'; 4 | 5 | describe('', () => { 6 | 7 | it('should call the `onPress` function when clicked', () => { 8 | const onPress = jest.fn(); 9 | const wrapper = shallow(); 10 | wrapper.simulate('press'); 11 | expect(onPress).toBeCalled(); 12 | }); 13 | }); -------------------------------------------------------------------------------- /android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Partnr 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/common/services/storage.js: -------------------------------------------------------------------------------- 1 | import SensitiveInfoStorage from 'react-native-sensitive-info'; 2 | import { Storage } from '@common/constants'; 3 | 4 | export function getItem(key) { 5 | return SensitiveInfoStorage.getItem(key, Storage.CONFIG).then(item => item || ''); 6 | } 7 | 8 | export function setItem(key, value) { 9 | return SensitiveInfoStorage.setItem(key, value || '', Storage.CONFIG); 10 | } 11 | 12 | export function deleteItem(key) { 13 | return SensitiveInfoStorage.deleteItem(key, Storage.CONFIG); 14 | } -------------------------------------------------------------------------------- /src/common/actions/__tests__/price.js: -------------------------------------------------------------------------------- 1 | import * as Action from '../prices'; 2 | import { prices as PricesStore } from '@common/stores'; 3 | 4 | describe('PriceActions', () => { 5 | 6 | it('should add the prices to the store', async function() { 7 | try { 8 | await Action.getPrice(); 9 | expect(PricesStore.usd).toBeGreaterThan(0); 10 | expect(PricesStore.eur).toBeGreaterThan(0); 11 | expect(PricesStore.jmd).toBeGreaterThan(0); 12 | } catch (e) { 13 | fail(e); 14 | } 15 | }); 16 | }); -------------------------------------------------------------------------------- /src/common/services/recents.js: -------------------------------------------------------------------------------- 1 | import * as StorageService from './storage'; 2 | import { Recents } from '@common/constants'; 3 | 4 | export async function loadRecentAddresses() { 5 | const recents = await StorageService.getItem(Recents.STORAGE_KEY); 6 | return recents ? JSON.parse(recents) : []; 7 | } 8 | 9 | export function saveRecentAddresses(recents) { 10 | return StorageService.setItem(Recents.STORAGE_KEY, JSON.stringify(recents)); 11 | } 12 | 13 | export function removeRecentAddresses() { 14 | return StorageService.deleteItem(Recents.STORAGE_KEY); 15 | } 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | plugins: [ 4 | [ 5 | 'module-resolver', 6 | { 7 | alias: { 8 | '@assets': './assets', 9 | '@common': './src/common', 10 | '@components': './src/components' 11 | } 12 | } 13 | ], 14 | ['@babel/plugin-proposal-decorators', { legacy: true }], 15 | [ 16 | '@babel/plugin-transform-runtime', 17 | { 18 | helpers: true, 19 | regenerator: false 20 | } 21 | ] 22 | ] 23 | }; 24 | -------------------------------------------------------------------------------- /src/common/actions/__tests__/general.js: -------------------------------------------------------------------------------- 1 | import * as Action from '../general'; 2 | 3 | describe('GeneralActions', () => { 4 | 5 | it('should trigger a call for a notification in the view', async function() { 6 | const notificationDriverMock = { show: jest.fn(), LENGTH_SHORT: 1000 }; 7 | try { 8 | await Action.notify('Test notification', 'short', notificationDriverMock); 9 | expect(notificationDriverMock.show).toBeCalledWith({ title: 'Test notification', duration: 1000 }); 10 | } catch (e) { 11 | fail(e); 12 | } 13 | }); 14 | }); -------------------------------------------------------------------------------- /src/components/widgets/InputWithIcon/test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { InputWithIcon } from './index'; 4 | 5 | describe.skip('', () => { 6 | 7 | it('should call the `onPressIcon` function when text was typed', () => { 8 | const onPressIcon = jest.fn(); 9 | const wrapper = shallow(); 10 | 11 | wrapper.childAt(0).simulate('changeText', 'abc'); 12 | wrapper.childAt(1).simulate('press'); 13 | expect(onPressIcon).toBeCalled(); 14 | 15 | }); 16 | }); -------------------------------------------------------------------------------- /src/components/widgets/HeaderIcon/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, TouchableOpacity, View } from 'react-native'; 3 | import { Icon } from '@components/widgets'; 4 | import { measures } from '@common/styles'; 5 | 6 | export const HeaderIcon = ({ onPress, ...props }) => ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | const styles = StyleSheet.create({ 15 | container: { 16 | marginHorizontal: measures.defaultMargin * 2 17 | } 18 | }); -------------------------------------------------------------------------------- /src/components/views/SelectDestination/NoRecents.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { colors, measures } from '@common/styles'; 4 | 5 | export default () => ( 6 | 7 | 8 | You didn't sent anything yet. 9 | 10 | 11 | ); 12 | 13 | const styles = StyleSheet.create({ 14 | container: { 15 | alignItems: 'center', 16 | paddingTop: measures.defaultPadding 17 | }, 18 | message: { 19 | color: colors.black 20 | } 21 | }); -------------------------------------------------------------------------------- /src/components/views/Settings/ListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, TouchableWithoutFeedback, View } from 'react-native'; 3 | import { measures } from '@common/styles'; 4 | 5 | export default ({ children, onPress }) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | const styles = StyleSheet.create({ 12 | container: { 13 | height: 64, 14 | alignItems: 'stretch', 15 | justifyContent: 'center', 16 | padding: measures.defaultPadding 17 | } 18 | }); -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.21 <0.6.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/views/ChangeCurrency/ListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, TouchableWithoutFeedback, View } from 'react-native'; 3 | import { measures } from '@common/styles'; 4 | 5 | export default ({ children, onPress }) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | const styles = StyleSheet.create({ 12 | container: { 13 | height: 64, 14 | alignItems: 'stretch', 15 | justifyContent: 'center', 16 | padding: measures.defaultPadding 17 | } 18 | }); -------------------------------------------------------------------------------- /src/components/views/WalletSettings/ListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, TouchableWithoutFeedback, View } from 'react-native'; 3 | import { measures } from '@common/styles'; 4 | 5 | export default ({ children, onPress }) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | const styles = StyleSheet.create({ 12 | container: { 13 | height: 64, 14 | alignItems: 'stretch', 15 | justifyContent: 'center', 16 | padding: measures.defaultPadding 17 | } 18 | }); -------------------------------------------------------------------------------- /src/components/views/WalletExtract/NoTransactions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { colors, measures } from '@common/styles'; 4 | 5 | export default () => ( 6 | 7 | 8 | There are still no transactions involving this wallet. 9 | 10 | 11 | ); 12 | 13 | const styles = StyleSheet.create({ 14 | container: { 15 | alignItems: 'center', 16 | paddingTop: measures.defaultPadding 17 | }, 18 | message: { 19 | color: colors.black 20 | } 21 | }); -------------------------------------------------------------------------------- /src/common/services/wallets.js: -------------------------------------------------------------------------------- 1 | import * as StorageService from './storage'; 2 | import { Wallet } from '@common/constants'; 3 | 4 | export async function loadWalletPKs() { 5 | const pks = await StorageService.getItem(Wallet.STORAGE_KEY); 6 | return pks ? JSON.parse(pks) : []; 7 | } 8 | 9 | export async function saveWalletPKs(wallets) { 10 | const map = wallets.map(({ description, name, privateKey }) => ({ description, name, privateKey })); 11 | await StorageService.setItem(Wallet.STORAGE_KEY, JSON.stringify(map)); 12 | } 13 | 14 | export function deleteWalletPKs() { 15 | return StorageService.deleteItem(Wallet.STORAGE_KEY); 16 | } 17 | -------------------------------------------------------------------------------- /src/common/actions/recents.js: -------------------------------------------------------------------------------- 1 | import { Recents as RecentsService } from '@common/services'; 2 | import { recents as RecentsStore } from '@common/stores'; 3 | 4 | export async function loadRecents(store=RecentsStore, service=RecentsService) { 5 | store.isLoading(true); 6 | const recents = await service.loadRecentAddresses(); 7 | store.loadAddresses(recents); 8 | store.isLoading(false); 9 | } 10 | 11 | export async function saveAddressToRecents(address, store=RecentsStore, service=RecentsService) { 12 | store.isLoading(true); 13 | store.addAddress(address); 14 | await service.saveRecentAddresses(store.list); 15 | store.isLoading(false); 16 | } -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/components/widgets/Button/test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native'; 3 | import { shallow } from 'enzyme'; 4 | import { Button } from './index'; 5 | 6 | describe('); 10 | expect(wrapper.find(Text).children().text()).toEqual('button label'); 11 | }); 12 | 13 | it('should call the `onPress` function when clicked', () => { 14 | const onPress = jest.fn(); 15 | const wrapper = shallow( 39 | 40 | 41 | ); 42 | } 43 | 44 | 45 | const styles = StyleSheet.create({ 46 | container: { 47 | flex: 1 48 | } 49 | }); -------------------------------------------------------------------------------- /src/components/views/ChangeCurrency/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollView, StyleSheet, Text, View } from 'react-native'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { colors, measures } from '@common/styles'; 5 | import ListItem from './ListItem'; 6 | 7 | @inject('prices') 8 | @observer 9 | export class ChangeCurrency extends React.Component { 10 | 11 | static navigationOptions = ({ navigation, screenProps }) => ({ 12 | title: 'Select currency' 13 | }); 14 | 15 | selectCurrency(currency) { 16 | 17 | } 18 | 19 | renderItems = (items) => items.map((item, index) => ( 20 | 21 | 22 | {item.title} 23 | 24 | 25 | )); 26 | 27 | render() { 28 | return ( 29 | 30 | {this.renderItems([ 31 | { title: 'Jamaican Dollar', action: () => this.selectCurrency('jmd') }, 32 | { title: 'Dollar', action: () => this.selectCurrency('usd') }, 33 | { title: 'Euro', action: () => this.selectCurrency('eur') }, 34 | ])} 35 | 36 | ); 37 | } 38 | } 39 | 40 | const styles = StyleSheet.create({ 41 | container: { 42 | backgroundColor: colors.defaultBackground, 43 | flex: 1 44 | }, 45 | itemContainer: { 46 | flexDirection: 'row', 47 | alignItems: 'center', 48 | justifyContent: 'flex-start' 49 | }, 50 | itemTitle: { 51 | fontSize: measures.fontSizeMedium, 52 | margin: measures.defaultMargin 53 | } 54 | }); -------------------------------------------------------------------------------- /ios/Partnr-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | NSLocationWhenInUseUsageDescription 40 | 41 | NSAppTransportSecurity 42 | 43 | 44 | NSExceptionDomains 45 | 46 | localhost 47 | 48 | NSExceptionAllowsInsecureHTTPLoads 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/partnr/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.partnr; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.react.ReactApplication; 6 | import com.oblador.vectoricons.VectorIconsPackage; 7 | import com.horcrux.svg.SvgPackage; 8 | import com.azendoo.reactnativesnackbar.SnackbarPackage; 9 | import br.com.classapp.RNSensitiveInfo.RNSensitiveInfoPackage; 10 | import com.swmansion.gesturehandler.react.RNGestureHandlerPackage; 11 | import org.reactnative.camera.RNCameraPackage; 12 | import com.facebook.react.ReactNativeHost; 13 | import com.facebook.react.ReactPackage; 14 | import com.facebook.react.shell.MainReactPackage; 15 | import com.facebook.soloader.SoLoader; 16 | 17 | import java.util.Arrays; 18 | import java.util.List; 19 | 20 | public class MainApplication extends Application implements ReactApplication { 21 | 22 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 23 | @Override 24 | public boolean getUseDeveloperSupport() { 25 | return BuildConfig.DEBUG; 26 | } 27 | 28 | @Override 29 | protected List getPackages() { 30 | return Arrays.asList( 31 | new MainReactPackage(), 32 | new VectorIconsPackage(), 33 | new SvgPackage(), 34 | new SnackbarPackage(), 35 | new RNSensitiveInfoPackage(), 36 | new RNGestureHandlerPackage(), 37 | new RNCameraPackage() 38 | ); 39 | } 40 | 41 | @Override 42 | protected String getJSMainModuleName() { 43 | return "index"; 44 | } 45 | }; 46 | 47 | @Override 48 | public ReactNativeHost getReactNativeHost() { 49 | return mReactNativeHost; 50 | } 51 | 52 | @Override 53 | public void onCreate() { 54 | super.onCreate(); 55 | SoLoader.init(this, /* native exopackage */ false); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/common/services/__tests__/api.js: -------------------------------------------------------------------------------- 1 | import * as Api from '../api'; 2 | 3 | describe('ApiService', () => { 4 | 5 | it('should be able to get the price conversion between ETH, USD, JMD, and EUR', async function() { 6 | try { 7 | const response = await Api.getPrice(); 8 | expect(response.status).toBe(200); 9 | expect(response.data.USD).toBeDefined(); 10 | expect(response.data.USD).not.toBeNaN(); 11 | expect(response.data.EUR).toBeDefined(); 12 | expect(response.data.EUR).not.toBeNaN(); 13 | expect(response.data.JMD).toBeDefined(); 14 | expect(response.data.JMD).not.toBeNaN(); 15 | } catch (e) { 16 | fail(e); 17 | } 18 | }); 19 | 20 | it('should be able to get the transaction history for an existing wallet address', async function() { 21 | try { 22 | const walletAddress = '0x589d41feE71B6c972F7AB20e1Fa6EeC11e6C3dF6'; 23 | const response = await Api.getHistory(walletAddress); 24 | expect(response.status).toBe(200); 25 | expect(response.data.status).toBe('1'); 26 | expect(response.data.result).toBeInstanceOf(Array); 27 | expect(response.data.result.length).toBeGreaterThan(0); 28 | } catch (e) { 29 | fail(e); 30 | } 31 | }); 32 | 33 | it('should be able to get an empty transaction history for an unexisting wallet address', async function() { 34 | try { 35 | const walletAddress = 'notavalidaddress'; 36 | const response = await Api.getHistory(walletAddress); 37 | expect(response.status).toBe(200); 38 | expect(response.data.status).toBe('0'); 39 | expect(response.data.result).toBe('Error! Invalid address format'); 40 | } catch (e) { 41 | fail(e); 42 | } 43 | }); 44 | }); -------------------------------------------------------------------------------- /src/common/utils/wallet.js: -------------------------------------------------------------------------------- 1 | import ethers from 'ethers'; 2 | 3 | const { HDNode, providers, utils, Wallet } = ethers; 4 | 5 | const network = (process.env.NODE_ENV === 'production') ? 'mainnet' : 'rinkeby'; 6 | // let network = (process.env.NODE_ENV === 'production') ? 7 | // { name: 'mainnet', ensAddress: '0x314159265dd8dbb310642f98f50c066173c1259b', chainId: 1 } : 8 | // { name: 'rinkeby', ensAddress: '0xe7410170f87102df0055eb195163a03b7f2bff4a', chainId: 4 }; 9 | 10 | const PROVIDER = providers.getDefaultProvider(network); 11 | 12 | export function generateMnemonics() { 13 | return HDNode.entropyToMnemonic(utils.randomBytes(16)).split(' '); 14 | } 15 | 16 | export function loadWalletFromMnemonics(mnemonics) { 17 | if (!(mnemonics instanceof Array) && typeof mnemonics !== 'string') 18 | throw new Error('invalid mnemonic'); 19 | else if (mnemonics instanceof Array) 20 | mnemonics = mnemonics.join(' '); 21 | 22 | const wallet = Wallet.fromMnemonic(mnemonics); 23 | wallet.provider = PROVIDER; 24 | return wallet; 25 | } 26 | 27 | export function loadWalletFromPrivateKey(pk) { 28 | try { 29 | if (pk.indexOf('0x') !== 0) pk = `0x${pk}`; 30 | return new Wallet(pk, PROVIDER); 31 | } catch (e) { 32 | throw new Error('invalid private key'); 33 | } 34 | } 35 | 36 | export function formatBalance(balance) { 37 | return utils.formatEther(balance); 38 | } 39 | 40 | export function reduceBigNumbers(items) { 41 | if (!(items instanceof Array)) throw new Error('The input is not an Array'); 42 | return items.reduce((prev, next) => prev.add(next), utils.bigNumberify('0')); 43 | } 44 | 45 | export function calculateFee({ gasUsed, gasPrice }) { 46 | return gasUsed * Number(formatBalance(gasPrice)); 47 | } 48 | 49 | export function estimateFee({ gasLimit, gasPrice }) { 50 | return utils.bigNumberify(String(gasLimit)).mul(String(gasPrice)); 51 | } -------------------------------------------------------------------------------- /src/common/actions/__tests__/recents.js: -------------------------------------------------------------------------------- 1 | import * as Action from '../recents'; 2 | 3 | describe('RecentsActions', () => { 4 | 5 | const recentsServiceStub = { 6 | loadRecentAddresses: jest.fn(), 7 | saveRecentAddresses: jest.fn() 8 | }; 9 | 10 | const recentsStoreStub = { 11 | addAddress: jest.fn(), 12 | isLoading: jest.fn(), 13 | loadAddresses: jest.fn() 14 | }; 15 | 16 | beforeEach(() => { 17 | recentsServiceStub.loadRecentAddresses.mockReset(); 18 | recentsStoreStub.addAddress.mockReset(); 19 | recentsStoreStub.isLoading.mockReset(); 20 | recentsStoreStub.loadAddresses.mockReset(); 21 | recentsStoreStub.list = []; 22 | }); 23 | 24 | it('should update the store while loading recents', async () => { 25 | const address = '0x12345'; 26 | recentsServiceStub.loadRecentAddresses.mockImplementationOnce(() => [address]); 27 | await Action.loadRecents(recentsStoreStub, recentsServiceStub); 28 | expect(recentsStoreStub.isLoading).toHaveBeenCalledTimes(2); 29 | expect(recentsStoreStub.isLoading).toHaveBeenNthCalledWith(1, true); 30 | expect(recentsStoreStub.isLoading).toHaveBeenNthCalledWith(2, false); 31 | expect(recentsStoreStub.loadAddresses).toHaveBeenCalledWith([address]) 32 | }); 33 | 34 | it('should save new address to the recents and storage', async () => { 35 | const address = '0x12345'; 36 | await Action.saveAddressToRecents(address, recentsStoreStub, recentsServiceStub); 37 | expect(recentsStoreStub.addAddress).toHaveBeenCalledWith(address); 38 | expect(recentsStoreStub.isLoading).toHaveBeenCalledTimes(2); 39 | expect(recentsStoreStub.isLoading).toHaveBeenNthCalledWith(1, true); 40 | expect(recentsStoreStub.isLoading).toHaveBeenNthCalledWith(2, false); 41 | expect(recentsServiceStub.saveRecentAddresses).toHaveBeenCalled(); 42 | }); 43 | }); -------------------------------------------------------------------------------- /src/components/views/SelectDestination/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { Button, Camera, InputWithIcon } from '@components/widgets'; 4 | import { colors, measures } from '@common/styles'; 5 | import Recents from './Recents'; 6 | 7 | export class SelectDestination extends React.Component { 8 | 9 | static navigationOptions = { title: 'Select destination' }; 10 | 11 | state = { address: '' }; 12 | 13 | onPressContinue() { 14 | const { amount } = this.props.navigation.state.params; 15 | const { address } = this.state; 16 | this.props.navigation.navigate('ConfirmTransaction', { address, amount }); 17 | } 18 | 19 | render() { 20 | return ( 21 | 22 | this.setState({ address })} 28 | onPressIcon={() => this.refs.camera.show()} /> 29 | 30 | this.refs.input.onChangeText(address)} /> 31 | 32 | 28 | 29 | 30 | 31 | ); 32 | } 33 | } 34 | 35 | const styles = StyleSheet.create({ 36 | container: { 37 | backgroundColor: colors.defaultBackground, 38 | alignItems: 'stretch', 39 | justifyContent: 'space-between', 40 | flex: 1, 41 | padding: measures.defaultPadding, 42 | }, 43 | contentContainer: { 44 | flex: 1, 45 | justifyContent: 'space-around' 46 | }, 47 | message: { 48 | color: colors.black, 49 | fontSize: 16, 50 | textAlign: 'center', 51 | marginVertical: measures.defaultMargin, 52 | marginHorizontal: 32 53 | }, 54 | buttonsContainer: { 55 | justifyContent: 'space-between' 56 | } 57 | }); -------------------------------------------------------------------------------- /src/common/stores/__tests__/recents.js: -------------------------------------------------------------------------------- 1 | import { RecentsStore } from '../recents'; 2 | 3 | describe('RecentsStore', () => { 4 | 5 | let recentsStore; 6 | 7 | beforeEach(() => recentsStore = new RecentsStore()); 8 | 9 | it('should be able to change the loading state', () => { 10 | recentsStore.isLoading(true); 11 | expect(recentsStore.loading).toBe(true); 12 | }); 13 | 14 | it('should be able to add a new address to the store list', () => { 15 | expect(recentsStore.list.length).toBe(0); 16 | recentsStore.addAddress('0x12345'); 17 | expect(recentsStore.list.length).toBe(1); 18 | expect(recentsStore.list[0]).toBe('0x12345'); 19 | }); 20 | 21 | it('should be filter repeated addresses', () => { 22 | expect(recentsStore.list.length).toBe(0); 23 | recentsStore.addAddress('0x12345'); 24 | expect(recentsStore.list.length).toBe(1); 25 | recentsStore.addAddress('0x12345'); 26 | expect(recentsStore.list.length).toBe(1); 27 | }); 28 | 29 | it('should be able to update the state with a list of addresses', () => { 30 | expect(recentsStore.list.length).toBe(0); 31 | recentsStore.loadAddresses(['0x12345', '0x54321']); 32 | expect(recentsStore.list.length).toBe(2); 33 | }); 34 | 35 | it('should avoid adding duplicates when updating the state with a list of addresses', () => { 36 | expect(recentsStore.list.length).toBe(0); 37 | recentsStore.loadAddresses(['0x12345', '0x54321', '0x12345']); 38 | expect(recentsStore.list.length).toBe(2); 39 | }); 40 | 41 | it('should be able to reset the store state', () => { 42 | recentsStore.isLoading(true); 43 | recentsStore.addAddress('0x12345'); 44 | recentsStore.reset(); 45 | expect(recentsStore.list.length).toBe(0); 46 | expect(recentsStore.loading).toBe(false); 47 | }); 48 | }); -------------------------------------------------------------------------------- /src/components/widgets/InputWithIcon/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, TextInput, TouchableOpacity, View } from 'react-native'; 3 | import { Icon } from '@components/widgets'; 4 | import { colors } from '@common/styles'; 5 | 6 | export class InputWithIcon extends React.Component { 7 | 8 | state = { text: '' }; 9 | 10 | onChangeText(text) { 11 | const { onChangeText } = this.props; 12 | this.setState({ text }); 13 | if (onChangeText) onChangeText(text); 14 | } 15 | 16 | onPressIcon() { 17 | let { text } = this.state; 18 | text = text.trim(); 19 | this.props.onPressIcon(text); 20 | } 21 | 22 | render() { 23 | const { autoFocus, icon, placeholder } = this.props; 24 | return ( 25 | 26 | this.onChangeText(text)} 33 | underlineColorAndroid="transparent" 34 | placeholder={placeholder} 35 | placeholderTextColor={colors.black} /> 36 | this.onPressIcon()}> 37 | 41 | 42 | 43 | ); 44 | } 45 | } 46 | 47 | const styles = StyleSheet.create({ 48 | container: { 49 | flexDirection: 'row', 50 | alignItems: 'center', 51 | justifyContent: 'space-between' 52 | }, 53 | input: { 54 | width: '90%', 55 | borderBottomWidth: 1, 56 | borderBottomColor: colors.black, 57 | padding: 4, 58 | paddingLeft: 0, 59 | marginRight: 2, 60 | textAlign: 'center', 61 | color: colors.black 62 | } 63 | }); -------------------------------------------------------------------------------- /src/folderStructure.txt: -------------------------------------------------------------------------------- 1 | . 2 | ├── Router.js 3 | ├── assets 4 | │ └── ethereum-logo.png 5 | ├── common 6 | │ ├── actions 7 | │ │ ├── __tests__ 8 | │ │ ├── general.js 9 | │ │ ├── index.js 10 | │ │ ├── prices.js 11 | │ │ ├── recents.js 12 | │ │ ├── transactions.js 13 | │ │ └── wallets.js 14 | │ ├── constants 15 | │ │ ├── index.js 16 | │ │ ├── recents.js 17 | │ │ ├── storage.js 18 | │ │ ├── url.js 19 | │ │ └── wallet.js 20 | │ ├── services 21 | │ │ ├── __tests__ 22 | │ │ ├── api.js 23 | │ │ ├── index.js 24 | │ │ ├── recents.js 25 | │ │ ├── storage.js 26 | │ │ ├── transactions.js 27 | │ │ └── wallets.js 28 | │ ├── stores 29 | │ │ ├── __tests__ 30 | │ │ ├── index.js 31 | │ │ ├── prices.js 32 | │ │ ├── recents.js 33 | │ │ ├── wallet.js 34 | │ │ └── wallets.js 35 | │ ├── styles 36 | │ │ ├── colors.js 37 | │ │ ├── index.js 38 | │ │ └── measures.js 39 | │ └── utils 40 | │ ├── __tests__ 41 | │ ├── image.js 42 | │ ├── index.js 43 | │ ├── shims.js 44 | │ ├── transaction.js 45 | │ └── wallet.js 46 | ├── components 47 | │ ├── views 48 | │ │ ├── ChangeCurrency 49 | │ │ ├── ConfirmMnemonics 50 | │ │ ├── ConfirmTransaction 51 | │ │ ├── CreateMnemonics 52 | │ │ ├── CreateWallet 53 | │ │ ├── LoadMnemonics 54 | │ │ ├── LoadPrivateKey 55 | │ │ ├── LoadWallet 56 | │ │ ├── NewWallet 57 | │ │ ├── NewWalletName 58 | │ │ ├── ReceiveCoins 59 | │ │ ├── SelectDestination 60 | │ │ ├── SendCoins 61 | │ │ ├── Settings 62 | │ │ ├── ShowPrivateKey 63 | │ │ ├── WalletDetails 64 | │ │ ├── WalletExtract 65 | │ │ ├── WalletSettings 66 | │ │ ├── WalletsOverview 67 | │ │ └── index.js 68 | │ └── widgets 69 | │ ├── Button 70 | │ ├── Calculator 71 | │ ├── Camera 72 | │ ├── HeaderIcon 73 | │ ├── Icon 74 | │ ├── InputWithIcon 75 | │ ├── NumberGrid 76 | │ ├── TabView 77 | │ ├── TextBullet 78 | │ └── index.js 79 | ├── index.js 80 | ├── partnr-tree.txt 81 | └── setupTests.js 82 | 83 | 43 directories, 37 files -------------------------------------------------------------------------------- /ios/PartnrTests/PartnrTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | #import 12 | #import 13 | 14 | #define TIMEOUT_SECONDS 600 15 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" 16 | 17 | @interface PartnrTests : XCTestCase 18 | 19 | @end 20 | 21 | @implementation PartnrTests 22 | 23 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 24 | { 25 | if (test(view)) { 26 | return YES; 27 | } 28 | for (UIView *subview in [view subviews]) { 29 | if ([self findSubviewInView:subview matching:test]) { 30 | return YES; 31 | } 32 | } 33 | return NO; 34 | } 35 | 36 | - (void)testRendersWelcomeScreen 37 | { 38 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 39 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 40 | BOOL foundElement = NO; 41 | 42 | __block NSString *redboxError = nil; 43 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 44 | if (level >= RCTLogLevelError) { 45 | redboxError = message; 46 | } 47 | }); 48 | 49 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 50 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 51 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 52 | 53 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 54 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 55 | return YES; 56 | } 57 | return NO; 58 | }]; 59 | } 60 | 61 | RCTSetLogFunction(RCTDefaultLogFunction); 62 | 63 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 64 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 65 | } 66 | 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /src/components/views/CreateWallet/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { Button } from '@components/widgets'; 4 | import { colors, measures } from '@common/styles'; 5 | 6 | export class CreateWallet extends React.Component { 7 | 8 | static navigationOptions = { title: 'Create Wallet' }; 9 | 10 | onPressProceed() { 11 | const { walletName, walletDescription } = this.props.navigation.state.params; 12 | this.props.navigation.navigate('CreateMnemonics', { walletName, walletDescription }); 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | 19 | 20 | When creating a new wallet you will receive a sequence of mnemonics which represent your "personal password". Anyone with this sequence may be able to reconfigure your wallet in any new device. Keep it stored as secure as possible. Only you should have access to this information. 21 | Write it somewhere safe so you can make sure you won't lose it, or you may lose permanently all your coins. There is no way to recover it later. 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | } 31 | 32 | const styles = StyleSheet.create({ 33 | container: { 34 | backgroundColor: colors.defaultBackground, 35 | alignItems: 'stretch', 36 | justifyContent: 'space-between', 37 | flex: 1, 38 | padding: measures.defaultPadding, 39 | }, 40 | contentContainer: { 41 | flex: 1, 42 | justifyContent: 'space-around' 43 | }, 44 | message: { 45 | color: colors.black, 46 | fontSize: 16, 47 | textAlign: 'center', 48 | marginVertical: measures.defaultMargin, 49 | marginHorizontal: 32 50 | }, 51 | buttonsContainer: { 52 | justifyContent: 'space-between' 53 | } 54 | }); -------------------------------------------------------------------------------- /src/components/views/WalletsOverview/TotalBalance.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { colors, measures } from '@common/styles'; 5 | import { Wallet as WalletUtils } from '@common/utils'; 6 | 7 | @inject('prices') 8 | @observer 9 | 10 | export default class TotalBalance extends React.Component { 11 | 12 | get balance() { 13 | const { wallets } = this.props; 14 | const balances = wallets.map(({ balance }) => balance); 15 | if (balances.some(el => !el)) return 0; 16 | const balance = WalletUtils.reduceBigNumbers(balances).toString(); 17 | return Number(WalletUtils.formatBalance(balance)); 18 | } 19 | 20 | get fiatBalance() { 21 | return Number(this.props.prices.usd * this.balance); 22 | } 23 | 24 | render() { 25 | return ( 26 | 27 | 28 | Total balance: 29 | 30 | 31 | ETH {this.balance.toFixed(3)} 32 | US$ {this.fiatBalance.toFixed(2)} 33 | 34 | 35 | ); 36 | } 37 | } 38 | 39 | const styles = StyleSheet.create({ 40 | container: { 41 | alignItems: 'center', 42 | justifyContent: 'flex-start', 43 | height: 60, 44 | flexDirection: 'row', 45 | borderBottomWidth: 1, 46 | borderBottomColor: colors.lightGray 47 | }, 48 | leftColumn: { 49 | flex: 1 50 | }, 51 | title: { 52 | fontSize: measures.fontSizeLarge, 53 | color: colors.gray 54 | }, 55 | balance: { 56 | fontSize: measures.fontSizeMedium + 2, 57 | fontWeight: 'bold', 58 | color: colors.gray 59 | }, 60 | fiatBalance: { 61 | fontSize: measures.fontSizeMedium - 3, 62 | color: colors.gray 63 | }, 64 | rightColumn: { 65 | flex: 1, 66 | alignItems: 'flex-end', 67 | justifyContent: 'center' 68 | } 69 | }); -------------------------------------------------------------------------------- /src/components/widgets/Calculator/Panel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { colors, measures } from '@common/styles'; 5 | 6 | @inject('prices') 7 | @observer 8 | export default class Panel extends React.Component { 9 | 10 | state = { amount: '' }; 11 | 12 | get amount() { 13 | return this.state.amount || 0; 14 | } 15 | 16 | get fiatAmount() { 17 | return (this.amount * this.props.prices.usd).toFixed(2); 18 | } 19 | 20 | onChange(value) { 21 | let { amount } = this.state; 22 | switch (value) { 23 | case 'erase': 24 | amount = amount.slice(0, amount.length-1); 25 | break; 26 | 27 | case '.': 28 | if (amount.indexOf('.') > -1) return; 29 | else if (!amount.length) amount += '0.'; 30 | else amount += '.'; 31 | break; 32 | 33 | default: 34 | if (amount === '0') amount = value; 35 | else amount += value; 36 | break; 37 | } 38 | this.setState({ amount }); 39 | } 40 | 41 | render() { 42 | return ( 43 | 44 | 45 | {this.amount} 46 | ETH 47 | 48 | 49 | US$ {this.fiatAmount} 50 | 51 | 52 | ); 53 | } 54 | } 55 | 56 | const styles = StyleSheet.create({ 57 | container: { 58 | flex: 1, 59 | alignItems: 'center', 60 | justifyContent: 'space-around' 61 | }, 62 | row: { 63 | alignItems: 'center', 64 | justifyContent: 'center', 65 | flexDirection: 'row' 66 | }, 67 | amount: { 68 | fontSize: measures.fontSizeLarge, 69 | fontWeight: 'bold' 70 | }, 71 | unit: { 72 | fontSize: measures.fontSizeMedium, 73 | color: colors.gray, 74 | marginLeft: measures.defaultMargin 75 | } 76 | }); -------------------------------------------------------------------------------- /contracts/Votes.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.22 < 0.6.0; 2 | 3 | /// @title Voting with delegation. 4 | contract Ballot { 5 | // This declares a new complex type which will 6 | // be used for variables later. 7 | // It will represent a single voter. 8 | struct Voter { 9 | bool voted; // if true, that person already voted 10 | uint vote; // index of the voted proposal 11 | } 12 | 13 | // This is a type for a single proposal. 14 | struct Proposal { 15 | bytes32 name; // short name (up to 32 bytes) 16 | uint voteCount; // number of accumulated votes 17 | } 18 | 19 | // This declares a state variable that 20 | // stores a `Voter` struct for each possible address. 21 | mapping(address => Voter) public voters; 22 | 23 | // A dynamically-sized array of `Proposal` structs. 24 | Proposal[] public proposals; 25 | 26 | 27 | /// Give your vote (including votes delegated to you) 28 | /// to proposal `proposals[proposal].name`. 29 | function vote(uint proposal) public { 30 | Voter storage sender = voters[msg.sender]; 31 | require( 32 | !sender.voted, 33 | "Already voted." 34 | ); 35 | sender.voted = true; 36 | sender.vote = proposal; 37 | 38 | // If `proposal` is out of the range of the array, 39 | // this will throw automatically and revert all 40 | // changes. 41 | proposals[proposal].voteCount ++; 42 | } 43 | 44 | /// @dev Computes the winning proposal taking all 45 | /// previous votes into account. 46 | function winningProposal() public view 47 | returns (uint winningProposal_) 48 | { 49 | uint winningVoteCount = 0; 50 | for (uint p = 0; p < proposals.length; p++) { 51 | if (proposals[p].voteCount > winningVoteCount) { 52 | winningVoteCount = proposals[p].voteCount; 53 | winningProposal_ = p; 54 | } 55 | } 56 | } 57 | 58 | // Calls winningProposal() function to get the index 59 | // of the winner contained in the proposals array and then 60 | // returns the name of the winner 61 | function winnerName() public view returns (bytes32 winnerName_) { 62 | winnerName_ = proposals[winningProposal()].name; 63 | } 64 | } -------------------------------------------------------------------------------- /src/Router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | createAppContainer, 4 | NavigationActions, 5 | createStackNavigator 6 | } from 'react-navigation'; 7 | import * as Views from './components/views'; 8 | import { colors } from './common/styles'; 9 | 10 | export const INITIAL_ROUTE = 'WalletsOverview'; 11 | 12 | const navigator = createStackNavigator( 13 | { 14 | ChangeCurrency: { screen: Views.ChangeCurrency }, 15 | ConfirmMnemonics: { screen: Views.ConfirmMnemonics }, 16 | ConfirmTransaction: { screen: Views.ConfirmTransaction }, 17 | CreateMnemonics: { screen: Views.CreateMnemonics }, 18 | CreateWallet: { screen: Views.CreateWallet }, 19 | LoadMnemonics: { screen: Views.LoadMnemonics }, 20 | LoadPrivateKey: { screen: Views.LoadPrivateKey }, 21 | LoadWallet: { screen: Views.LoadWallet }, 22 | NewWallet: { screen: Views.NewWallet }, 23 | NewWalletName: { screen: Views.NewWalletName }, 24 | SelectDestination: { screen: Views.SelectDestination }, 25 | Settings: { screen: Views.Settings }, 26 | ShowPrivateKey: { screen: Views.ShowPrivateKey }, 27 | WalletDetails: { screen: Views.WalletDetails }, 28 | WalletsOverview: { screen: Views.WalletsOverview } 29 | }, 30 | { 31 | initialRouteName: INITIAL_ROUTE, 32 | defaultNavigationOptions: { 33 | headerStyle: { 34 | backgroundColor: colors.primary 35 | }, 36 | headerTintColor: colors.secondary, 37 | tintColor: colors.secondary 38 | } 39 | } 40 | ); 41 | 42 | const parentGetStateForAction = navigator.router.getStateForAction; 43 | 44 | navigator.router.getStateForAction = (action, inputState) => { 45 | const state = parentGetStateForAction(action, inputState); 46 | 47 | // fix it up if applicable 48 | if (state && action.type === NavigationActions.NAVIGATE) { 49 | if (action.params && action.params.replaceRoute) { 50 | const leave = action.params.leave || 1; 51 | delete action.params.replaceRoute; 52 | while (state.routes.length > leave && state.index > 0) { 53 | const oldIndex = state.index - 1; 54 | // remove one that we are replacing 55 | state.routes.splice(oldIndex, 1); 56 | // index now one less 57 | state.index = oldIndex; 58 | } 59 | } 60 | } 61 | 62 | return state; 63 | }; 64 | 65 | const AppContainer = createAppContainer(navigator); 66 | 67 | export default AppContainer; 68 | -------------------------------------------------------------------------------- /src/components/widgets/NumberGrid/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'; 3 | import { Icon } from '@components/widgets'; 4 | import { colors, measures } from '@common/styles'; 5 | 6 | export class NumberGrid extends React.Component { 7 | 8 | renderBlock = (label, value) => ( 9 | this.props.onPressNumber(value)}> 10 | 11 | {label} 12 | 13 | 14 | ); 15 | 16 | render() { 17 | return ( 18 | 19 | 20 | {this.renderBlock('9', '9')} 21 | {this.renderBlock('8', '8')} 22 | {this.renderBlock('7', '7')} 23 | 24 | 25 | {this.renderBlock('6', '6')} 26 | {this.renderBlock('5', '5')} 27 | {this.renderBlock('4', '4')} 28 | 29 | 30 | {this.renderBlock('3', '3')} 31 | {this.renderBlock('2', '2')} 32 | {this.renderBlock('1', '1')} 33 | 34 | 35 | {this.renderBlock(, '.')} 36 | {this.renderBlock('0', '0')} 37 | {this.renderBlock(, 'erase')} 38 | 39 | 40 | ); 41 | } 42 | } 43 | 44 | const styles = StyleSheet.create({ 45 | container: { 46 | flex: 4, 47 | alignItems: 'stretch', 48 | justifyContent: 'center', 49 | flexDirection: 'column' 50 | }, 51 | row: { 52 | flex: 1, 53 | flexDirection: 'row', 54 | alignItems: 'stretch', 55 | justifyContent: 'center' 56 | }, 57 | block: { 58 | flex: 1, 59 | borderWidth: 2, 60 | borderColor: colors.secondary, 61 | backgroundColor: colors.lightGray, 62 | alignItems: 'center', 63 | justifyContent: 'center' 64 | }, 65 | label: { 66 | fontSize: measures.fontSizeMedium, 67 | fontWeight: 'bold' 68 | } 69 | }); -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | 16 | ; Ignore polyfills 17 | .*/Libraries/polyfills/.* 18 | 19 | ; Ignore metro 20 | .*/node_modules/metro/.* 21 | 22 | [include] 23 | 24 | [libs] 25 | node_modules/react-native/Libraries/react-native/react-native-interface.js 26 | node_modules/react-native/flow/ 27 | node_modules/react-native/flow-github/ 28 | 29 | [options] 30 | emoji=true 31 | 32 | module.system=haste 33 | module.system.haste.use_name_reducers=true 34 | # get basename 35 | module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1' 36 | # strip .js or .js.flow suffix 37 | module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1' 38 | # strip .ios suffix 39 | module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1' 40 | module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' 41 | module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' 42 | module.system.haste.paths.blacklist=.*/__tests__/.* 43 | module.system.haste.paths.blacklist=.*/__mocks__/.* 44 | module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.* 45 | module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.* 46 | 47 | munge_underscores=true 48 | 49 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 50 | 51 | module.file_ext=.js 52 | module.file_ext=.jsx 53 | module.file_ext=.json 54 | module.file_ext=.native.js 55 | 56 | suppress_type=$FlowIssue 57 | suppress_type=$FlowFixMe 58 | suppress_type=$FlowFixMeProps 59 | suppress_type=$FlowFixMeState 60 | 61 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 62 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 63 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 64 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 65 | 66 | [version] 67 | ^0.98.1 68 | -------------------------------------------------------------------------------- /ios/Partnr/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Partnr 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSLocationWhenInUseUsageDescription 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UIViewControllerBasedStatusBarAppearance 42 | 43 | NSAppTransportSecurity 44 | 45 | NSAllowsArbitraryLoads 46 | 47 | NSExceptionDomains 48 | 49 | localhost 50 | 51 | NSExceptionAllowsInsecureHTTPLoads 52 | 53 | 54 | 55 | 56 | UIAppFonts 57 | 58 | AntDesign.ttf 59 | Entypo.ttf 60 | EvilIcons.ttf 61 | Feather.ttf 62 | FontAwesome.ttf 63 | FontAwesome5_Brands.ttf 64 | FontAwesome5_Regular.ttf 65 | FontAwesome5_Solid.ttf 66 | Foundation.ttf 67 | Ionicons.ttf 68 | MaterialCommunityIcons.ttf 69 | MaterialIcons.ttf 70 | Octicons.ttf 71 | SimpleLineIcons.ttf 72 | Zocial.ttf 73 | Fontisto.ttf 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/components/widgets/Icon/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform } from 'react-native'; 3 | import Entypo from 'react-native-vector-icons/dist/Entypo'; 4 | import EvilIcon from 'react-native-vector-icons/dist/EvilIcons'; 5 | import Feather from 'react-native-vector-icons/dist/Feather'; 6 | import FontAwesome from 'react-native-vector-icons/dist/FontAwesome'; 7 | import Foundation from 'react-native-vector-icons/dist/Foundation'; 8 | import Ionicon from 'react-native-vector-icons/dist/Ionicons'; 9 | import MaterialIcon from 'react-native-vector-icons/dist/MaterialIcons'; 10 | import MaterialCommunityIcon from 'react-native-vector-icons/dist/MaterialCommunityIcons'; 11 | import Octicon from 'react-native-vector-icons/dist/Octicons'; 12 | import Zocial from 'react-native-vector-icons/dist/Zocial'; 13 | import SimpleLineIcon from 'react-native-vector-icons/SimpleLineIcons'; 14 | import { colors, measures } from '@common/styles'; 15 | 16 | function getSize(size) { 17 | if ((size >>> 0) > 0) return size; 18 | switch (size) { 19 | case 'small': return measures.iconSizeSmall; 20 | case 'large': return measures.iconSizeLarge; 21 | default: 22 | case 'medium': return measures.iconSizeMedium; 23 | } 24 | } 25 | 26 | function getIonicon({ name, size, ...props }) { 27 | name = (Platform.OS === 'ios') ? `ios-${name}` : `md-${name}`; 28 | return ; 29 | } 30 | 31 | export const Icon = (props) => { 32 | if (!props.name) return null; 33 | const size = getSize(props.size); 34 | const color = props.color || colors.black; 35 | switch (props.type) { 36 | case 'ent': return ; 37 | case 'ei': return ; 38 | case 'fe': return ; 39 | case 'fa': return ; 40 | case 'fo': return ; 41 | case 'md': return ; 42 | case 'mdc': return ; 43 | case 'oct': return ; 44 | case 'zo': return ; 45 | case 'simple': return ; 46 | 47 | default: 48 | case 'ionicons': return getIonicon({ ...props, color, size }); 49 | } 50 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Partnr", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "node node_modules/react-native/local-cli/cli.js start", 7 | "test": "jest", 8 | "test:watch": "jest --watch", 9 | "android:build": "cd android && ./gradlew assembleDebug && cd .. && cp android/app/build/outputs/apk/debug/app-debug.apk EthereumWallet.apk", 10 | "android:bundle": "react-native bundle --dev false --platform android --entry-file index.js --bundle-output ./android/app/build/intermediates/assets/debug/index.android.bundle --assets-dest ./android/app/build/intermediates/res/merged/debug", 11 | "android:clean": "cd android && ./gradlew clean && cd ..", 12 | "android:generate-apk": "npm run android:bundle && npm run android:build", 13 | "android": "react-native run-android", 14 | "logcat": "adb logcat *:S ReactNative:V ReactNativeJS:V", 15 | "ios": "react-native run-ios", 16 | "postinstall": "rndebugger-open" 17 | }, 18 | "dependencies": { 19 | "axios": "0.28.0", 20 | "ethers": "6.13.2", 21 | "identicon.js": "2.3.3", 22 | "mobx": "5.9.4", 23 | "mobx-react": "5.4.4", 24 | "moment": "2.29.4", 25 | "native-base": "^2.13.8", 26 | "react": "16.8.6", 27 | "react-native": "0.71.4", 28 | "react-native-camera": "2.10.1", 29 | "react-native-easy-grid": "^0.2.2", 30 | "react-native-gesture-handler": "1.2.1", 31 | "react-native-modal": "11.0.1", 32 | "react-native-permissions": "1.1.1", 33 | "react-native-qrcode-svg": "5.1.2", 34 | "react-native-sensitive-info": "5.4.1", 35 | "react-native-snackbar": "1.0.0", 36 | "react-native-svg": "9.4.0", 37 | "react-native-vector-icons": "^6.6.0", 38 | "react-navigation": "3.11.0" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "7.4.5", 42 | "@babel/plugin-proposal-decorators": "7.4.4", 43 | "@babel/plugin-transform-runtime": "7.4.4", 44 | "@babel/runtime": "7.4.5", 45 | "babel-core": "7.0.0-bridge.0", 46 | "babel-jest": "29.7.0", 47 | "babel-plugin-module-resolver": "5.0.0", 48 | "babel-preset-react-native": "4.0.1", 49 | "enzyme": "3.9.0", 50 | "enzyme-adapter-react-16": "1.13.1", 51 | "eslint-config-rallycoding": "3.2.0", 52 | "jest": "29.5.0", 53 | "metro-react-native-babel-preset": "0.54.1", 54 | "react-native-debugger-open": "^0.3.22", 55 | "react-native-mock-render": "0.1.3", 56 | "react-test-renderer": "16.8.6" 57 | }, 58 | "jest": { 59 | "preset": "react-native", 60 | "setupFilesAfterEnv": [ 61 | "/src/setupTests.js" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/views/ConfirmMnemonics/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { Button } from '@components/widgets'; 4 | import { Wallet as WalletUtils } from '@common/utils'; 5 | import { colors, measures } from '@common/styles'; 6 | import { General as GeneralActions, Wallets as WalletsActions } from '@common/actions'; 7 | import ConfirmBox from './ConfirmBox'; 8 | 9 | export class ConfirmMnemonics extends React.Component { 10 | 11 | static navigationOptions = { title: 'Create Wallet' }; 12 | 13 | state = { mnemonics: [] }; 14 | 15 | componentDidMount() { 16 | const { mnemonics, walletName, walletDescription } = this.props.navigation.state.params; 17 | this.setState({ mnemonics, walletName, walletDescription }); 18 | } 19 | 20 | async onPressConfirm() { 21 | if (!this.refs.confirm.isValidSequence()) return; 22 | try { 23 | const { mnemonics, walletName, walletDescription } = this.state; 24 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics); 25 | await WalletsActions.addWallet(walletName, wallet, walletDescription); 26 | this.props.navigation.navigate('WalletsOverview', { replaceRoute: true }); 27 | await WalletsActions.saveWallets(); 28 | } catch (e) { 29 | GeneralActions.notify(e.message, 'long'); 30 | } 31 | } 32 | 33 | render() { 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | } 45 | 46 | const styles = StyleSheet.create({ 47 | container: { 48 | alignItems: 'center', 49 | justifyContent: 'space-between', 50 | flex: 1, 51 | backgroundColor: colors.defaultBackground 52 | }, 53 | message: { 54 | color: colors.black, 55 | fontSize: 16, 56 | textAlign: 'center', 57 | marginVertical: measures.defaultMargin, 58 | marginHorizontal: 32 59 | }, 60 | mnemonicsContainer: { 61 | flexDirection: 'row', 62 | justifyContent: 'center', 63 | flexWrap: 'wrap', 64 | maxWidth: '80%' 65 | }, 66 | mnemonic: { 67 | margin: 4 68 | }, 69 | buttonsContainer: { 70 | width: '100%', 71 | justifyContent: 'flex-end', 72 | height: 104 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /src/components/views/Settings/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, ScrollView, StyleSheet, Text, View } from 'react-native'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { Icon } from '@components/widgets'; 5 | import { colors, measures } from '@common/styles'; 6 | import { General as GeneralActions } from '@common/actions'; 7 | import ListItem from './ListItem'; 8 | 9 | @inject('wallet') 10 | @observer 11 | export class Settings extends React.Component { 12 | 13 | static navigationOptions = ({ navigation, screenProps }) => ({ 14 | title: 'Settings' 15 | }); 16 | 17 | goToChangeCurrencyPage() { 18 | this.props.navigation.navigate('ChangeCurrency'); 19 | } 20 | 21 | eraseAllData() { 22 | GeneralActions.eraseAllData(); 23 | this.props.navigation.pop(); 24 | } 25 | 26 | confirmErase() { 27 | Alert.alert( 28 | 'Erase all data', 29 | 'This action cannot be undone. Are you sure?', 30 | [ 31 | { text: 'Cancel', onPress: () => {}, style: 'cancel' }, 32 | { text: 'Erase', onPress: () => this.eraseAllData() } 33 | ], 34 | { cancelable: false } 35 | ); 36 | } 37 | 38 | renderItems = (items) => items.map((item, index) => ( 39 | 40 | 41 | 42 | 43 | 44 | {item.title} 45 | 46 | 47 | )); 48 | 49 | render() { 50 | return ( 51 | 52 | {this.renderItems([ 53 | { title: 'Change currency', iconName: 'attach-money', iconType: 'md', action: () => this.goToChangeCurrencyPage() }, 54 | { title: 'Erase all data', iconName: 'trash', iconType: '', action: () => this.confirmErase() }, 55 | ])} 56 | 57 | ); 58 | } 59 | } 60 | 61 | const styles = StyleSheet.create({ 62 | container: { 63 | backgroundColor: colors.defaultBackground, 64 | flex: 1 65 | }, 66 | itemContainer: { 67 | flexDirection: 'row', 68 | alignItems: 'center', 69 | justifyContent: 'flex-start' 70 | }, 71 | icon: { 72 | width: 24, 73 | height: 24, 74 | margin: measures.defaultMargin 75 | }, 76 | itemTitle: { 77 | fontSize: measures.fontSizeMedium 78 | } 79 | }); -------------------------------------------------------------------------------- /src/components/views/SelectDestination/Recents.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator, FlatList, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'; 3 | import { colors, measures } from '@common/styles'; 4 | import { inject, observer } from 'mobx-react'; 5 | import { General as GeneralActions, Recents as RecentsActions } from '@common/actions'; 6 | import NoRecents from './NoRecents'; 7 | 8 | @inject('recents') 9 | @observer 10 | export default class Recents extends React.Component { 11 | 12 | componentDidMount() { 13 | this.loadRecents(); 14 | } 15 | 16 | async loadRecents() { 17 | try { 18 | await RecentsActions.loadRecents(); 19 | } catch (e) { 20 | GeneralActions.notify(e.message, 'long'); 21 | } 22 | } 23 | 24 | renderRecent = ({ item }) => ( 25 | this.props.onPressItem(item)}> 26 | 27 | {item} 28 | 29 | 30 | ); 31 | 32 | renderList = (recents) => (!recents.length) ? : ( 33 | item} 37 | renderItem={this.renderRecent} /> 38 | ); 39 | 40 | renderBody = ({ loading, list }) => loading ? : this.renderList(list); 41 | 42 | render() { 43 | return ( 44 | 45 | 46 | Recent destinations 47 | 48 | {this.renderBody(this.props.recents)} 49 | 50 | ); 51 | } 52 | } 53 | 54 | const styles = StyleSheet.create({ 55 | container: { 56 | flex: 1, 57 | alignItems: 'stretch', 58 | justifyContent: 'flex-start', 59 | flexDirection: 'column' 60 | }, 61 | title: { 62 | fontSize: measures.fontSizeMedium - 3 63 | }, 64 | listContainer: { 65 | borderWidth: 1, 66 | borderColor: colors.lightGray 67 | }, 68 | itemContainer: { 69 | borderBottomWidth: 1, 70 | borderColor: colors.lightGray, 71 | height: 64, 72 | alignItems: 'flex-start', 73 | justifyContent: 'center', 74 | padding: measures.defaultPadding 75 | }, 76 | itemTitle: { 77 | fontSize: measures.fontSizeMedium - 1 78 | }, 79 | noItems: { 80 | marginTop: measures.defaultMargin * 2 81 | } 82 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Partnr 2 | This is a decentralized savings platform built to run on ethereum, using [Dai.js](https://makerdao.com/documentation/) 3 | 4 | [![CircleCI](https://circleci.com/gh/maschad/partnr.svg?style=svg)](https://circleci.com/gh/maschad/partnr) 5 | 6 | 7 | ## Table of Contents 8 | 9 | * [Available Scripts](#available-scripts) 10 | * [npm start](#npm-start) 11 | * [npm test](#npm-test) 12 | * [npm run ios](#npm-run-ios) 13 | * [npm run android](#npm-run-android) 14 | * [npm run android-build](#npm-run-android-build) 15 | * [npm run android-bundle](#npm-run-android-bundle) 16 | * [npm run android-clean](#npm-run-android-clean) 17 | * [Writing and Running Tests](#writing-and-running-tests) 18 | * [License](#license) 19 | * [Contribute](#contribute) 20 | 21 | ## Available Scripts 22 | 23 | ### `npm install` 24 | 25 | Installs all dependencies and prepares the app to run. 26 | 27 | ### `npm start` 28 | 29 | Runs Packager to provide your app in development mode. 30 | 31 | #### `npm test` 32 | 33 | Runs the [jest](https://github.com/facebook/jest) test runner on your tests. 34 | 35 | #### `npm run ios` 36 | 37 | Open your app in the iOS Simulator if you're on a Mac and have it installed. Depends on `npm start`. 38 | 39 | #### `npm run android` 40 | 41 | Open your app on a connected Android device or emulator. Requires an installation of Android build tools (see [React Native docs](https://facebook.github.io/react-native/docs/getting-started.html) for detailed setup). Depends on `npm start`. 42 | 43 | #### `npm run android:build` 44 | 45 | Build the Android app and generate the APK to install on the device. 46 | 47 | #### `npm run android:bundle` 48 | 49 | Bundles the ReactNative JavaScript code. Run it before running the build command to be able to run the test without depending on the development server. 50 | 51 | #### `npm run android:generate-apk` 52 | 53 | Bundle and build the Android app. 54 | 55 | #### `npm run android:clean` 56 | 57 | Clean the Android generated build files. 58 | 59 | ## Writing and Running Tests 60 | 61 | This project is set up to use [jest](https://facebook.github.io/jest/) for tests. You can configure whatever testing strategy you like, but jest works out of the box. Create test files in directories called `__tests__` or with the `.test` extension to have the files loaded by jest. See [the template project](https://github.com/react-community/create-react-native-app/blob/master/react-native-scripts/template/App.test.js) for an example test. The [jest documentation](https://facebook.github.io/jest/docs/getting-started.html) is also a wonderful resource, as is the [React Native testing tutorial](https://facebook.github.io/jest/docs/tutorial-react-native.html). 62 | 63 | ## License 64 | 65 | MIT 66 | 67 | 68 | ## Contribute 69 | 70 | TODO -------------------------------------------------------------------------------- /src/common/stores/__tests__/wallets.js: -------------------------------------------------------------------------------- 1 | import { WalletsStore } from '../wallets'; 2 | import { Wallet as WalletUtils } from '@common/utils'; 3 | 4 | describe('WalletsStore', () => { 5 | 6 | let walletsStore; 7 | 8 | beforeEach(() => walletsStore = new WalletsStore()); 9 | 10 | it('should be able to change the loading state', () => { 11 | walletsStore.isLoading(true); 12 | expect(walletsStore.loading).toBe(true); 13 | }); 14 | 15 | it('should be able to add a wallet instance to the store list', () => { 16 | const mnemonics = WalletUtils.generateMnemonics(); 17 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics); 18 | expect(walletsStore.list.length).toBe(0); 19 | walletsStore.addWallet("walletName", wallet, "description"); 20 | expect(walletsStore.list.length).toBe(1); 21 | expect(walletsStore.list[0].name).toBe("walletName"); 22 | expect(walletsStore.list[0].description).toBe('description'); 23 | expect(walletsStore.list[0].getAddress()).toBe(wallet.getAddress()); 24 | }); 25 | 26 | it('should be able to modify a wallet balance in the list', () => { 27 | const mnemonics1 = WalletUtils.generateMnemonics(); 28 | const wallet1 = WalletUtils.loadWalletFromMnemonics(mnemonics1); 29 | const mnemonics2 = WalletUtils.generateMnemonics(); 30 | const wallet2 = WalletUtils.loadWalletFromMnemonics(mnemonics2); 31 | walletsStore.addWallet("walletName1", wallet1); 32 | walletsStore.addWallet("walletName2", wallet2); 33 | expect(walletsStore.list.length).toBe(2); 34 | expect(walletsStore.list[1].getAddress()).toBe(wallet2.getAddress()); 35 | walletsStore.setBalance(wallet2.getAddress(), 1000); 36 | expect(walletsStore.list[1].balance).toBe(1000); 37 | }); 38 | 39 | it('should be able to remove a wallet instance from the store list', () => { 40 | const mnemonics = WalletUtils.generateMnemonics(); 41 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics); 42 | walletsStore.addWallet("walletName", wallet); 43 | expect(walletsStore.list.length).toBe(1); 44 | walletsStore.removeWallet(wallet); 45 | expect(walletsStore.list.length).toBe(0); 46 | }); 47 | 48 | it('should be able to reset the store state', () => { 49 | const mnemonics = WalletUtils.generateMnemonics(); 50 | const wallet = WalletUtils.loadWalletFromMnemonics(mnemonics); 51 | expect(walletsStore.list.length).toBe(0); 52 | walletsStore.addWallet("walletName", wallet); 53 | walletsStore.isLoading(true); 54 | walletsStore.reset(); 55 | expect(walletsStore.list.length).toBe(0); 56 | expect(walletsStore.loading).toBeFalsy(); 57 | }); 58 | }); -------------------------------------------------------------------------------- /src/components/views/ReceiveCoins/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Clipboard, Share, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'; 3 | import { inject, observer } from 'mobx-react'; 4 | import QRCode from 'react-native-qrcode-svg'; 5 | import { Icon } from '@components/widgets'; 6 | import { General as GeneralActions } from '@common/actions'; 7 | import { colors, measures } from '@common/styles'; 8 | 9 | @inject('wallet') 10 | @observer 11 | export class ReceiveCoins extends React.Component { 12 | 13 | copyToClipboard() { 14 | const { item } = this.props.wallet; 15 | Clipboard.setString(item.getAddress()); 16 | GeneralActions.notify('Copied to clipboard', 'short'); 17 | } 18 | 19 | share() { 20 | const { item } = this.props.wallet; 21 | Share.share({ 22 | title: 'Wallet address:', 23 | message: item.getAddress() 24 | }); 25 | } 26 | 27 | renderColumn = (icon, label, action) => ( 28 | 29 | 30 | 31 | {label} 32 | 33 | 34 | ); 35 | 36 | render() { 37 | const { wallet: { item } } = this.props; 38 | return ( 39 | 40 | Show the code below to receive coins 41 | 42 | 43 | 44 | {item.getAddress()} 45 | 46 | 47 | {this.renderColumn('copy', 'Copy', () => this.copyToClipboard())} 48 | {this.renderColumn('share', 'Share', () => this.share())} 49 | 50 | 51 | 52 | ); 53 | } 54 | } 55 | 56 | const styles = StyleSheet.create({ 57 | container: { 58 | backgroundColor: colors.white, 59 | flex: 1, 60 | alignItems: 'stretch', 61 | justifyContent: 'space-around', 62 | padding: measures.defaultPadding 63 | }, 64 | actions: { 65 | height: 56 66 | }, 67 | actionsBar: { 68 | flexDirection: 'row', 69 | flex: 3 70 | }, 71 | actionColumn: { 72 | flexDirection: 'column', 73 | flex: 1, 74 | alignItems: 'center', 75 | justifyContent: 'center' 76 | }, 77 | centered: { 78 | alignSelf: 'center' 79 | } 80 | }); -------------------------------------------------------------------------------- /src/components/views/CreateMnemonics/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { Button, TextBullet } from '@components/widgets'; 4 | import { Wallet as WalletUtils } from '@common/utils'; 5 | import { colors, measures } from '@common/styles'; 6 | 7 | export class CreateMnemonics extends React.Component { 8 | 9 | static navigationOptions = { title: 'Create Wallet' }; 10 | 11 | state = { mnemonics: null }; 12 | 13 | onPressProceed() { 14 | const { mnemonics } = this.state; 15 | const { walletName, walletDescription } = this.props.navigation.state.params; 16 | this.props.navigation.navigate('ConfirmMnemonics', { mnemonics, walletName, walletDescription }); 17 | } 18 | 19 | onPressReveal() { 20 | const mnemonics = WalletUtils.generateMnemonics(); 21 | this.setState({ mnemonics }); 22 | } 23 | 24 | renderMnemonic = (mnemonic, index) => ( 25 | 26 | {mnemonic} 27 | 28 | ); 29 | 30 | renderBody() { 31 | const { mnemonics } = this.state; 32 | if (!mnemonics) return ; 33 | return ( 34 | 35 | {mnemonics.map(this.renderMnemonic)} 36 | 37 | ); 38 | } 39 | 40 | render() { 41 | return ( 42 | 43 | 44 | Save carefully your sequence of mnemonics: 45 | {this.renderBody()} 46 | 47 | {this.state.mnemonics && ( 48 | 49 | )} 50 | 51 | 52 | ); 53 | } 54 | } 55 | 56 | const styles = StyleSheet.create({ 57 | container: { 58 | alignItems: 'center', 59 | justifyContent: 'space-between', 60 | flex: 1, 61 | backgroundColor: colors.defaultBackground, 62 | padding: measures.defaultPadding 63 | }, 64 | message: { 65 | color: colors.black, 66 | fontSize: 16, 67 | textAlign: 'center', 68 | marginVertical: measures.defaultMargin, 69 | marginHorizontal: 32 70 | }, 71 | mnemonicsContainer: { 72 | flexDirection: 'row', 73 | justifyContent: 'center', 74 | flexWrap: 'wrap', 75 | maxWidth: '80%' 76 | }, 77 | mnemonic: { 78 | margin: 4 79 | }, 80 | buttonsContainer: { 81 | width: '100%', 82 | justifyContent: 'flex-end', 83 | height: 104 84 | } 85 | }); -------------------------------------------------------------------------------- /src/components/views/ConfirmMnemonics/ConfirmBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, TouchableOpacity, View, Text } from 'react-native'; 3 | import _ from 'lodash'; 4 | import { TextBullet } from '@components/widgets'; 5 | import { colors } from '@common/styles'; 6 | 7 | export default class ConfirmBox extends React.Component { 8 | 9 | state = { selectable: [], selected: [] }; 10 | 11 | isValidSequence() { 12 | return _.isEqual(this.props.mnemonics, this.state.selected); 13 | } 14 | 15 | static getDerivedStateFromProps(nextProps, prevState) { 16 | const newState = { ...prevState }; 17 | if (!prevState.selectable.length && !prevState.selected.length) { 18 | newState.selectable = _.shuffle([...nextProps.mnemonics]); 19 | } 20 | return newState; 21 | } 22 | 23 | onPressMnemonic(mnemonic, isSelected) { 24 | const { selectable, selected } = this.state; 25 | if (isSelected) this.setState({ 26 | selectable: selectable.filter(m => m !== mnemonic), 27 | selected: selected.concat([mnemonic]) 28 | }); 29 | else this.setState({ 30 | selectable: selectable.concat([mnemonic]), 31 | selected: selected.filter(m => m !== mnemonic) 32 | }); 33 | } 34 | 35 | renderMnemonic = (mnemonic, index, selected) => ( 36 | this.onPressMnemonic(mnemonic, selected)}> 37 | 38 | {mnemonic} 39 | 40 | 41 | ); 42 | 43 | renderSelected = () => ( 44 | 45 | {this.state.selected.map((mnemonic, index) => this.renderMnemonic(mnemonic, index, false))} 46 | 47 | ); 48 | 49 | renderSelectable = () => ( 50 | 51 | {this.state.selectable.map((mnemonic, index) => this.renderMnemonic(mnemonic, index, true))} 52 | 53 | ); 54 | 55 | render() { 56 | return ( 57 | 58 | Sequence: 59 | {this.renderSelected()} 60 | Click on the words in the correct order: 61 | {this.renderSelectable()} 62 | 63 | ); 64 | } 65 | } 66 | 67 | const styles = StyleSheet.create({ 68 | container: { 69 | flexDirection: 'column', 70 | alignItems: 'center' 71 | }, 72 | mnemonics: { 73 | flexDirection: 'row', 74 | flexWrap: 'wrap', 75 | justifyContent: 'center', 76 | margin: 4 77 | }, 78 | mnemonic: { 79 | margin: 4 80 | }, 81 | text: { 82 | color: colors.black 83 | } 84 | }); -------------------------------------------------------------------------------- /src/components/views/WalletsOverview/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList, RefreshControl, StyleSheet, View } from 'react-native'; 3 | 4 | import { inject, observer } from 'mobx-react'; 5 | import { HeaderIcon } from '@components/widgets'; 6 | import { colors, measures } from '@common/styles'; 7 | 8 | import { Container, Card, CardItem, Text, Body } from 'native-base'; 9 | import { Grid } from 'react-native-easy-grid'; 10 | 11 | import { General as GeneralActions, Wallets as WalletActions, Prices as PricesActions } from '@common/actions'; 12 | 13 | 14 | import NoWallets from './NoWallets'; 15 | import TotalBalance from './TotalBalance'; 16 | import WalletCard from './WalletCard'; 17 | 18 | @inject('prices', 'wallets') 19 | @observer 20 | export class WalletsOverview extends React.Component { 21 | 22 | static navigationOptions = ({ navigation, screenProps }) => ({ 23 | title: 'Home', 24 | headerRight: ( 25 | navigation.navigate('Settings')} /> 31 | ) 32 | }); 33 | 34 | get loading() { 35 | return this.props.prices.loading || this.props.wallets.loading; 36 | } 37 | 38 | componentDidMount() { 39 | this.populate(); 40 | } 41 | 42 | async populate() { 43 | try { 44 | await Promise.all([ 45 | WalletActions.loadWallets(), 46 | PricesActions.getPrice() 47 | ]); 48 | } catch (e) { 49 | GeneralActions.notify(e.message, 'long'); 50 | } 51 | } 52 | 53 | onPressWallet(wallet) { 54 | if (this.loading) return; 55 | WalletActions.selectWallet(wallet); 56 | this.props.navigation.navigate('WalletDetails', { wallet }); 57 | } 58 | 59 | renderItem = ({ item }) => this.onPressWallet(item)} /> 60 | 61 | renderBody = (list) => ( 62 | 63 | 64 | this.populate()} />} 68 | keyExtractor={(item, index) => String(index)} 69 | renderItem={this.renderItem} /> 70 | 71 | 72 | ); 73 | 74 | render() { 75 | const { list } = this.props.wallets; 76 | const { navigation } = this.props; 77 | return ( 78 | 79 | {(!list.length && !this.loading) ? : this.renderBody(list)} 80 | 81 | ); 82 | } 83 | } 84 | 85 | const styles = StyleSheet.create({ 86 | content: { 87 | marginTop: measures.defaultMargin 88 | } 89 | }); -------------------------------------------------------------------------------- /src/components/widgets/Camera/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, TouchableWithoutFeedback, Vibration, View } from 'react-native'; 3 | import { RNCamera } from 'react-native-camera'; 4 | import Modal from 'react-native-modal'; 5 | import Permissions from 'react-native-permissions'; 6 | import { Icon } from '@components/widgets'; 7 | import { colors } from '@common/styles'; 8 | 9 | const { BarCodeType } = RNCamera.Constants; 10 | 11 | export class Camera extends React.Component { 12 | 13 | state = { isModalVisible: false }; 14 | 15 | async show() { 16 | var status; 17 | try { 18 | status = await Permissions.check('camera'); 19 | if (status === 'authorized') this.setState({ isModalVisible: true }); 20 | else { 21 | status = await Permissions.request('camera'); 22 | if (status === 'authorized') this.setState({ isModalVisible: true }); 23 | else throw new Error('Not allowed to use the camera.'); 24 | } 25 | } catch (e) { 26 | console.error(e); 27 | this.setState({ isModalVisible: false }); 28 | } 29 | } 30 | 31 | hide() { 32 | this.setState({ isModalVisible: false }); 33 | } 34 | 35 | onBarCodeRead({ type, data }) { 36 | if (type === BarCodeType.qr) { 37 | Vibration.vibrate(); 38 | this.hide(); 39 | this.props.onBarCodeRead(data); 40 | } 41 | } 42 | 43 | renderView = (onClose) => ( 44 | 45 | this.onBarCodeRead(data)} /> 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | 56 | render() { 57 | const { modal, onClose } = this.props; 58 | return !modal ? this.renderView(onClose) : ( 59 | 63 | ); 64 | } 65 | } 66 | 67 | const styles = StyleSheet.create({ 68 | container: { 69 | flex: 1, 70 | alignItems: 'stretch', 71 | justifyContent: 'center', 72 | }, 73 | camera: { 74 | flex: 1 75 | }, 76 | closeIcon: { 77 | position: 'absolute', 78 | zIndex: 1, 79 | top: 8, 80 | right: 10 81 | }, 82 | marker: { 83 | position: 'absolute', 84 | alignSelf: 'center', 85 | zIndex: 1, 86 | width: 200, 87 | height: 200, 88 | borderWidth: 4, 89 | borderColor: 'green' 90 | } 91 | }); -------------------------------------------------------------------------------- /src/components/views/NewWalletName/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Keyboard, StyleSheet, Text, TextInput, View } from 'react-native'; 3 | import { Button } from '@components/widgets'; 4 | import { colors, measures } from '@common/styles'; 5 | 6 | export class NewWalletName extends React.Component { 7 | 8 | static navigationOptions = { title: 'New Wallet Name' }; 9 | 10 | state = { walletName: '', walletDescription: '' }; 11 | 12 | onPressContinue() { 13 | Keyboard.dismiss(); 14 | const { walletName, walletDescription } = this.state; 15 | if (!walletName) return; 16 | this.props.navigation.navigate('NewWallet', { walletName, walletDescription }); 17 | } 18 | 19 | render() { 20 | return ( 21 | 22 | 23 | Give a name to the new wallet 24 | this.setState({ walletName })} /> 29 | Give a description too (optional) 30 | this.setState({ walletDescription })} /> 35 | 36 | 37 |