diff --git a/__tests__/src/DAppsSDK/0.0.1/utils/renderer.js b/__tests__/src/DAppsSDK/0.0.1/utils/renderer.js new file mode 100644 index 000000000..d5d120201 --- /dev/null +++ b/__tests__/src/DAppsSDK/0.0.1/utils/renderer.js @@ -0,0 +1,256 @@ +import React from 'react'; + +import { getTypeElementFromText, renderJSON, validateProps } from '../../../../../src/DAppsSDK/0.0.1/utils/renderer'; +import View from '../../../../../src/DAppsSDK/0.0.1/components/View'; +import Text from '../../../../../src/DAppsSDK/0.0.1/components/Text'; +import TextInput from '../../../../../src/DAppsSDK/0.0.1/components/TextInput'; +import Button from '../../../../../src/DAppsSDK/0.0.1/components/Button'; + +React.createElement = jest.fn().mockImplementation((component, props, children) => ({ + component, + props, + children, +})); + +test('getTypeElementFromText', () => { + expect(getTypeElementFromText('text')).toEqual(Text); + expect(getTypeElementFromText('view')).toEqual(View); + expect(getTypeElementFromText('textInput')).toEqual(TextInput); + expect(getTypeElementFromText('button')).toEqual(Button); + expect(getTypeElementFromText('something unknown')).toBeUndefined(); +}); + +test('validateProps', () => { + expect(validateProps( + { + allowedNativeProp1: 'ALLOWED_NATIVE_PROP_1', + allowedNativeProp2: 'ALLOWED_NATIVE_PROP_2', + disallowedNativeProp3: 'DISALLOWED_NATIVE_PROP_3', + someCustomProp1: 'SOME_CUSTOM_PROP_1', + someCustomProp2: 'SOME_CUSTOM_PROP_2', + badCustomProp: 'BAD_CUSTOM_PROP', + onPress: 'ON_PRESS', + badCallback: 'BAD_CALLBACK', + }, { + native: ['allowedNativeProp1', 'allowedNativeProp2'], + custom: ['someCustomProp1', 'someCustomProp2'], + callbacks: ['onPress'], + }, + 'COMPONENT_TYPE', + )).toEqual({ + nativeProps: { + allowedNativeProp1: 'ALLOWED_NATIVE_PROP_1', + allowedNativeProp2: 'ALLOWED_NATIVE_PROP_2', + }, + customProps: { + someCustomProp1: 'SOME_CUSTOM_PROP_1', + someCustomProp2: 'SOME_CUSTOM_PROP_2', + }, + callbackProps: { + onPress: 'ON_PRESS', + }, + }); +}); + +describe('renderJSON', () => { + test('single view', () => { + expect(renderJSON({ + type: 'view', + props: {}, + children: null, + }, undefined, () => ({}))).toEqual({ + component: View, + props: { nativeProps: {} }, + children: null, + }); + }); + + test('unknown component', () => { + expect(renderJSON({ type: 'Something unknown' }, undefined, () => ({}))).toBeNull(); + }); + + test('string literal outside text', () => { + expect(renderJSON({ type: 'view', children: ['Something unknown'], props: {} }, undefined, () => ({}))).toEqual({ + component: View, + props: { nativeProps: {} }, + children: [null], + }); + + expect(renderJSON({ type: 'view', children: 'Something unknown', props: {} }, undefined, () => ({}))).toEqual({ + component: View, + props: { nativeProps: {} }, + children: null, + }); + }); + + test('string literal inside text', () => { + expect(renderJSON({ type: 'text', children: 'Something unknown', props: {} }, undefined, () => ({}))).toEqual({ + component: Text, + props: { nativeProps: {} }, + children: 'Something unknown', + }); + }); + + test('number literal inside text', () => { + expect(renderJSON({ type: 'text', children: 5, props: {} }, undefined, () => ({}))).toEqual({ + component: Text, + props: { nativeProps: {} }, + children: 5, + }); + }); + + test('number literal outside text', () => { + expect(renderJSON({ type: 'view', children: 5, props: {} }, undefined, () => ({}))).toEqual({ + component: View, + props: { nativeProps: {} }, + children: null, + }); + }); + + test('complex JSON', () => { + const json = { + type: 'view', + props: { style: { backgroundColor: 'yellow', flex: 1 } }, + children: [ + { + type: 'text', + props: { style: { color: 'red' } }, + children: [ + 'Red text', + ], + }, + { + type: 'view', + props: { style: { backgroundColor: 'blue' } }, + children: [ + { + type: 'text', + props: { style: { color: 'white' } }, + children: [{ + type: 'text', + props: { style: { color: 'red' } }, + children: [ + 'White bold text', + ], + }, + 'And text', + ], + }, + ], + }, + { + type: 'textInput', + props: { + style: { width: 200, height: 50 }, + onEndEditing: 1, + }, + }, + { + type: 'textInput', + props: { style: { width: 200, height: 50 } }, + }, + { + type: 'button', + props: { + style: { width: 100, height: 50, backgroundColor: 'green' }, + title: 'Hey', + onPress: 2, + }, + }, + ], + }; + + const customPropsProvider = (component, ownProps) => ({ + ownPropsCount: Object.keys(ownProps).length, + }); + + expect(renderJSON(json, undefined, customPropsProvider)).toEqual({ + component: View, + props: { + nativeProps: { style: { backgroundColor: 'yellow', flex: 1 } }, + key: undefined, + ownPropsCount: 1, + }, + children: [ + { + component: Text, + props: { + nativeProps: {}, + style: { color: 'red' }, + key: '0', + ownPropsCount: 1, + }, + children: [ + 'Red text', + ], + }, + { + component: View, + props: { + nativeProps: { style: { backgroundColor: 'blue' } }, + key: '1', + ownPropsCount: 1, + }, + children: [ + { + component: Text, + props: { + nativeProps: {}, + style: { color: 'white' }, + key: '0', + ownPropsCount: 1, + }, + children: [{ + component: Text, + props: { + nativeProps: {}, + style: { color: 'red' }, + key: '0', + ownPropsCount: 1, + }, + children: [ + 'White bold text', + ], + }, + 'And text', + ], + }, + ], + }, + { + children: null, + component: TextInput, + props: { + nativeProps: { + style: { width: 200, height: 50 }, + }, + key: '2', + ownPropsCount: 2, + }, + }, + { + children: null, + component: TextInput, + props: { + nativeProps: { + style: { width: 200, height: 50 }, + }, + key: '3', + ownPropsCount: 1, + }, + }, + { + children: null, + component: Button, + props: { + nativeProps: {}, + style: { width: 100, height: 50, backgroundColor: 'green' }, + title: 'Hey', + key: '4', + ownPropsCount: 3, + }, + }, + ], + }); + }); +}); diff --git a/__tests__/src/sagas/activity/sagas.js b/__tests__/src/sagas/activity/sagas.js index 4505229a9..a2faaeec2 100644 --- a/__tests__/src/sagas/activity/sagas.js +++ b/__tests__/src/sagas/activity/sagas.js @@ -26,7 +26,7 @@ describe('onCurrentAccountChange', () => { version: 3, display: true, interpret: true, - created_at: new Date(), + createdAt: new Date(), }; const gen = onCurrentAccountChange([mockMessage]); @@ -64,7 +64,7 @@ describe('addNewMessageSaga', () => { const mockAction = { type: ADD_NEW_MESSAGE, message: 'Test Message', - params: '', + params: {}, interpret: true, callback: jest.fn(), }; @@ -82,7 +82,7 @@ describe('addNewMessageSaga', () => { const mockMessage = buildMessageObject(1, testAccountId, mockAction.message, mockAction.params, mockAction.interpret); const convertedMessage = convertToDatabase(mockMessage); - delete convertedMessage.created_at; + delete convertedMessage.createdAt; delete convertedMessage.params; expect(realm.objects('MessageJob').filtered(`accountId == '${testAccountId}'`)[0]) .toMatchObject({ diff --git a/__tests__/src/sagas/index.js b/__tests__/src/sagas/index.js index e18eb0a93..038fc679d 100644 --- a/__tests__/src/sagas/index.js +++ b/__tests__/src/sagas/index.js @@ -13,9 +13,9 @@ import serviceContainer from '../../../src/sagas/serviceContainer'; import modifyNation from '../../../src/sagas/modifyNation'; import nations from '../../../src/sagas/nations'; import txProcessor from '../../../src/sagas/txProcessor'; -import dApps from '../../../src/sagas/nativeDApps'; import chat from '../../../src/sagas/chat'; import migration from '../../../src/sagas/migration'; +import dApps from '../../../src/sagas/dApps'; import upstream from '../../../src/sagas/upstream'; test('rootSaga', () => { @@ -32,6 +32,7 @@ test('rootSaga', () => { call(modifyNation), call(nations), call(txProcessor), + call(migration), call(dApps), call(migration), call(upstream), diff --git a/__tests__/src/screens/Dashboard/__snapshots__/index.js.snap b/__tests__/src/screens/Dashboard/__snapshots__/index.js.snap index 0c12bcd02..7646ed7c9 100644 --- a/__tests__/src/screens/Dashboard/__snapshots__/index.js.snap +++ b/__tests__/src/screens/Dashboard/__snapshots__/index.js.snap @@ -55,7 +55,6 @@ ShallowWrapper { }, "onAddDummyMessage": [Function], "onSelectNation": [Function], - "openDApp": [Function], "store": Object { "clearActions": [Function], "dispatch": [Function], @@ -119,7 +118,6 @@ ShallowWrapper { }, "onAddDummyMessage": [Function], "onSelectNation": [Function], - "openDApp": [Function], "store": Object { "clearActions": [Function], "dispatch": [Function], diff --git a/__tests__/src/services/database/index.js b/__tests__/src/services/database/index.js index 7d47962c1..5cfc0f198 100644 --- a/__tests__/src/services/database/index.js +++ b/__tests__/src/services/database/index.js @@ -42,23 +42,42 @@ describe('db', () => { realm2.close(); }); - test('schema v3 - v4', async () => { - expect.assertions(4); + test('schema v3 - v6', async () => { + expect.assertions(5); const dbPath = randomDbPath(); const databaseGenerator = factory(dbPath, 3); - const realm3 = await databaseGenerator.next().value; + const realm3: Realm = await databaseGenerator.next().value; expect(Realm.schemaVersion(dbPath)).toBe(3); - const realm4 = await databaseGenerator.next(realm3).value; + const realm4: Realm = await databaseGenerator.next(realm3).value; expect(Realm.schemaVersion(dbPath)).toBe(4); - const realm5 = await databaseGenerator.next(realm4).value; + const realm5: Realm = await databaseGenerator.next(realm4).value; expect(Realm.schemaVersion(dbPath)).toBe(5); - const realm6 = await databaseGenerator.next(realm5).value; + // Add profile to DB + realm5.write(() => { + realm5.create('Profile', { + name: 'name', + location: 'location', + image: 'image', + identity_pub_key: 'identity_pub_key', + ethereum_pub_Key: 'ethereum_pub_Key', + chat_id_key: 'chat_id_key', + timestamp: new Date(), + version: 0, + identity_key_signature: 'identity_key_signature', + ethereum_key_signature: 'ethereum_key_signature', + }); + }); + + const realm6: Realm = await databaseGenerator.next(realm5).value; expect(Realm.schemaVersion(dbPath)).toBe(6); + // Check that profile was deleted correctly. + expect(realm6.objects('Profile')).toHaveLength(0); + realm6.close(); }); }); diff --git a/__tests__/src/services/ethereum/index.js b/__tests__/src/services/ethereum/index.js index f5fa0eb4e..1918eda56 100644 --- a/__tests__/src/services/ethereum/index.js +++ b/__tests__/src/services/ethereum/index.js @@ -5,7 +5,6 @@ import ethers from 'ethers'; import providers from 'ethers/providers'; import Ethereum from '../../../../src/services/ethereum/index'; -import ContractInfo from '../../../../src/dapps/escrow/ERC20TokenEscrow.json'; describe('ethereum', () => { let ethereum; @@ -41,18 +40,4 @@ describe('ethereum', () => { const numCitizens = await nationsObject.getNumCitizens(0); expect(numCitizens.toNumber()).toEqual(0); }); - test('Create contract test', async () => { - const wallet = new ethers.Wallet('0xefc27ba5330258fcfb75e28e4e6efd88458751086998bbfad99257035fb3e160'); - wallet.provider = new providers.InfuraProvider('rinkeby'); - ethereum = new Ethereum(wallet, 'dev'); - const txReceipt = await ethereum.deployContract( - ContractInfo.bytecode, - ContractInfo.abi, - '0', - '0xC3830A6206fB9d089D1ce824598978532D14d8Aa', - '0', - '0', - '0xcd4dd4fd12acD06fD49509516Bb136A0B496d451', - ); - }); }); diff --git a/__tests__/src/utils/mapping/activity.js b/__tests__/src/utils/mapping/activity.js index 460837aa7..bef993dba 100644 --- a/__tests__/src/utils/mapping/activity.js +++ b/__tests__/src/utils/mapping/activity.js @@ -11,7 +11,7 @@ describe('convert message to database', () => { msg: 'test message', params: '', interpret: true, - created_at: new Date(), + createdAt: new Date(), }; expect(convertToDatabase(msgObj)).toEqual({ @@ -23,7 +23,7 @@ describe('convert message to database', () => { version: 1, display: true, interpret: msgObj.interpret, - created_at: msgObj.created_at, + createdAt: msgObj.createdAt, }); }); }); @@ -39,7 +39,7 @@ describe('convert message from database', () => { version: 1, display: true, interpret: true, - created_at: '737545435435', + createdAt: '737545435435', }; expect(convertFromDatabase(message)).toEqual({ @@ -48,7 +48,7 @@ describe('convert message from database', () => { msg: message.msg, params: message.params, interpret: message.interpret, - created_at: message.created_at, + createdAt: message.createdAt, }); }); }); diff --git a/android/app/src/main/java/co/bitnation/PanthalassaModule.java b/android/app/src/main/java/co/bitnation/PanthalassaModule.java index 7c62be510..e848692c5 100644 --- a/android/app/src/main/java/co/bitnation/PanthalassaModule.java +++ b/android/app/src/main/java/co/bitnation/PanthalassaModule.java @@ -25,7 +25,7 @@ * Created by Estarrona on 19/04/18. */ -public class PanthalassaModule extends ReactContextBaseJavaModule { +public class PanthalassaModule extends ReactContextBaseJavaModule implements UpStream { final String TAG = "Panthalassa"; UpStream client, ui; @@ -382,7 +382,7 @@ public void PanthalassaCallDAppFunction(final ReadableMap jsonParams, final Prom new Thread(new Runnable() { public void run() { try { - Panthalassa.callDAppFunction(jsonParams.getString("dAppId"), + Panthalassa.callDAppFunction(jsonParams.getString("signingKey"), jsonParams.getInt("id"), jsonParams.getString("args")); promise.resolve(true); @@ -519,6 +519,26 @@ public void run() { //===== + // This method should be deleted due is not the active protocol listener now + @Override + public void send(String s) { + Log.v("Upstream","Received from callback"); + + WritableMap params = Arguments.createMap(); + params.putString("upstream", s); + Activity activity = getCurrentActivity(); + if (activity != null) { + MainApplication application = (MainApplication) activity.getApplication(); + ReactNativeHost reactNativeHost = application.getReactNativeHost(); + ReactInstanceManager reactInstanceManager = reactNativeHost.getReactInstanceManager(); + ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); + + if (reactContext != null) { + sendEvent(reactContext, "PanthalassaUpStream", params); + } + } + } + private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) { diff --git a/android/panthalassa/panthalassa.aar b/android/panthalassa/panthalassa.aar index fe915494a..6bcf3b368 100644 Binary files a/android/panthalassa/panthalassa.aar and b/android/panthalassa/panthalassa.aar differ diff --git a/ios/Frameworks/panthalassa.framework/Versions/A/Panthalassa b/ios/Frameworks/panthalassa.framework/Versions/A/Panthalassa index 1f43516e1..cbd6c90b6 100644 Binary files a/ios/Frameworks/panthalassa.framework/Versions/A/Panthalassa and b/ios/Frameworks/panthalassa.framework/Versions/A/Panthalassa differ diff --git a/ios/Pangea.xcodeproj/project.pbxproj b/ios/Pangea.xcodeproj/project.pbxproj index 58cf9f3c0..138005ab0 100644 --- a/ios/Pangea.xcodeproj/project.pbxproj +++ b/ios/Pangea.xcodeproj/project.pbxproj @@ -991,6 +991,7 @@ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 29461E77F15976B43EDDE297 /* [CP] Copy Pods Resources */, 172769071FFB316400AD076B /* Embed Frameworks */, + AE64DAC15102E61A536228EB /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -1576,13 +1577,12 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-Pangea/Pods-Pangea-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/QBImagePickerController/QBImagePicker.bundle", + "$PODS_CONFIGURATION_BUILD_DIR/QBImagePickerController/QBImagePicker.bundle", "${PODS_ROOT}/RSKImageCropper/RSKImageCropper/RSKImageCropperStrings.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RSKImageCropperStrings.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1621,6 +1621,21 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + AE64DAC15102E61A536228EB /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Pangea/Pods-Pangea-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/ios/Panthalassa.m b/ios/Panthalassa.m index 3c6b7f96b..81f088918 100644 --- a/ios/Panthalassa.m +++ b/ios/Panthalassa.m @@ -408,7 +408,7 @@ - (void)initUpstreams { BOOL response; NSError *error = nil; - response = PanthalassaCallDAppFunction([RCTConvert NSString:config[@"dAppId"]], + response = PanthalassaCallDAppFunction([RCTConvert NSString:config[@"signingKey"]], [[RCTConvert NSNumber:config[@"id"]] longValue], [RCTConvert NSString:config[@"args"]], &error); diff --git a/src/components/nativeDApps/AmountSelect.js b/src/DAppsSDK/0.0.1/components/AmountSelect/AmountSelect.js similarity index 73% rename from src/components/nativeDApps/AmountSelect.js rename to src/DAppsSDK/0.0.1/components/AmountSelect/AmountSelect.js index e699b8001..1a35730e5 100644 --- a/src/components/nativeDApps/AmountSelect.js +++ b/src/DAppsSDK/0.0.1/components/AmountSelect/AmountSelect.js @@ -8,10 +8,10 @@ import ActionSheet from 'react-native-actionsheet'; import { BigNumber } from 'bignumber.js'; import _ from 'lodash'; -import i18n from '../../global/i18n'; -import GlobalStyles from '../../global/Styles'; -import Colors from '../../global/colors'; -import type { CurrencyType, WalletType } from '../../types/Wallet'; +import i18n from '../../../../global/i18n'; +import GlobalStyles from '../../../../global/Styles'; +import Colors from '../../../../global/colors'; +import type { CurrencyType, WalletType } from '../../../../types/Wallet'; export type InternalProps = { /** @@ -26,13 +26,13 @@ export type Props = { */ style?: Object, /** - * @desc + * @desc Function that called on each change. */ - onAmountSelected: (amount: string, currency: CurrencyType, walletAddress: string, isValid: boolean) => void, + onAmountSelected: (amount: string, currency: CurrencyType, walletAddress: string, isValidAmount: boolean, isLessThanBalance: boolean) => void, /** - * @desc Flag whether amount is invalid if it greater than balance. + * @desc Function that called on end editing with result. */ - shouldCheckLess: boolean, + onFinalChange: (amount: string, currency: CurrencyType, walletAddress: string, isValid: boolean, isLessThanBalance: boolean) => void, /** * @desc Amount to show on a component. */ @@ -52,6 +52,12 @@ export default class AmountSelect extends Component { changeCurrencyEnabled: true, }; + constructor(props: Props & InternalProps) { + super(props); + + this.reportFinalChange(); + } + onSelectWallet = (index: number) => { if (index < this.props.wallets.length) { const wallet = this.props.wallets[index]; @@ -59,8 +65,10 @@ export default class AmountSelect extends Component { this.props.amount, wallet.currency, wallet.ethAddress, - this.isValidAmount(this.props.amount, wallet), + this.isValidAmount(this.props.amount), + this.isLessThanBalance(this.props.amount, wallet), ); + this.reportFinalChange(); } }; @@ -72,7 +80,8 @@ export default class AmountSelect extends Component { amount, wallet.currency, wallet.ethAddress, - this.isValidAmount(amount, wallet), + this.isValidAmount(amount), + this.isLessThanBalance(this.props.amount, wallet), ); }; @@ -81,22 +90,45 @@ export default class AmountSelect extends Component { return _.find(this.props.wallets, (wallet => wallet.currency === this.props.currency)); } - isValidAmount(amount: string, wallet: WalletType): boolean { + reportFinalChange = () => { + const wallet = this.getWallet(); + if (wallet == null) return; + + this.props.onFinalChange( + this.props.amount, + wallet.currency, + wallet.ethAddress, + this.isValidAmount(this.props.amount), + this.isLessThanBalance(this.props.amount, wallet), + ); + }; + + isValidAmount(amount: string): boolean { if (amount == null || amount.length === 0) return false; try { const bnAmount = new BigNumber(amount); if (bnAmount.isZero()) return false; if (!bnAmount.isFinite()) return false; - if (this.props.shouldCheckLess) { - return bnAmount.lessThanOrEqualTo(new BigNumber(wallet.balance)); - } return true; } catch (e) { return false; } } + isLessThanBalance(amount: string, wallet: WalletType): boolean { + if (amount == null || amount.length === 0) return false; + + try { + const bnAmount = new BigNumber(amount); + if (bnAmount.isZero()) return false; + if (!bnAmount.isFinite()) return false; + return bnAmount.lessThanOrEqualTo(new BigNumber(wallet.balance)); + } catch (e) { + return false; + } + } + actionSheet: any; render() { @@ -134,6 +166,7 @@ export default class AmountSelect extends Component { placeholder='0.00000' placeholderTextColor={Colors.placeholderTextColor} onChangeText={this.onChangeAmount} + onEndEditing={this.reportFinalChange} value={this.props.amount} keyboardType='numeric' /> diff --git a/src/DAppsSDK/0.0.1/components/AmountSelect/AmountSelectController.js b/src/DAppsSDK/0.0.1/components/AmountSelect/AmountSelectController.js new file mode 100644 index 000000000..2ded3e043 --- /dev/null +++ b/src/DAppsSDK/0.0.1/components/AmountSelect/AmountSelectController.js @@ -0,0 +1,90 @@ +/* eslint-disable no-use-before-define */ +// @flow + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import type { InternalProps } from './AmountSelect'; +import AmountSelect from './AmountSelect'; +import type { CurrencyType } from '../../../../types/Wallet'; + +export type Props = { + /** + * @desc Style to apply to container view. + */ + style?: Object, + /** + * @desc Callback on selected amount change. + */ + onAmountSelected: ({ amount: string, currency: CurrencyType, walletAddress: string, isValid: boolean, isLessThanBalance: boolean }) => void, + /** + * @desc Amount to be set at component constructing + */ + initialAmount: string, + /** + * @desc Currency to be set at component constructing + */ + initialCurrency: string, + nativeProps: { + /** + * @desc Style object to pass to container. + */ + style: Object, + }, +} + +class AmountSelectController extends Component { + static validNativeProps = [ + 'style', + 'changeCurrencyEnabled', + ]; + + static callbackProps = [ + 'onAmountSelected', + ]; + + static customProps = [ + 'initialAmount', + 'initialCurrency', + ]; + + constructor(props: Props & InternalProps) { + super(props); + + this.state = { + amount: this.props.initialAmount || '', + currency: this.props.initialCurrency || 'ETH', + }; + } + + render() { + return ( + { + this.setState({ amount, currency }); + }} + onFinalChange={(amount, currency, address, isValid, isLessThanBalance) => { + this.props.onAmountSelected({ + amount, + currency, + walletAddress: address, + isValid, + isLessThanBalance, + }); + }} + wallets={this.props.wallets} + /> + ); + } +} + +const mapStateToProps = state => ({ + wallets: state.wallet.wallets, +}); + +const mapDispatchToProps = () => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(AmountSelectController); diff --git a/src/DAppsSDK/0.0.1/components/Button.js b/src/DAppsSDK/0.0.1/components/Button.js new file mode 100644 index 000000000..2d24de448 --- /dev/null +++ b/src/DAppsSDK/0.0.1/components/Button.js @@ -0,0 +1,121 @@ +// @flow + +import React, { Component } from 'react'; +import { + TouchableOpacity, + View, + Text, +} from 'react-native'; + +import styles from '../../../global/Styles'; + +type Props = { + /** + * @desc Props that should be passed as they are to backed native component. + */ + nativeProps: Object, + /** + * @desc Name of predefined type of component. + */ + type: string, + /** + * @desc Style object. + */ + style?: Object, + /** + * @desc Title of the button. + */ + title: string, + /** + * @desc Style of button title. + */ + titleStyle?: Object, + /** + * @desc Set if button should be disabled. + */ + disabled: boolean, + /** + * @desc Callback to be called when button is pressed. + */ + onPress: () => any, +} + +export default class Button extends Component { + static validNativeProps = [ + 'disabled', + ]; + + static callbackProps = [ + 'onPress', + ]; + + static customProps = [ + 'type', + 'title', + 'style', + 'titleStyle', + ]; + + static defaultProps = { + type: 'transparent', + disabled: false, + title: '', + onPress: () => undefined, + }; + + rootStyleForType(type: string) { + switch (type) { + case 'transparent': + return styles.baseButton; + case 'action': + return styles.actionButton; + case 'custom': + return null; + default: + return undefined; + } + } + + titleStyleForType(type: string) { + switch (type) { + case 'transparent': + return styles.buttonTitle; + case 'action': + return styles.actionButtonTitle; + case 'custom': + return null; + default: + return undefined; + } + } + + render() { + const typeStyle = this.rootStyleForType(this.props.type); + if (typeStyle === undefined) { + console.warn(`Invalid value '${this.props.type}' for 'type' property of 'Button' component`); + } + + return ( + + + this.props.onPress()} + > + { + + {this.props.title} + + } + + + ); + } +} diff --git a/src/DAppsSDK/0.0.1/components/Root.js b/src/DAppsSDK/0.0.1/components/Root.js new file mode 100644 index 000000000..860ef44b8 --- /dev/null +++ b/src/DAppsSDK/0.0.1/components/Root.js @@ -0,0 +1,79 @@ +// @flow + +import React, { Component } from 'react'; +import { TouchableWithoutFeedback, View as ReactNativeView, Keyboard } from 'react-native'; +import { connect } from 'react-redux'; + +import { type ComponentsJSON, renderJSON } from '../utils/renderer'; + +import { performDAppCallback } from '../../../actions/dApps'; +import GlobalStyles from '../../../global/Styles'; + +export type Props = { + /** + * @desc Layout in JSON form provided by DApp developer to be rendered inside Root component + */ + layout: ComponentsJSON, + /** + * @desc Public key of DApp that view is related to. + */ + dAppPublicKey: string, + /** + * @desc Action to perform a DApp callback. + */ + performDAppCallback: (appId: string, callbackID: number, args: Object) => void, +} + +class Root extends Component { + performCallbackByID = (callbackID: number, args: Object) => { + this.props.performDAppCallback(this.props.dAppPublicKey, callbackID, args); + }; + + generateCustomProps = (component: any, ownProps: Object) => { + if (component == null) { + return {}; + } + + const { callbackProps = [] } = component; + const resultedProps = {}; + + if (Array.isArray(callbackProps)) { + callbackProps.forEach((callbackName) => { + if (typeof callbackName !== 'string') { + return; + } + + const callbackID = ownProps[callbackName]; + if (callbackID == null) return; + if (typeof callbackID !== 'number') { + console.warn(`Callback id ${callbackID} must be a number.`); + return; + } + + resultedProps[callbackName] = data => this.performCallbackByID(callbackID, data); + }); + } + + return resultedProps; + }; + + render() { + return ( + + + {renderJSON(this.props.layout, undefined, this.generateCustomProps)} + + + ); + } +} + +const mapStateToProps = () => ({}); + +const mapDispatchToProps = dispatch => ({ + performDAppCallback(dAppId, callbackId, payload) { + dispatch(performDAppCallback(dAppId, callbackId, { payload })); + }, +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Root); diff --git a/src/DAppsSDK/0.0.1/components/Text.js b/src/DAppsSDK/0.0.1/components/Text.js new file mode 100644 index 000000000..f89796ac3 --- /dev/null +++ b/src/DAppsSDK/0.0.1/components/Text.js @@ -0,0 +1,85 @@ +// @flow + +import * as React from 'react'; +import { Text as ReactNativeText } from 'react-native'; + +import styles from '../../../global/Styles'; + +type Props = { + /** + * @desc Props that should be passed as they are to backed native component. + */ + nativeProps: Object, + /** + * @desc Name of predefined type of component. + */ + type: string, + /** + * @desc Style object. + */ + style?: Object, + /** + * @desc Children components. + */ + children: React.Node, +} + +const types = [ + 'largeTitle', + 'title1', + 'title2', + 'title3', + 'headline', + 'body', + 'bodyBold', + 'bodyBoldBlack', + 'bodyBoldBlackSmall', + 'bodyBlack', + 'callout', + 'subhead', + 'footnote', + 'caption1', + 'caption2', + 'disabledText', +]; + +export default class Text extends React.Component { + static validNativeProps = [ + 'numberOfLines', + 'ellipsizeMode', + ]; + + static customProps = [ + 'style', + 'type', + ]; + + static defaultProps = { + type: 'body', + }; + + styleForType(type: string) { + if (types.includes(type) === false) { + return null; + } + + return styles[type]; + } + + render() { + const typeStyle = this.props.type ? this.styleForType(this.props.type) : null; + if (this.props.type != null && typeStyle == null) { + console.warn(`Invalid value '${this.props.type}' for 'type' property of 'Text' component`); + } + + return ( + + {this.props.children} + + ); + } +} + diff --git a/src/DAppsSDK/0.0.1/components/TextInput.js b/src/DAppsSDK/0.0.1/components/TextInput.js new file mode 100644 index 000000000..26bb7c8cf --- /dev/null +++ b/src/DAppsSDK/0.0.1/components/TextInput.js @@ -0,0 +1,57 @@ +// @flow + +import React, { Component } from 'react'; + +import { TextInput as ReactNativeTextInput } from 'react-native'; + +type Props = { + /** + * @desc Props that should be passed as they are to backed native component. + */ + nativeProps: Object, + /** + * @desc Callback to be called when entering into text field is done. + */ + onEndEditing: (text: string) => any, +} + +export default class TextInput extends Component { + static validNativeProps = [ + 'style', + 'autoCorrect', + 'autoCapitalize', + 'defaultValue', + 'editable', + 'keyboardType', + 'maxLength', + 'multiline', + 'placeholder', + 'placeholderTextColor', + 'secureTextEntry', + 'spellCheck', + ]; + + static callbackProps = [ + 'onEndEditing', + ]; + + static defaultProps = { + onEndEditing: () => undefined, + }; + + constructor(props: Props) { + super(props); + this.state = { value: '' }; + } + + render() { + return ( + this.setState({ value })} + onEndEditing={() => this.props.onEndEditing(this.state.value)} + /> + ); + } +} diff --git a/src/DAppsSDK/0.0.1/components/View.js b/src/DAppsSDK/0.0.1/components/View.js new file mode 100644 index 000000000..ffe36aeb0 --- /dev/null +++ b/src/DAppsSDK/0.0.1/components/View.js @@ -0,0 +1,30 @@ +// @flow + +import * as React from 'react'; + +import { View as ReactNativeView } from 'react-native'; + +type Props = { + /** + * @desc Props that should be passed as they are to backed native component. + */ + nativeProps: Object, + /** + * @desc Children components. + */ + children: React.Node, +} + +export default class View extends React.Component { + static validNativeProps = [ + 'style', + ]; + + render() { + return ( + + {this.props.children} + + ); + } +} diff --git a/src/DAppsSDK/0.0.1/components/index.js b/src/DAppsSDK/0.0.1/components/index.js new file mode 100644 index 000000000..48c4c3f22 --- /dev/null +++ b/src/DAppsSDK/0.0.1/components/index.js @@ -0,0 +1,13 @@ +import Text from '../components/Text'; +import View from '../components/View'; +import TextInput from '../components/TextInput'; +import Button from '../components/Button'; +import AmountSelectController from './AmountSelect/AmountSelectController'; + +export default { + text: Text, + view: View, + textInput: TextInput, + button: Button, + amountSelect: AmountSelectController, +}; diff --git a/src/DAppsSDK/0.0.1/utils/renderer.js b/src/DAppsSDK/0.0.1/utils/renderer.js new file mode 100644 index 000000000..3120d72e2 --- /dev/null +++ b/src/DAppsSDK/0.0.1/utils/renderer.js @@ -0,0 +1,126 @@ +// @flow + +import React from 'react'; +import components from '../components'; + +export type ComponentsJSON = { type: string, props: Object, children: ?(Array | string | number) } | string | number; + +/** + * @desc Function to get element class or function from text representation, e.g. 'Text' means Text component. + * @param {string} typeName String representation of the type. + * @return {*} Class or function of component or null if type is unknown. + */ +export const getTypeElementFromText = (typeName: string) => components[typeName]; + +/** + * @desc Validates props against list of valid names and filter only whitelisted ones. + * Used to prevent DApp developer to pass something bad to components we provide. + * @param {Object} props Props to validate. + * @param {string[]} validProps Object containing arrays by keys `native`, `stateBased` and `callbacks` according to corresponding type of props. + * @param {string} type String representation of component type. + * @return {Object} Props filtered by groups. + */ +export const validateProps = ( + props: Object, + validProps: { native: Array, custom: Array, callbacks: Array }, + type: string, +) => { + const nativeProps = {}; + const customProps = {}; + const callbackProps = {}; + Object.keys(props).forEach((propName) => { + if (typeof propName !== 'string') { + console.warn('Prop name is not a string'); + return; + } + + if (validProps.native.includes(propName)) { + // It's an allowed native prop. + nativeProps[propName] = props[propName]; + } else if (validProps.custom.includes(propName)) { + // It's an allowed custom prop. + customProps[propName] = props[propName]; + } else if (validProps.callbacks.includes(propName)) { + // It's an allowed prop to perform a callback. + callbackProps[propName] = props[propName]; + } else { + console.warn(`Prop ${propName} is not allowed on component ${type}`); + } + }); + + return { nativeProps, customProps, callbackProps }; +}; + +/** + * @desc Renders JSON provided by DApp developer into react components. + * @param {ComponentsJSON} json JSON object that represents components to render. + * @param {?string} key Key of component (used for updates). + * @param {Function} customPropsProvider Function that takes component class or function and props and returns new props if it's needed to add ones. + * @param {*} parent Function or class of component that is parent of currently used or undefined for root. + * @return {*} Tree of components that is ready to be render. + */ +export const renderJSON = (json: ComponentsJSON, key: ?string, customPropsProvider: (component: any, ownProps: Object) => Object, parent: any) => { + const { type, children } = typeof json === 'object' ? json : {}; + + // This is the case when we have string or number literal inside or instead of children. + // It's only allowed for Text. + if (typeof json === 'string' || typeof json === 'number') { + if (parent === components.text) { + return json; + } + + if (typeof json === 'number') { + console.warn(`Number literal may only appear inside Text component. Found "${json}" outside.`); + } + if (typeof json === 'string') { + console.warn(`String literal may only appear inside Text component. Found "${json}" outside.`); + } + return null; + } + + const component = components[type]; + + if (component == null) { + console.warn(`Trying to render unknown component type ${type}.`); + return null; + } + + if (component.validNativeProps != null && Array.isArray(component.validNativeProps) === false) { + console.warn(`validNativeProps field of component ${type} should be an array of strings.`); + return null; + } + if (component.customProps != null && Array.isArray(component.customProps) === false) { + console.warn(`customProps field of component ${type} should be an array of strings.`); + return null; + } + if (component.callbackProps != null && Array.isArray(component.callbackProps) === false) { + console.warn(`callbackProps field of component ${type} should be an array of strings.`); + return null; + } + + const { props } = json; + + const { nativeProps = props, customProps = {}, callbackProps = {} } = + validateProps(json.props, { + native: component.validNativeProps || [], + custom: component.customProps || [], + callbacks: component.callbackProps || [], + }, type); + + return React.createElement( + component, + { + nativeProps, + ...customProps, + ...customPropsProvider(component, { ...nativeProps, ...customProps, ...callbackProps }), + key, + }, + (() => { + if (children == null) return null; + if (typeof children === 'string' || typeof children === 'number') { + return renderJSON(children, '0', customPropsProvider, component); + } + return children.map((child, index) => renderJSON(child, `${index}`, customPropsProvider, component)); + })(), + ); +}; diff --git a/src/actions/chat.js b/src/actions/chat.js index 0f5afaaf4..de502209e 100644 --- a/src/actions/chat.js +++ b/src/actions/chat.js @@ -174,10 +174,10 @@ export function addCreatedChatSession(chat: ChatSessionType): AddCreatedChatSess /** * @desc Action for updating chats - * @param {Array} chats Updated chats + * @param {Array} chats Updated chats * @returns {UpdateChatsAction} An action */ -export function chatsUpdated(chats: Array): UpdateChatsAction { +export function chatsUpdated(chats: Array): UpdateChatsAction { return { type: CHATS_UPDATED, chats, diff --git a/src/actions/dApps.js b/src/actions/dApps.js new file mode 100644 index 000000000..0c23cbecd --- /dev/null +++ b/src/actions/dApps.js @@ -0,0 +1,171 @@ +// @flow + +import { type DApp } from '../types/DApp'; +import type { DAppMessageType } from '../types/Chat'; +import type { DAppLaunchState } from '../reducers/dApps'; +import type { DAppChatContext, DAppModalInfo } from '../types/DApp'; + +export type DAppsListUpdatedAction = { +type: 'DAPPS_LIST_UPDATED', availableDApps: Array }; +export type StartDAppAction = { +type: 'START_DAPP', dAppPublicKey: string }; +export type StopDAppAction = { +type: 'STOP_DAPP', dAppPublicKey: string }; +export type SetDAppContextAction = { +type: 'SET_DAPP_CONTEXT', context: DAppChatContext | null }; +export type DAppLaunchStateChangedAction = { +type: 'DAPP_LAUNCH_STATE_CHANGED', dAppPublicKey: string, launchState: DAppLaunchState }; +export type OpenDAppAction = { +type: 'OPEN_DAPP', dAppPublicKey: string, callback: (success: boolean, error: ?Error) => void }; +export type StoreDAppModalAction = { +type: 'STORE_DAPP_MODAL', modal: DAppModalInfo }; +export type CleanDAppModalAction = { +type: 'CLEAN_DAPP_MODAL', modalID: string }; +export type PerformDAppCallbackAction = { +type: 'PERFORM_DAPP_CALLBACK', dAppPublicKey: string, callbackID: number, args: Object }; +export type RenderDAppMessageAction = { +type: 'RENDER_DAPP_MESSAGE', message: DAppMessageType, callback: (layout: ?Object) => void }; + +export type Action = + | DAppsListUpdatedAction + | StartDAppAction + | StopDAppAction + | SetDAppContextAction + | DAppLaunchStateChangedAction + | OpenDAppAction + | StoreDAppModalAction + | CleanDAppModalAction + | PerformDAppCallbackAction + | RenderDAppMessageAction; + +export const DAPPS_LIST_UPDATED = 'DAPPS_LIST_UPDATED'; +export const START_DAPP = 'START_DAPP'; +export const STOP_DAPP = 'STOP_DAPP'; +export const SET_DAPP_CONTEXT = 'SET_DAPP_CONTEXT'; +export const DAPP_LAUNCH_STATE_CHANGED = 'DAPP_LAUNCH_STATE_CHANGED'; +export const OPEN_DAPP = 'OPEN_DAPP'; +export const STORE_DAPP_MODAL = 'STORE_DAPP_MODAL'; +export const CLEAN_DAPP_MODAL = 'CLEAN_DAPP_MODAL'; +export const PERFORM_DAPP_CALLBACK = 'PERFORM_DAPP_CALLBACK'; +export const RENDER_DAPP_MESSAGE = 'RENDER_DAPP_MESSAGE'; + +/** + * @desc Action creator for an action that is called when DApps list updated. + * @param {DApp[]} availableDApps Array of available DApps. + * @returns {DAppsListUpdatedAction} An action. + */ +export function dAppsListUpdated(availableDApps: Array): DAppsListUpdatedAction { + return { + type: DAPPS_LIST_UPDATED, + availableDApps, + }; +} + +/** + * @desc Action creator for an action that is called when DApp changed his launch state, e.g. started/opened/closed/etc. + * @param {string} dAppPublicKey Public key of DApp that failed to start. + * @param {DAppLaunchState} launchState State that DApp was switched to. + * @return {DAppLaunchStateChangedAction} An action. + */ +export function dAppLaunchStateChanged(dAppPublicKey: string, launchState: DAppLaunchState): DAppLaunchStateChangedAction { + return { + type: DAPP_LAUNCH_STATE_CHANGED, + dAppPublicKey, + launchState, + }; +} + +/** + * @desc Action creator for an action to start DApp by its public key. + * @param {string} dAppPublicKey Public key of DApp to start. + * @return {StartDAppAction} An action. + */ +export function startDApp(dAppPublicKey: string): StartDAppAction { + return { + type: START_DAPP, + dAppPublicKey, + }; +} + +/** + * @desc Action creator for an action to start DApp by its public key. + * @param {string} dAppPublicKey Public key of DApp to stop. + * @return {StopDAppAction} An action. + */ +export function stopDApp(dAppPublicKey: string): StopDAppAction { + return { + type: STOP_DAPP, + dAppPublicKey, + }; +} + +/** + * @desc Action creator for an action to set current context for DApps. + * @param {DAppChatContext} context Context to set. + * @return {StopDAppAction} An action. + */ +export function setDAppContext(context: DAppChatContext | null): SetDAppContextAction { + return { + type: SET_DAPP_CONTEXT, + context, + }; +} + +/** + * @desc Action creator for an action to open DApp by its public key. + * @param {string} dAppPublicKey Public key of DApp to open. + * e.g. object containing ethereum address of recipient. + * @param {function} callback Callback to be called on finish. + * @return {OpenDAppAction} An action. + */ +export function openDApp(dAppPublicKey: string, callback: () => void = () => undefined): OpenDAppAction { + return { + type: OPEN_DAPP, + dAppPublicKey, + callback, + }; +} + +/** + * @desc Action creator for an action to update or create DApp modal. + * @param {DAppModalInfo} modal Modal to store + * @return {StoreDAppModalAction} An action. + */ +export function storeDAppModal(modal: DAppModalInfo): StoreDAppModalAction { + return { + type: STORE_DAPP_MODAL, + modal, + }; +} + +/** + * @desc Action creator for an action to clean DApp modal, i.e. remove its info from state. + * @param {string} modalID ID of modal to clean + * @return {CleanDAppModalAction} An action. + */ +export function cleanDAppModal(modalID: string): CleanDAppModalAction { + return { + type: CLEAN_DAPP_MODAL, + modalID, + }; +} + +/** + * @desc Action creator for an action to perform DApp callback. + * @param {string} dAppPublicKey Public key of DApp. + * @param {number} callbackID Id of callback + * @param {Object} args Arguments to be passed. + * @return {PerformDAppCallbackAction} An action. + */ +export function performDAppCallback(dAppPublicKey: string, callbackID: number, args: Object): PerformDAppCallbackAction { + return { + type: PERFORM_DAPP_CALLBACK, + dAppPublicKey, + callbackID, + args, + }; +} + +/** + * @desc Action creator for an action to send DApp message. + * @param {DAppMessageType} message Message to send. + * @param {function} callback Callback to call on finish or error. + * @return {RenderDAppMessageAction} An action. + */ +export function renderDAppMessage(message: DAppMessageType, callback: (layout: ?Object) => void): RenderDAppMessageAction { + return { + type: RENDER_DAPP_MESSAGE, + message, + callback, + }; +} diff --git a/src/actions/nativeDApps.js b/src/actions/nativeDApps.js deleted file mode 100644 index 127b6e9e3..000000000 --- a/src/actions/nativeDApps.js +++ /dev/null @@ -1,46 +0,0 @@ -// @flow - -import type { DAppMessageType, ChatSessionType, GiftedChatMessageType } from '../types/Chat'; - -export type OpenDAppAction = { +type: 'OPEN_DAPP', dAppPublicKey: string, context: Object, callback: (success: boolean, error: ?Error) => void }; -export type SendDAppMessageAction = { +type: 'SEND_DAPP_MESSAGE', message: DAppMessageType, session: ChatSessionType, callback: (message: ?GiftedChatMessageType) => void }; - -export type Action = - | OpenDAppAction - | SendDAppMessageAction; - -export const OPEN_DAPP = 'OPEN_DAPP'; -export const SEND_DAPP_MESSAGE = 'SEND_DAPP_MESSAGE'; - -/** - * @desc Action creator for an action to open DApp by its public key. - * @param {string} dAppPublicKey Public key of DApp to open. - * @param {Object} context Context that will be passed to DApp, - * e.g. object containing ethereum address of recipient. - * @param {function} callback Callback to be called on finish. - * @return {OpenDAppAction} An action. - */ -export function openDApp(dAppPublicKey: string, context: Object = {}, callback: () => void = () => undefined): OpenDAppAction { - return { - type: OPEN_DAPP, - dAppPublicKey, - context, - callback, - }; -} - -/** - * @desc Action creator for an action to send DApp message. - * @param {DAppMessageType} message Message to send. - * @param {ChatSessionType} session Session to send to. - * @param {function} callback Callback to call on finish or error. - * @return {SendDAppMessageAction} An action. - */ -export function sendDAppMessage(message: DAppMessageType, session: ChatSessionType, callback: (message: ?GiftedChatMessageType) => void): SendDAppMessageAction { - return { - type: SEND_DAPP_MESSAGE, - message, - session, - callback, - }; -} diff --git a/src/components/common/DAppMessage.js b/src/components/common/DAppMessage.js index ca5f7077f..d356103e2 100644 --- a/src/components/common/DAppMessage.js +++ b/src/components/common/DAppMessage.js @@ -6,39 +6,146 @@ */ import React from 'react'; -import { View, Text } from 'react-native'; +import { ActivityIndicator, Text, View } from 'react-native'; import { MediaQueryStyleSheet } from 'react-native-responsive'; +import { connect } from 'react-redux'; +import Root from '../../DAppsSDK/0.0.1/components/Root'; +import type { DAppMessageType } from '../../types/Chat'; import GlobalStyles from '../../global/Styles'; +import { renderDAppMessage } from '../../actions/dApps'; +import i18n from '../../global/i18n'; +import { getDApp, type State as DAppsState } from '../../reducers/dApps'; type Props = { /** - * * @desc Message to be displayed. */ - message: string, + message: DAppMessageType, /** - * - * @desc Time to be displayed. + * @desc Function to render DApp message and get layout. */ - time: string, + renderDAppMessage: (message: DAppMessageType, callback: (layout: ?Object) => void) => void, + /** + * @desc DApp redux state. + */ + dAppsState: DAppsState, }; +type State = { + isRendering: boolean, + layout: ?Object, +} + const styles = MediaQueryStyleSheet.create({ ...GlobalStyles, + dAppMessageRootView: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + + dAppMessageContainer: { + paddingTop: 6, + flexGrow: 1, + alignItems: 'stretch', + justifyContent: 'center', + flex: 0, + }, + + loadingContainer: { + flex: 1, + height: 50, + alignItems: 'center', + justifyContent: 'center', + }, + + fallbackUIContainer: { + paddingLeft: 6, + paddingRight: 6, + }, +}); + +class DAppMessage extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + isRendering: true, + layout: null, + }; + this.props.renderDAppMessage(props.message, (layout) => { + this.setState(() => ({ + isRendering: false, + layout, + })); + }); + } + + componentDidCatch() { + this.setState({ + isRendering: false, + layout: null, + }); + } + + renderFallbackUI() { + const dApp = getDApp(this.props.dAppsState, this.props.message.dAppPublicKey); + let textToShow = i18n.t('dApps.unknownDAppMessage'); + if (dApp != null) { + textToShow = i18n.t('dApps.failedDAppMessageRender', { dAppName: dApp.name }); + } + return ( + + + {textToShow} + + + ); + } + + renderLoading() { + return ( + + + + ); + } + + render() { + return ( + + + { + this.state.layout != null + && + + } + { + this.state.isRendering === false + && this.state.layout == null + && this.renderFallbackUI() + } + + { + this.state.isRendering === true && this.renderLoading() + } + + ); + } +} + +const mapStateToProps = state => ({ + dAppsState: state.dApps, +}); + +const mapDispatchToProps = dispatch => ({ + renderDAppMessage(message, callback) { + dispatch(renderDAppMessage(message, callback)); + }, }); -const DAppMessage = ({ - time, message, -}: Props) => ( - - - {time} - - - {message} - - -); - -export default DAppMessage; +export default connect(mapStateToProps, mapDispatchToProps)(DAppMessage); diff --git a/src/components/nativeDApps/AmountSelectController.js b/src/components/nativeDApps/AmountSelectController.js deleted file mode 100644 index e271aed2c..000000000 --- a/src/components/nativeDApps/AmountSelectController.js +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable no-use-before-define */ -// @flow - -import React, { Component } from 'react'; -import type { InternalProps } from './AmountSelect'; -import AmountSelect from './AmountSelect'; -import type { CurrencyType } from '../../types/Wallet'; - -export type Props = { - /** - * @desc Style to apply to container view. - */ - style?: Object, - /** - * @desc - */ - onAmountSelected: (amount: string, currency: CurrencyType, walletAddress: string, isValid: boolean) => void, - /** - * @desc Flag whether amount is invalid if it greater than balance. - */ - shouldCheckLess: boolean, - /** - * @desc Flag whether to allow user to change currency. - */ - changeCurrencyEnabled?: boolean, -} - -export default class AmountSelectController extends Component { - static defaultProps: Object = { - changeCurrencyEnabled: true, - }; - - constructor(props: Props & InternalProps) { - super(props); - - this.state = { - amount: '', - currency: 'ETH', - }; - } - - render() { - return ( - { - this.setState({ amount, currency }); - this.props.onAmountSelected(amount, currency, address, isValid); - }} - shouldCheckLess={this.props.shouldCheckLess || false} - wallets={this.props.wallets} - changeCurrencyEnabled={this.props.changeCurrencyEnabled} - /> - ); - } -} diff --git a/src/components/nativeDApps/DAppProvider.js b/src/components/nativeDApps/DAppProvider.js deleted file mode 100644 index a42440648..000000000 --- a/src/components/nativeDApps/DAppProvider.js +++ /dev/null @@ -1,183 +0,0 @@ -/* eslint-disable react/prop-types,no-empty */ -// @flow - -import * as React from 'react'; - -import type { CurrencyType, WalletType } from '../../types/Wallet'; -import type { Account } from '../../types/Account'; -import AmountSelect, { type Props as AmountSelectProps } from './AmountSelect'; -import type { Props as AmountSelectControllerProps } from './AmountSelectController'; -import type { ChatSessionType, DAppMessageType, GiftedChatMessageType, ProfileType } from '../../types/Chat'; -import type { Navigator } from '../../types/ReactNativeNavigation'; -import EthereumService from '../../services/ethereum'; -import ServiceContainer from '../../services/container'; -import type { DAppType } from '../../dapps'; -import AmountSelectController from './AmountSelectController'; - -export type ProviderProps = { - /** - * @desc Wallets array - */ - wallets: Array, - /** - * @desc Chat session. - */ - session: ChatSessionType, - /** - * @desc Public key of DApp. - */ - dApp: DAppType, - /** - * @desc Function to send a DApp message. - */ - sendMessage: (message: DAppMessageType, session: ChatSessionType, callback: (message: ?GiftedChatMessageType) => void) => void, - /** - * @desc React Native Navigation navigator object. - */ - navigator: Navigator, - /** - * @desc Account of current user - */ - currentAccount: Account, - /** - * @desc Profile of current chat friend. - */ - friend: ProfileType, - /** - * @desc Function to show or hide loading. - */ - setLoadingVisible: (visible: boolean) => void, -}; - -export type ProvidedProps = { - context: { - /** - * @desc Account of current user. - */ - currentAccount: Account, - /** - * @desc Profile of current chat friend. - */ - friend: ProfileType, - }, - components: { - /** - * @desc Renders AmountSelect component. - * @param {AmountSelectProps} props Props to pass to AmountSelect component - * @param {boolean} autoControlled Flag whether state of component should be handled automatically - */ - renderAmountSelect: (props: AmountSelectProps | AmountSelectControllerProps, autoControlled?: boolean) => React.Node, - /** - * @desc Function to show or hide loading. - */ - setLoadingVisible: (visible: boolean) => void, - }, - services: { - /** - * @desc Function to send a message. - */ - sendMessage: (type: string, groupId: string, params: Object, callback: (message: ?GiftedChatMessageType) => void) => void, - /** - * @desc Service to deal with ethereum. - */ - ethereumService: EthereumService, - /** - * @desc Function to send money - * @param {CurrencyType} currency String with currency symbol (ETH, XPAT) - * @param {string} toAddress Address to send ether to. - * @param {string} amount Amount in base currency unit (ether, XPAT) - * @return {Promise} Promise that resolves into transaction. - */ - sendMoney: (currency: CurrencyType, toAddress: string, amount: string) => Promise, - /** - * @desc Deploy contract and return a deploy transaction. - * @param {string} bytecode Byte code of contract - * @param {string} abi ABI of contract - * @param {string} txValue Value in ether to set to deploy transaction. - * @param {any} params Additional params to pass. - * @return {Promise} Promise that resolves into transaction - */ - deployContract: (bytecode: string, abi: string, txValue?: string, ...params: any) => Promise, - /** - * @desc Function to get XPAT token contract address (based on current account network). - */ - getXPATTokenAddress: () => string, - }, - navigation: { - /** - * @desc Dismiss the modal. - */ - dismiss: () => void - } -}; - -/** - * @desc HOC builder for providing helper functions and context into DApp component. - * @param {React.Component} Component Component to wrap to. - * @return {*} HOC - */ -export const DAppProvider = (Component: React.ComponentType) => (props: ProviderProps) => { - const { ethereumService, dAppsWalletService } = ServiceContainer.instance; - const { identityPublicKey: dAppPublicKey, name } = props.dApp; - if (ethereumService == null || dAppsWalletService == null) { - return null; - } - - const dAppName = `${name} DApp`; - - const providedProps: ProvidedProps = { - context: { - currentAccount: props.currentAccount, - friend: props.friend, - }, - components: { - renderAmountSelect(customProps: AmountSelectProps | AmountSelectControllerProps, autoControlled: boolean = true) { - return (autoControlled === true) ? - - : - ; - }, - setLoadingVisible: props.setLoadingVisible, - }, - services: { - sendMessage(type: string, groupId: string, params: Object, callback: (message: ?GiftedChatMessageType) => void) { - // @todo Add error providing. - if (type.length > 100) { - callback(null); - return; - } - if (groupId.length > 100) { - callback(null); - return; - } - try { - const stringified = JSON.stringify(params); - if (stringified.length > 5000000) return; - props.sendMessage({ - dapp_id: dAppPublicKey, - type, - group_id: groupId, - params: stringified, - should_send: true, - should_render: true, - }, props.session, callback); - } catch (e) { - callback(null); - } - }, - ethereumService, - sendMoney: (currency, toAddress, amount) => dAppsWalletService.sendMoney(dAppName, currency, toAddress, amount), - deployContract: (bytecode, abi, txValue, ...params) => dAppsWalletService.deployContract(dAppName, bytecode, abi, txValue, ...params), - getXPATTokenAddress: dAppsWalletService.getXPATTokenAddress, - }, - navigation: { - dismiss() { - props.navigator.dismissModal(); - }, - }, - }; - - return ( - - ); -}; diff --git a/src/components/nativeDApps/MessageParamsValidator.js b/src/components/nativeDApps/MessageParamsValidator.js deleted file mode 100644 index e6c7e6521..000000000 --- a/src/components/nativeDApps/MessageParamsValidator.js +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable prefer-destructuring */ -// @flow - -import * as React from 'react'; -import type { ProvidedProps as MessageProvidedProps } from '../../components/nativeDApps/MessageProvider'; - -export const MessageParamsValidator = - (Component: React.ComponentType, validator: (Data) => boolean) => (props: (Props)): React.Node => { - const params: string = props.context.dAppMessage.params; - const data: Data = JSON.parse(params); - if (validator(data) === false) return null; - - return ; - }; diff --git a/src/components/nativeDApps/MessageProvider.js b/src/components/nativeDApps/MessageProvider.js deleted file mode 100644 index 92aab547d..000000000 --- a/src/components/nativeDApps/MessageProvider.js +++ /dev/null @@ -1,110 +0,0 @@ -/* eslint-disable react/prop-types,no-empty */ -// @flow - -import * as React from 'react'; - -import type { Account } from '../../types/Account'; -import type { DAppMessageType, ProfileType } from '../../types/Chat'; -import type { Navigator } from '../../types/ReactNativeNavigation'; -import EthereumService from '../../services/ethereum'; -import ServiceContainer from '../../services/container'; -import type { CurrencyType } from '../../types/Wallet'; -import type { DAppType } from '../../dapps'; - - -type ProviderProps = { - /** - * @desc Public key of DApp. - */ - dApp: DAppType, - /** - * @desc React Native Navigation navigator object. - */ - navigator: Navigator, - /** - * @desc Account of current user - */ - currentAccount: Account, - /** - * @desc Profile of current chat friend. - */ - friend: ProfileType, - /** - * @desc Message to render. - */ - dAppMessage: DAppMessageType, - /** - * @desc Wallet address of current account. - */ - walletAddress: string, -}; - -export type ProvidedProps = { - context: { - /** - * @desc Message to render. - */ - dAppMessage: DAppMessageType, - /** - * @desc Account of current user. - */ - currentAccount: Account, - /** - * @desc Profile of current chat friend. - */ - friend: ProfileType, - /** - * @desc Wallet address of current account. - */ - walletAddress: string, - }, - services: { - /** - * @desc Service to deal with ethereum. - */ - ethereumService: EthereumService, - /** - * @desc Function to send money - * @param {CurrencyType} currency String with currency symbol (ETH, XPAT) - * @param {string} toAddress Address to send ether to. - * @param {string} amount Amount in base currency unit (ether, XPAT) - * @return {Promise} Promise that resolves into transaction. - */ - sendMoney: (currency: CurrencyType, toAddress: string, amount: string) => Promise, - /** - * @desc Gets contract instance. - * @param {string} address Address of deployed contract. - * @param {(string|Object)} abi ABI of contract. - * @return {*} Contract instance. - */ - getContract: (address: string, abi: (string | Object)) => Promise, - }, -}; - -/** - * @desc HOC builder for providing helper functions and context into DApp messages. - * @param {React.Component} Component Message component to wrap to. - * @return {*} HOC - */ -export const MessageProvider = (Component: React.ComponentType) => (props: ProviderProps) => { - const { ethereumService, dAppsWalletService } = ServiceContainer.instance; - if (ethereumService == null || dAppsWalletService == null) { - return null; - } - - const providedProps: ProvidedProps = { - context: { - dAppMessage: props.dAppMessage, - currentAccount: props.currentAccount, - friend: props.friend, - walletAddress: props.walletAddress, - }, - services: { - ethereumService, - sendMoney: (...args) => dAppsWalletService.sendMoney(props.dApp.name, ...args), - getContract: (...args) => dAppsWalletService.getContract(props.dApp.name, ...args), - }, - }; - - return ; -}; diff --git a/src/dapps/escrow/Constants.js b/src/dapps/escrow/Constants.js deleted file mode 100644 index 3a2e2637f..000000000 --- a/src/dapps/escrow/Constants.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow - -export type MessageData = { - deployTxHash: string, - etherAmount: string, - tokenAmount: string, - /** - * @desc Address of token contract - */ - tokenContractAddress: string, - /** - * @desc Address of person who should pay the tokens. - */ - tokensFromAddress: string, - /** - * @desc Name of person who should pay the tokens. - */ - tokensFromName: string, -} diff --git a/src/dapps/escrow/ContractInteractionMessage.js b/src/dapps/escrow/ContractInteractionMessage.js deleted file mode 100644 index a3d993a27..000000000 --- a/src/dapps/escrow/ContractInteractionMessage.js +++ /dev/null @@ -1,227 +0,0 @@ -/* eslint-disable react/sort-comp */ -// @flow - -import * as React from 'react'; -import { Text, View, StyleSheet } from 'react-native'; -import { BigNumber } from 'bignumber.js'; - -import Colors from '../../global/colors'; -import i18n from '../../global/i18n'; -import type { ProvidedProps as MessageProvidedProps } from '../../components/nativeDApps/MessageProvider'; -import type { MessageData } from './Constants'; -import Button from '../../components/common/Button'; -import { alert, errorAlert } from '../../global/alerts'; - -const styles = StyleSheet.create({ - text: { - fontSize: 17, - color: Colors.BitnationDarkGrayColor, - }, - textBold: { - fontWeight: 'bold', - }, - buttonContainer: { - flexDirection: 'column', - alignItems: 'stretch', - justifyContent: 'space-between', - }, - spacer: { height: 12 }, -}); - -type OwnProps = { - /** - * @desc Smart contract object of escrow contract. - */ - contract: Object, - /** - * @desc Address of deployed smart contract. - */ - contractAddress: string, -} - -type Status = 'unknown' | 'pending' | 'readyForWithdraw' | 'withdrawn' | 'drained'; - -type State = { - contractStatus: Status, - userHasSpends: boolean, -} - -type Props = MessageData & OwnProps & MessageProvidedProps - -export default class ContractInteractionMessage extends React.Component { - constructor(props: Props) { - super(props); - - this.state = { - contractStatus: 'unknown', - userHasSpends: false, - }; - } - - timeoutHandler: ?number; - - componentWillMount() { - this.startFetching(); - } - - componentWillUnmount() { - this.stopFetching(); - } - - startFetching = async (oneTime: boolean = false) => { - try { - const [status, userHasSpends] = await this.fetchStatus(); - this.setState({ contractStatus: status, userHasSpends }); - } catch (error) { - console.log(`[Escrow DApp] Failed to fetch contract info with error ${error.message}`); - } - if (oneTime === false) { - this.timeoutHandler = setTimeout(this.startFetching, 8000); - } - }; - - fetchStatus = async (): Promise<[Status, boolean]> => { - const withdrawn = await this.props.contract.withdrawled(); - const agreedEtherAmount = new BigNumber(this.props.etherAmount); - const etherBalance = new BigNumber(await this.props.services.ethereumService.getOtherBalance(this.props.contractAddress)); - const agreedTokenAmount = new BigNumber(this.props.tokenAmount); - const tokenBalance = new BigNumber(await this.props.services.ethereumService.getOtherTokenBalance(this.props.tokenContractAddress, this.props.contractAddress)); - - const userHasSpends = this.props.tokensFromAddress === this.props.context.walletAddress - ? tokenBalance.isZero() === false - : etherBalance.isZero() === false; - - if (withdrawn === true) { - return ['withdrawn', userHasSpends]; - } - - if (etherBalance.lessThan(agreedEtherAmount)) { - return ['drained', userHasSpends]; - } - - if (tokenBalance.lessThan(agreedTokenAmount)) { - return ['pending', userHasSpends]; - } - - return ['readyForWithdraw', userHasSpends]; - }; - - stopFetching = () => { - if (this.timeoutHandler) { - clearInterval(this.timeoutHandler); - } - }; - - onPressSend = async () => { - await this.props.services.sendMoney('XPAT', this.props.contractAddress, this.props.tokenAmount); - }; - - onPressWithdraw = async () => { - await this.completeContract(); - }; - - onPressCancel = async () => { - alert('dApps.escrow.cancelConfirmationAlert', [ - { name: 'confirm', onPress: this.cancelContract }, - { name: 'cancel', style: 'cancel' }, - ]); - }; - - cancelContract = async () => { - try { - await this.props.contract.drain(); - await this.startFetching(true); - } catch (error) { - errorAlert(error); - } - }; - - completeContract = async () => { - try { - await this.props.contract.withdrawal(); - await this.startFetching(true); - } catch (error) { - errorAlert(error); - } - }; - - renderStatus(status: Status) { - const shouldShowSendTokens = this.props.tokensFromAddress === this.props.context.walletAddress && status === 'pending'; - if (status === 'unknown') { - return ( - - Checking smart contract status. - - ); - } - - const statusText = (() => { - switch (this.state.contractStatus) { - case 'pending': - return shouldShowSendTokens - ? 'Send XPAT to complete exchange' - : `Waiting for ${this.props.tokensFromName} to complete exchange`; - case 'readyForWithdraw': - return 'Smart contract is ready to proceed exchange.\n' + - 'Press exchange to confirm withdrawal transaction.\n' + - 'Only one party need to do that.'; - case 'withdrawn': - return 'Exchange finished.\n' + - 'Check out your wallets.'; - case 'drained': - return 'Exchange was aborted.\n' + - 'Funds are returned back.'; - default: - return ''; - } - })(); - - return ( - - - {statusText} - - - - { - shouldShowSendTokens && -