From 558a600e23b5cbc03632d84c3e50510bdeff1e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Fri, 15 May 2020 15:26:16 -0400 Subject: [PATCH] Release 0.2.16 (#1583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Loosen nvmrc (#1524) Co-authored-by: Esteban Miño * bugfix/check for sai method (#1545) * update docs link in readme (#1521) Co-authored-by: Esteban Miño * Add settings to nav bar (#1544) * Add settings to nav bar * Remove settings Icon * Add icons for all drawer links * Improvement/tx status notification (#1475) * simple not update * text update * wip * delete old details * delete old confirm * almost done withtx details * modal working * modal title * rm transfer element * clean * fix transfer * transfer and payment channel * decodeTransferFromTx * decodeDeploymentTx * decodeConfirmTx * onpress * status * close on view web * more cleanup * payment * showing not * closer? * comment * tx details and not * animated * tx not * enable access view on not * animated * rename * using txnnot manager * working * receive * rm unused * rm logs * handle browser not * parse date * handle asset details * tx summary rename props * Refactor names in details * handle primary currency * missing props * almost there * working but browser * finally * one more thing * done * snaps * missing locales * update ethereum address * snaps * handle instapay txs * snaps * feeless tx * data check * No fee * instance._hideTransactionNotification * fix instapay notifications * elevation * fix remaining issues * apeed up cancel * transaction modal * speed cancel * speedup cancel ui * working * added engine methods * done * snaps * fix qaing * fix ios build * one snap * remove test * status text fix * cancelled * margin * snaps * fix insufficient funds * doc * Transaction Header Component (#1487) * Remove redundant imports, remove redundant styles, comment typo correction, remove renderPageInformation(), split props line by line in render(), swap rendering renderPageInformation() with TransactionHeader component, pass props * added lock and warning icons * removed domain prop * new TransactionHeader component, imports, styling, prop types, lock/warning icon change based on URL protocol, network status indicator (color) changes if network is online/not online * re-generated snapshot for SignatureRequest, created new test for TransactionHeader * network changes based on selected network * update snapshot * update snapshot * remove function, use css for network capitalization * move network status logic to renderNetworkStatusIndicator() * render icon logic moved to renderSecureIcon() * add comments * update snapshot * remove redundant getTrackingParams, use props directly * remove png icons from image dir, use react native svg icons (FontAwesome), update snapshot * TransactionHeader: use 'Ethereum' instead of 'Mainnet' * Add shortnames to networks util, TransactionHeader: use networks util to display network name, update snapshot * Remove redundant imports, remove redundant styles, comment typo correction, remove renderPageInformation(), split props line by line in render(), swap rendering renderPageInformation() with TransactionHeader component, pass props * added lock and warning icons * removed domain prop * new TransactionHeader component, imports, styling, prop types, lock/warning icon change based on URL protocol, network status indicator (color) changes if network is online/not online * re-generated snapshot for SignatureRequest, created new test for TransactionHeader * network changes based on selected network * update snapshot * update snapshot * remove function, use css for network capitalization * move network status logic to renderNetworkStatusIndicator() * render icon logic moved to renderSecureIcon() * add comments * update snapshot * remove redundant getTrackingParams, use props directly * remove png icons from image dir, use react native svg icons (FontAwesome), update snapshot * TransactionHeader: use 'Ethereum' instead of 'Mainnet' * Add shortnames to networks util, TransactionHeader: use networks util to display network name, update snapshot * fixed import error Co-authored-by: Esteban Miño * Feature/block screenshots (#1495) * native * test on wallet * block in some screens * ios check * rm asyncs * helper * missing places * Detect if site has been added to Favorites (#1538) * Detect if site has been added to Favorites Previously we were `Alert`ing after attempting to `addBookmark`. Instead, we remove the option from the menu entirely. closes: #1511 * Rename isFavorite -> isBookmark * Use "web-search" keyboardType on iOS (#1539) * Use web-search keyboard Use web-search for the omnibar keyboard * Add new mobile provider (#1517) Co-authored-by: Esteban Miño * Sig request design fixed (#1493) * new folder for AccountInfoCard component, remove signature_request.title from message, personal & typed sign components, remove redundant style * SignatureRequest: remove account information from top, proptypes, props, styles, imports * SignatureRequest: change signing message to 'Sign this message?', make bold and larger * remove keyboard aware scroll view * Add AccountInfoCard component to SignatureRequest * AccountInfoCard: implement proper styling * AccountInfoCard: use renderShortAddress, fix styles, use conversionRate to display $ amount * ActionView: add isSigning prop, disables scroll when true, SignatureRequest: pass isSigning=true to ActionView * AccountInfoCard: remove top level view * SignatureRequest: apply styles & layout, add website + arrow icons * Signing components: update styling, rename informationRow --> informationCol * remove message style * TypedSign: put back message style, add messageWrapper style, ensure data fits in box and hides overflow * AccountInfoCard: add snapshot test * update snapshots * styling of 'sign this message' * update snapshot * update snapshot * SignatureRequest: Always render arrow if children coming from TypedSign * SignatureRequest: change to regular component with state to show expanded message content, wrap touchableWithoutFeedback around the message children and move rendering to renderActionViewChildren, tapping the message currently does nothing * fix dapp icon style, fix render inf loop * remove textwrap for typed sign. Now renders properly for V1, V3 & V4 * AccountInfoCard: fix paddings, identicon style, use widths instead of flex, float address to left, fix font weights * TypedSign: use width instead of flex * SignatureRequest: remove website icon wrapper, fix arrow positioning, remove assetLogo style * temp disable warning to match style, ensure message fits within box * PersonalSign: create message wrapper, ensure message fits within box * Message & Personal Sign: use ellipses mode for text wrapper, drop messageWrapper styles * SignatureRequest: remove shouldRenderArrow, add shouldRenderArrow prop * MessageSign: change to stateful component, add renderArrow state, decides if should render arrow upon text component layout, then adjusts the text accordingly * PersonalSign: change to stateful component, add renderArrow state, decides if should render arrow upon text component layout, then adjusts the text accordingly * TypedSign: shouldRenderArrow always passes down as true. Will never be a situation where an entire typed message fits in the box * SignatureRequest: change back to pure component, change handleMessageTap into prop * change handleMessageTap to toggleExpandedMessage * TypedSign: implement message expansion and retraction * modify message, add message_from * new ExpandedMessage component, rendered by typed, personal & regular message components * TypedSign: use ExpandedMessage component * ExpandedMessage: test * SignatureRequest: add renderArrow prop, make box not expandable if false * MessageSign: implement message expanding and retracting * PersonalSign: implement message expand & retract * ExpandedMessage: add mock fns, update all snapshots * ActionView: get rid of top border * new button styles * signing components: ensure a top left and right rounded border * change Cancel & Sign to lowercase * snapshot update * adjust style for android * use unique button style for signing components, fix percentage in stylesheet * change isSigning prop to noScroll * snapshot update * update more snapshots * Signing components: revert to pure component * AccountInfoCard: use weiToFiat & hexToBn helpers to display fiat value, add currentCurrency prop * Signing components: shift renderRootView() contents into render() * update snapshot * AccountInfoCard, SignatureRequest: fix paddings per design * AccountInfoCard: remove bottom margin * TransactionHeader: remove margins, use padding * MessageSign: larger min height, showWarning prop * WarningMessage: use flexstart alignment for bell icon * locales: change eth_sign_warning * WarningMessage: add object as secondary prop type for warning message * SignatureRequest: use WarningMessage component, fix paddings, use renderWarning as prop for WarningMessage * snapshot update yo * SignatureRequest: move AccountInfoCard ontop of message children * snapshot update * AccountInfoCard: remove width * ActionView: remove no scroll - small devices * Signing Components: remove root style, move to SignatureRequest * SignatureRequest: remove style redundancies, add in root style * SignatureRequest: fix height of modal based on screen height * ExpandedMessage: fix styling, move scrollview to signing components renderMessage * ExpandedMessage: Put the scroll back * PersonalSign: remove expandedMessage text wrapper * SignatureRequest: fix up styling, add more overhead (reduced from signing components) * Signing components: reduce view hierarchy, move to SignatureRequest * Locales: add Read more to signature_request * AccountInfoCard: add operation prop, if operation is signing, only display the account name and address * TypedSign: add shouldTruncateMessage & truncateMessage in state * PersonalSign: remove console log * Signing components: change renderArrow to truncateMessage * Nav/Main: add showExpandedMessage to state, add toggleExpandedMessage, configure expanded signing modal to go back on android back button press, pass down props to signing components * Signing Components: move showExpandedMessage out of state, move out toggleExpandedMessage * TypedSign: create different messageWrapper height for iOS & Android, fix text clipping mid-line * ExpandedMessage: fix scrollview * snapshot update * AccountInfoCard: use getTicker * Signing components: change margin bottom from 5 to 4 * Device: new getMediumDevice, SignatureRequest: use getMediumDevice * ActionView, styledButtonStyles: add cancel button style for signing components * snapshot update * SignatureRequest: fix the domain logo not being a circle * update snapshot * Use gaba@1.11.0 (#1552) * Fix settings everywhere (#1556) * Fix day and month numbers in toDateFormat (#1557) * Make send flows consistent (#1465) * Move components and styles from Confirm into TransactionReview * Add ActionView back in * Add missing styles * Revert TransactionReview changes * Update send screen: from accounts editable and redesign gas edit link * Use sendflow confirm for payment requests and when editing * Update sendflow/confirm tests * Use new send flow designs for instapayment / payment channel transactions * Use existing send flow screens for deep link transactions * Fix editing of payment request transactions * Fix unit tests on consistent-send-flow branch * Fix navigation for deep link tx edits on the amount screen. * Refactor: combine transaction and newTransaction reducers * Fix bugs on consistent-send-flow * Fix confirm and edit of transactions created from dapps * Update transaction edit text color * Only allow changing from field on confirm screen of payment requests * Fix amount validation for payment channel transactions * Fix qr payment requests, payment channel payment requests, and token payment requests; plus other small fixes * Fix token approvals * Fix sending of decimals on payment channles * Show correct error messages when accounts are changed and/or token balances are insufficient * Update navbar options in edit mode * Ensure tokens cannot be sent in cases where user has not added the token * Correctly validate payment channel transaction on mount/update * Use sai.svg instead of dai.svg Co-authored-by: Dan Miller Co-authored-by: Esteban Miño * Use setTimeout hack (again) to get paste context in token search (#1548) * Use setTimeout hack (again) to get paste context in token search * Update test Co-authored-by: Esteban Miño * V0.2.16 (#1561) * bump * changelog * rm entry * Fix amount validation for large token payment requests (#1572) * Fix validating of amount when sending a collectible (#1565) * Fix validating of amount when sending a collectible * Validate collectible ownership on amount screen. * Ensure correct updating of collectible transaction after edit on the amount screen * Ensure collectibles that use 'transfer' method show a fee in tx history list (#1574) Co-authored-by: Esteban Miño * Disable confirm screen edit button when no tokens of a payment request (#1570) * Disable confirm screen edit button when account has no tokens of a payment request * Ensure account switching from undefined token balance accounts enables edit on pay reqs * Improve logic in componentDidUpdate of send/index.js * v0.2.16 changelog (#1575) * Instapay deposit navbar cancel (#1582) * fix * works * rm log * add this to changelog and update date * amount title Co-authored-by: Erik Marks <25517051+rekmarks@users.noreply.github.com> Co-authored-by: Jenny Pollack Co-authored-by: ricky Co-authored-by: Etienne Dusseault Co-authored-by: Whymarrh Whitby Co-authored-by: Ibrahim Taveras Co-authored-by: Dan J Miller --- .nvmrc | 2 +- CHANGELOG.md | 24 + README.md | 2 +- android/app/build.gradle | 4 +- .../java/io/metamask/MainApplication.java | 4 +- .../nativeModules/PreventScreenshot.java | 55 ++ .../PreventScreenshotPackage.java | 23 + app/actions/newTransaction/index.js | 72 --- app/actions/transaction/index.js | 98 +++- app/actions/transactionNotification/index.js | 15 + app/components/Nav/Main/index.js | 199 ++++--- .../__snapshots__/index.test.js.snap | 93 +++ app/components/UI/AccountInfoCard/index.js | 132 +++++ .../UI/AccountInfoCard/index.test.js | 44 ++ .../UI/AccountList/AccountElement/index.js | 32 +- app/components/UI/AccountList/index.js | 28 +- .../UI/ActionModal/ActionContent/index.js | 156 +++++ .../__snapshots__/index.test.js.snap | 105 +--- app/components/UI/ActionModal/index.js | 79 +-- .../__snapshots__/index.test.js.snap | 2 - app/components/UI/ActionView/index.js | 8 +- .../__snapshots__/index.test.js.snap | 2 +- app/components/UI/AssetOverview/index.js | 2 +- .../__snapshots__/index.test.js.snap | 14 +- app/components/UI/AssetSearch/index.js | 12 +- .../UI/CollectibleContractOverview/index.js | 2 +- app/components/UI/DrawerView/index.js | 27 +- app/components/UI/EthInput/index.js | 4 +- app/components/UI/EthereumAddress/index.js | 1 - .../__snapshots__/index.test.js.snap | 77 +-- app/components/UI/MessageSign/index.js | 121 ++-- app/components/UI/Navbar/index.js | 70 ++- app/components/UI/PaymentRequest/index.js | 2 +- .../__snapshots__/index.test.js.snap | 91 +-- app/components/UI/PersonalSign/index.js | 136 +++-- .../__snapshots__/index.test.js.snap | 107 ++++ .../SignatureRequest/ExpandedMessage/index.js | 105 ++++ .../ExpandedMessage/index.test.js | 19 + .../__snapshots__/index.test.js.snap | 289 ++++------ app/components/UI/SignatureRequest/index.js | 276 +++++---- .../UI/StyledButton/styledButtonStyles.js | 24 + .../TransactionActionContent/index.js | 105 ++++ .../UI/TransactionActionModal/index.js | 103 ++++ app/components/UI/TransactionEdit/index.js | 3 +- app/components/UI/TransactionEditor/index.js | 11 +- .../UI/TransactionEditor/index.test.js | 3 +- .../__snapshots__/index.test.js.snap | 531 +++++------------- .../TransactionDetails/index.js | 356 ++++++------ .../TransactionDetails/index.test.js | 8 +- .../__snapshots__/index.test.js.snap | 35 -- .../TransferElement/index.js | 269 --------- .../TransferElement/index.test.js | 32 -- .../__snapshots__/index.test.js.snap | 139 +---- app/components/UI/TransactionElement/index.js | 409 ++++---------- .../UI/TransactionElement/index.test.js | 3 + app/components/UI/TransactionElement/utils.js | 505 +++++++++++++++++ .../__snapshots__/index.test.js.snap | 105 ++++ app/components/UI/TransactionHeader/index.js | 139 +++++ .../UI/TransactionHeader/index.test.js | 37 ++ .../UI/TransactionNotification/index.js | 126 ++--- .../TransactionReviewInformation/index.js | 4 +- .../TransactionReviewSummary/index.js | 9 +- app/components/UI/TransactionReview/index.js | 4 +- app/components/UI/Transactions/index.js | 130 +---- app/components/UI/TxNotification/index.js | 501 +++++++++++++++++ .../__snapshots__/index.test.js.snap | 73 +-- app/components/UI/TypedSign/index.js | 121 ++-- .../Views/AccountBackupStep4/index.js | 9 +- app/components/Views/Approval/index.js | 12 +- .../Views/ApproveView/Approve/index.js | 9 +- .../__snapshots__/index.test.js.snap | 1 + app/components/Views/BrowserTab/index.js | 30 +- app/components/Views/CollectibleView/index.js | 2 +- .../Views/ImportPrivateKey/index.js | 14 +- .../Views/ImportPrivateKeySuccess/index.js | 9 +- app/components/Views/ImportWallet/index.js | 7 + .../PaymentChannelDeposit/index.js | 1 - .../__snapshots__/index.test.js.snap | 2 +- .../PaymentChannelSend/index.js | 8 +- app/components/Views/PaymentChannel/index.js | 11 +- .../Views/RevealPrivateCredential/index.js | 22 +- app/components/Views/Send/index.js | 161 ++++-- app/components/Views/SendFlow/Amount/index.js | 269 +++++++-- .../Views/SendFlow/Amount/index.test.js | 2 +- .../Confirm/__snapshots__/index.test.js.snap | 240 ++------ .../Views/SendFlow/Confirm/index.js | 450 +++++++++++---- .../Views/SendFlow/Confirm/index.test.js | 8 +- app/components/Views/SendFlow/SendTo/index.js | 43 +- .../Views/SendFlow/SendTo/index.test.js | 2 +- .../Views/SendFlow/WarningMessage/index.js | 5 +- .../Views/TransactionDirection/index.js | 3 +- .../Views/TransactionSummary/index.js | 136 +++++ app/components/Views/Wallet/index.js | 15 +- app/core/DeeplinkManager.js | 4 +- app/core/PreventScreenshot.js | 8 + app/core/TransactionsNotificationManager.js | 105 ++-- app/reducers/index.js | 6 +- app/reducers/newTransaction/index.js | 63 --- app/reducers/transaction/index.js | 70 ++- app/reducers/transactionNotification/index.js | 28 + app/store/index.js | 6 + app/styles/common.js | 3 +- app/util/Device.js | 4 + app/util/date.js | 17 + app/util/networks.js | 6 + app/util/number.js | 17 + app/util/transaction-reducer-helpers.js | 26 + app/util/transactions.js | 32 ++ e2e/add-custom-rpc.spec.js | 4 +- e2e/addressbook-tests.spec.js | 2 +- e2e/import-seed-phrase.spec.js | 2 +- ios/MetaMask.xcodeproj/project.pbxproj | 4 +- locales/en.json | 48 +- locales/es.json | 9 +- package.json | 6 +- scripts/build.sh | 2 +- yarn.lock | 18 +- 117 files changed, 5065 insertions(+), 3215 deletions(-) create mode 100644 android/app/src/main/java/io/metamask/nativeModules/PreventScreenshot.java create mode 100644 android/app/src/main/java/io/metamask/nativeModules/PreventScreenshotPackage.java delete mode 100644 app/actions/newTransaction/index.js create mode 100644 app/actions/transactionNotification/index.js create mode 100644 app/components/UI/AccountInfoCard/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/AccountInfoCard/index.js create mode 100644 app/components/UI/AccountInfoCard/index.test.js create mode 100644 app/components/UI/ActionModal/ActionContent/index.js create mode 100644 app/components/UI/SignatureRequest/ExpandedMessage/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/SignatureRequest/ExpandedMessage/index.js create mode 100644 app/components/UI/SignatureRequest/ExpandedMessage/index.test.js create mode 100644 app/components/UI/TransactionActionModal/TransactionActionContent/index.js create mode 100644 app/components/UI/TransactionActionModal/index.js delete mode 100644 app/components/UI/TransactionElement/TransferElement/__snapshots__/index.test.js.snap delete mode 100644 app/components/UI/TransactionElement/TransferElement/index.js delete mode 100644 app/components/UI/TransactionElement/TransferElement/index.test.js create mode 100644 app/components/UI/TransactionElement/utils.js create mode 100644 app/components/UI/TransactionHeader/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/TransactionHeader/index.js create mode 100644 app/components/UI/TransactionHeader/index.test.js create mode 100644 app/components/UI/TxNotification/index.js create mode 100644 app/components/Views/TransactionSummary/index.js create mode 100644 app/core/PreventScreenshot.js delete mode 100644 app/reducers/newTransaction/index.js create mode 100644 app/reducers/transactionNotification/index.js create mode 100644 app/util/transaction-reducer-helpers.js diff --git a/.nvmrc b/.nvmrc index 70047db82e0..e338b86593f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v10.16.3 +v10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0babb023290..4cc1843efed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ ## Current Develop Branch +## v0.2.16 - May 15 2020 +- [#1582](https://github.com/MetaMask/metamask-mobile/pull/1582): Instapay deposit navbar cancel (#1582) +- [#1570](https://github.com/MetaMask/metamask-mobile/pull/1570): Disable confirm screen edit button when no tokens of a payment request (#1570) +- [#1574](https://github.com/MetaMask/metamask-mobile/pull/1574): Ensure collectibles that use 'transfer' method show a fee in tx history list (#1574) +- [#1565](https://github.com/MetaMask/metamask-mobile/pull/1565): Fix validating of amount when sending a collectible (#1565) +- [#1572](https://github.com/MetaMask/metamask-mobile/pull/1572): Fix amount validation for large token payment requests (#1572) +- [#1561](https://github.com/MetaMask/metamask-mobile/pull/1561): V0.2.16 (#1561) +- [#1548](https://github.com/MetaMask/metamask-mobile/pull/1548): Use setTimeout hack (again) to get paste context in token search (#1548) +- [#1465](https://github.com/MetaMask/metamask-mobile/pull/1465): Make send flows consistent (#1465) +- [#1557](https://github.com/MetaMask/metamask-mobile/pull/1557): Fix day and month numbers in toDateFormat (#1557) +- [#1556](https://github.com/MetaMask/metamask-mobile/pull/1556): Fix settings everywhere (#1556) +- [#1552](https://github.com/MetaMask/metamask-mobile/pull/1552): Use gaba@1.11.0 (#1552) +- [#1493](https://github.com/MetaMask/metamask-mobile/pull/1493): Sig request design fixed (#1493) +- [#1517](https://github.com/MetaMask/metamask-mobile/pull/1517): Add new mobile provider (#1517) +- [#1539](https://github.com/MetaMask/metamask-mobile/pull/1539): Use "web-search" keyboardType on iOS (#1539) +- [#1538](https://github.com/MetaMask/metamask-mobile/pull/1538): Detect if site has been added to Favorites (#1538) +- [#1495](https://github.com/MetaMask/metamask-mobile/pull/1495): Feature/block screenshots (#1495) +- [#1487](https://github.com/MetaMask/metamask-mobile/pull/1487): Transaction Header Component (#1487) +- [#1475](https://github.com/MetaMask/metamask-mobile/pull/1475): Improvement/tx status notification (#1475) +- [#1544](https://github.com/MetaMask/metamask-mobile/pull/1544): Add settings to nav bar (#1544) +- [#1521](https://github.com/MetaMask/metamask-mobile/pull/1521): update docs link in readme (#1521) +- [#1545](https://github.com/MetaMask/metamask-mobile/pull/1545): bugfix/check for sai method (#1545) +- [#1524](https://github.com/MetaMask/metamask-mobile/pull/1524): Loosen nvmrc (#1524) + ## v0.2.15 - May 1 2020 - [#1529](https://github.com/MetaMask/metamask-mobile/pull/1529): sentry android production (#1529) - [#1528](https://github.com/MetaMask/metamask-mobile/pull/1528): Bugfix/sentry in circle ci (#1528) diff --git a/README.md b/README.md index 327f79d0cc4..ce88a4a5207 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ MetaMask is a mobile web browser that provides easy access to websites that use For up to the minute news, follow our [Twitter](https://twitter.com/metamask_io) or [Medium](https://medium.com/metamask) pages. -To learn how to develop MetaMask-compatible applications, visit our [Developer Docs](https://metamask.github.io/metamask-docs/). +To learn how to develop MetaMask-compatible applications, visit our [Developer Docs](https://docs.metamask.io). ## MetaMask Mobile diff --git a/android/app/build.gradle b/android/app/build.gradle index cfd9f574046..6452afe2059 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -139,8 +139,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 28 - versionName "0.2.15" + versionCode 29 + versionName "0.2.16" multiDexEnabled true testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy "minReactNative", "minReactNative46" diff --git a/android/app/src/main/java/io/metamask/MainApplication.java b/android/app/src/main/java/io/metamask/MainApplication.java index 6c0d06dc6b1..d32a17f98e0 100644 --- a/android/app/src/main/java/io/metamask/MainApplication.java +++ b/android/app/src/main/java/io/metamask/MainApplication.java @@ -17,6 +17,7 @@ import io.branch.rnbranch.RNBranchPackage; import io.branch.rnbranch.RNBranchModule; import io.metamask.nativeModules.RCTAnalyticsPackage; +import io.metamask.nativeModules.PreventScreenshotPackage; import com.oblador.vectoricons.VectorIconsPackage; import cl.json.RNSharePackage; import com.bitgo.randombytes.RandomBytesPackage; @@ -74,7 +75,8 @@ protected List getPackages() { new RNOSModule(), new RNSharePackage(), new VectorIconsPackage(), - new RCTAnalyticsPackage() + new RCTAnalyticsPackage(), + new PreventScreenshotPackage() ); } diff --git a/android/app/src/main/java/io/metamask/nativeModules/PreventScreenshot.java b/android/app/src/main/java/io/metamask/nativeModules/PreventScreenshot.java new file mode 100644 index 00000000000..56054100944 --- /dev/null +++ b/android/app/src/main/java/io/metamask/nativeModules/PreventScreenshot.java @@ -0,0 +1,55 @@ +package io.metamask.nativeModules; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.Promise; + +import android.view.WindowManager; + +import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread; + +public class PreventScreenshot extends ReactContextBaseJavaModule { + private static final String PREVENT_SCREENSHOT_ERROR_CODE = "PREVENT_SCREENSHOT_ERROR_CODE"; + private final ReactApplicationContext reactContext; + + PreventScreenshot(ReactApplicationContext context) { + super(context); + reactContext = context; + } + + @Override + public String getName() { + return "PreventScreenshot"; + } + + @ReactMethod + public void forbid(Promise promise) { + runOnUiThread(new Runnable() { + @Override + public void run() { + try { + getCurrentActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); + promise.resolve("Done. Screenshot taking locked."); + } catch(Exception e) { + promise.reject(PREVENT_SCREENSHOT_ERROR_CODE, "Forbid screenshot taking failure."); + } + } + }); + } + + @ReactMethod + public void allow(Promise promise) { + runOnUiThread(new Runnable() { + @Override + public void run() { + try { + getCurrentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); + promise.resolve("Done. Screenshot taking unlocked."); + } catch (Exception e) { + promise.reject(PREVENT_SCREENSHOT_ERROR_CODE, "Allow screenshot taking failure."); + } + } + }); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/io/metamask/nativeModules/PreventScreenshotPackage.java b/android/app/src/main/java/io/metamask/nativeModules/PreventScreenshotPackage.java new file mode 100644 index 00000000000..f8ecb9b0f5e --- /dev/null +++ b/android/app/src/main/java/io/metamask/nativeModules/PreventScreenshotPackage.java @@ -0,0 +1,23 @@ +package io.metamask.nativeModules; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class PreventScreenshotPackage implements ReactPackage { + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(new PreventScreenshot(reactContext)); + } +} \ No newline at end of file diff --git a/app/actions/newTransaction/index.js b/app/actions/newTransaction/index.js deleted file mode 100644 index b7f0358b1ae..00000000000 --- a/app/actions/newTransaction/index.js +++ /dev/null @@ -1,72 +0,0 @@ -import TransactionTypes from '../../core/TransactionTypes'; - -const { - ASSET: { ETH, ERC20, ERC721 } -} = TransactionTypes; - -/** - * Clears transaction object completely - */ -export function resetTransaction() { - return { - type: 'RESET_TRANSACTION' - }; -} - -/** - * Starts a new transaction state with an asset - * - * @param {object} selectedAsset - Asset to start the transaction with - */ -export function newAssetTransaction(selectedAsset) { - return { - type: 'NEW_ASSET_TRANSACTION', - selectedAsset, - assetType: selectedAsset.isETH ? ETH : selectedAsset.tokenId ? ERC721 : ERC20 - }; -} - -/** - * Sets transaction to address and ensRecipient in case is available - * - * @param {string} from - Address to send the transaction from - * @param {string} to - Address to send the transaction to - * @param {string} ensRecipient - Resolved ens name to send the transaction to - * @param {string} transactionToName - Resolved address book name for to address - * @param {string} transactionFromName - Resolved address book name for from address - */ -export function setRecipient(from, to, ensRecipient, transactionToName, transactionFromName) { - return { - type: 'SET_RECIPIENT', - from, - to, - ensRecipient, - transactionToName, - transactionFromName - }; -} - -/** - * Sets asset as selectedAsset - * - * @param {object} selectedAsset - Asset to start the transaction with - */ -export function setSelectedAsset(selectedAsset) { - return { - type: 'SET_SELECTED_ASSET', - selectedAsset, - assetType: selectedAsset.isETH ? ETH : selectedAsset.tokenId ? ERC721 : ERC20 - }; -} - -/** - * Sets transaction object to be sent - * - * @param {object} transaction - Transaction object with from, to, data, gas, gasPrice, value - */ -export function prepareTransaction(transaction) { - return { - type: 'PREPARE_TRANSACTION', - transaction - }; -} diff --git a/app/actions/transaction/index.js b/app/actions/transaction/index.js index 495d24488aa..d4ef6210770 100644 --- a/app/actions/transaction/index.js +++ b/app/actions/transaction/index.js @@ -1,9 +1,103 @@ +import TransactionTypes from '../../core/TransactionTypes'; + +const { + ASSET: { ETH, ERC20, ERC721 } +} = TransactionTypes; + /** * Clears transaction object completely */ -export function newTransaction() { +export function resetTransaction() { + return { + type: 'RESET_TRANSACTION' + }; +} + +/** + * Starts a new transaction state with an asset + * + * @param {object} selectedAsset - Asset to start the transaction with + */ +export function newAssetTransaction(selectedAsset) { + return { + type: 'NEW_ASSET_TRANSACTION', + selectedAsset, + assetType: selectedAsset.isETH ? ETH : selectedAsset.tokenId ? ERC721 : ERC20 + }; +} + +/** + * Sets transaction to address and ensRecipient in case is available + * + * @param {string} from - Address to send the transaction from + * @param {string} to - Address to send the transaction to + * @param {string} ensRecipient - Resolved ens name to send the transaction to + * @param {string} transactionToName - Resolved address book name for to address + * @param {string} transactionFromName - Resolved address book name for from address + */ +export function setRecipient(from, to, ensRecipient, transactionToName, transactionFromName) { + return { + type: 'SET_RECIPIENT', + from, + to, + ensRecipient, + transactionToName, + transactionFromName + }; +} + +/** + * Sets asset as selectedAsset + * + * @param {object} selectedAsset - Asset to start the transaction with + */ +export function setSelectedAsset(selectedAsset) { + return { + type: 'SET_SELECTED_ASSET', + selectedAsset, + assetType: selectedAsset.isETH ? ETH : selectedAsset.tokenId ? ERC721 : ERC20 + }; +} + +/** + * Sets transaction object to be sent + * + * @param {object} transaction - Transaction object with from, to, data, gas, gasPrice, value + */ +export function prepareTransaction(transaction) { + return { + type: 'PREPARE_TRANSACTION', + transaction + }; +} + +/** + * Sets transaction object to be sent. All properties can be updated + * + * @param {object} config + * @param {object} config.transaction - Transaction object with from, to, data, gas, gasPrice, value + * @param {string} config.ensRecipient - Resolved ens name to send the transaction to + * @param {string} config.transactionToName - Resolved address book name for to address + * @param {string} config.transactionFromName - Resolved address book name for from address + * @param {object} config.selectedAsset - Asset to start the transaction with + * @param {string} config.assetType - The selectedAsset's type + */ +export function prepareFullTransaction({ + transaction, + ensRecipient, + transactionToName, + transactionFromName, + selectedAsset, + assetType +}) { return { - type: 'NEW_TRANSACTION' + type: 'PREPARE_FULL_TRANSACTION', + transaction, + ensRecipient, + transactionToName, + transactionFromName, + selectedAsset, + assetType }; } diff --git a/app/actions/transactionNotification/index.js b/app/actions/transactionNotification/index.js new file mode 100644 index 00000000000..8c63d107e08 --- /dev/null +++ b/app/actions/transactionNotification/index.js @@ -0,0 +1,15 @@ +export function hideTransactionNotification() { + return { + type: 'HIDE_TRANSACTION_NOTIFICATION' + }; +} + +export function showTransactionNotification({ autodismiss, transaction, status }) { + return { + type: 'SHOW_TRANSACTION_NOTIFICATION', + isVisible: true, + autodismiss, + transaction, + status + }; +} diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 84e40b3acb0..bd10ccef766 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -15,7 +15,6 @@ import { createStackNavigator } from 'react-navigation-stack'; import { createBottomTabNavigator } from 'react-navigation-tabs'; import ENS from 'ethjs-ens'; import GlobalAlert from '../../UI/GlobalAlert'; -import FlashMessage from 'react-native-flash-message'; import BackgroundTimer from 'react-native-background-timer'; import Browser from '../../Views/Browser'; import AddBookmark from '../../Views/AddBookmark'; @@ -59,7 +58,6 @@ import PaymentChannel from '../../Views/PaymentChannel'; import ImportPrivateKeySuccess from '../../Views/ImportPrivateKeySuccess'; import PaymentRequest from '../../UI/PaymentRequest'; import PaymentRequestSuccess from '../../UI/PaymentRequestSuccess'; -import { TransactionNotification } from '../../UI/TransactionNotification'; import TransactionsNotificationManager from '../../../core/TransactionsNotificationManager'; import Engine from '../../../core/Engine'; import AppConstants from '../../../core/AppConstants'; @@ -100,6 +98,8 @@ import Amount from '../../Views/SendFlow/Amount'; import Confirm from '../../Views/SendFlow/Confirm'; import ContactForm from '../../Views/Settings/Contacts/ContactForm'; import TransactionTypes from '../../../core/TransactionTypes'; +import TxNotification from '../../UI/TxNotification'; +import { showTransactionNotification, hideTransactionNotification } from '../../../actions/transactionNotification'; const styles = StyleSheet.create({ flex: { @@ -417,7 +417,27 @@ class Main extends PureComponent { /** * A string representing the network name */ - providerType: PropTypes.string + providerType: PropTypes.string, + /** + * Dispatch showing a transaction notification + */ + showTransactionNotification: PropTypes.func, + /** + * Dispatch hiding a transaction notification + */ + hideTransactionNotification: PropTypes.func, + /** + * Indicates whether the current transaction is a payment channel transaction + */ + isPaymentChannelTransaction: PropTypes.bool, + /** + * Indicates whether the current transaction is a deep link transaction + */ + isPaymentRequest: PropTypes.bool, + /** + /* Identities object required to get account name + */ + identities: PropTypes.object }; state = { @@ -432,7 +452,10 @@ class Main extends PureComponent { paymentChannelRequest: false, paymentChannelRequestLoading: false, paymentChannelRequestCompleted: false, - paymentChannelRequestInfo: {} + paymentChannelRequestInfo: {}, + showExpandedMessage: false, + paymentChannelBalance: null, + paymentChannelReady: false }; backgroundMode = false; @@ -515,7 +538,11 @@ class Main extends PureComponent { }); setTimeout(() => { - TransactionsNotificationManager.init(this.props.navigation); + TransactionsNotificationManager.init( + this.props.navigation, + this.props.showTransactionNotification, + this.props.hideTransactionNotification + ); this.pollForIncomingTransactions(); this.initializeWalletConnect(); @@ -538,16 +565,17 @@ class Main extends PureComponent { let hasSAI = false; Object.keys(this.props.allTokens).forEach(account => { const tokens = this.props.allTokens[account].mainnet; - tokens.forEach(token => { - if (token.address.toLowerCase() === AppConstants.SAI_ADDRESS.toLowerCase()) { - if (this.props.contractBalances[AppConstants.SAI_ADDRESS]) { - const balance = this.props.contractBalances[AppConstants.SAI_ADDRESS]; - if (!balance.isZero()) { - hasSAI = true; + tokens && + tokens.forEach(token => { + if (token.address.toLowerCase() === AppConstants.SAI_ADDRESS.toLowerCase()) { + if (this.props.contractBalances[AppConstants.SAI_ADDRESS]) { + const balance = this.props.contractBalances[AppConstants.SAI_ADDRESS]; + if (!balance.isZero()) { + hasSAI = true; + } } } - } - }); + }); }); if (hasSAI) { @@ -615,8 +643,39 @@ class Main extends PureComponent { WalletConnect.init(); }; + initiatePaymentChannelRequest = ({ amount, to }) => { + this.props.setTransactionObject({ + selectedAsset: { + address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', + decimals: 18, + logo: 'sai.svg', + symbol: 'SAI', + assetBalance: this.state.paymentChannelBalance + }, + value: amount, + readableValue: amount, + transactionTo: to, + from: this.props.selectedAddress, + transactionFromName: this.props.identities[this.props.selectedAddress].name, + paymentChannelTransaction: true, + type: 'PAYMENT_CHANNEL_TRANSACTION' + }); + + this.props.navigation.navigate('Confirm'); + }; + + onPaymentChannelStateChange = state => { + if (state.balance !== this.state.paymentChannelBalance || !this.state.paymentChannelReady) { + this.setState({ + paymentChannelBalance: state.balance, + paymentChannelReady: true + }); + } + }; + initializePaymentChannels = () => { PaymentChannelsClient.init(this.props.selectedAddress); + PaymentChannelsClient.hub.on('state::change', this.onPaymentChannelStateChange); PaymentChannelsClient.hub.on('payment::request', async request => { const validRequest = { ...request }; // Validate amount @@ -642,48 +701,34 @@ class Main extends PureComponent { // Check if ENS and resolve the address before sending if (isENS(request.to)) { - this.setState( - { - paymentChannelRequest: true, - paymentChannelRequestInfo: null - }, - () => { - InteractionManager.runAfterInteractions(async () => { - const { - state: { network }, - provider - } = Engine.context.NetworkController; - const ensProvider = new ENS({ provider, network }); - try { - const resolvedAddress = await ensProvider.lookup(request.to.trim()); - if (isValidAddress(resolvedAddress)) { - validRequest.to = resolvedAddress; - validRequest.ensName = request.to; - this.setState({ - paymentChannelRequest: true, - paymentChannelRequestInfo: validRequest - }); - return; - } - } catch (e) { - Logger.log('Error with payment request', request); - } - Alert.alert( - strings('payment_channel_request.title_error'), - strings('payment_channel_request.address_error_message') - ); - this.setState({ - paymentChannelRequest: false, - paymentChannelRequestInfo: null - }); - }); + InteractionManager.runAfterInteractions(async () => { + const { + state: { network }, + provider + } = Engine.context.NetworkController; + const ensProvider = new ENS({ provider, network }); + try { + const resolvedAddress = await ensProvider.lookup(request.to.trim()); + if (isValidAddress(resolvedAddress)) { + validRequest.to = resolvedAddress; + validRequest.ensName = request.to; + this.initiatePaymentChannelRequest(validRequest); + return; + } + } catch (e) { + Logger.log('Error with payment request', request); } - ); - } else { - this.setState({ - paymentChannelRequest: true, - paymentChannelRequestInfo: validRequest + Alert.alert( + strings('payment_channel_request.title_error'), + strings('payment_channel_request.address_error_message') + ); + this.setState({ + paymentChannelRequest: false, + paymentChannelRequestInfo: null + }); }); + } else { + this.initiatePaymentChannelRequest(validRequest); } }); @@ -916,8 +961,19 @@ class Main extends PureComponent { this.setState({ signMessage: false }); }; + toggleExpandedMessage = () => { + this.setState({ showExpandedMessage: !this.state.showExpandedMessage }); + }; + renderSigningModal = () => { - const { signMessage, signMessageParams, signType, currentPageTitle, currentPageUrl } = this.state; + const { + signMessage, + signMessageParams, + signType, + currentPageTitle, + currentPageUrl, + showExpandedMessage + } = this.state; return ( )} {signType === 'typed' && ( @@ -947,6 +1005,8 @@ class Main extends PureComponent { onCancel={this.onSignAction} onConfirm={this.onSignAction} currentPageInformation={{ title: currentPageTitle, url: currentPageUrl }} + toggleExpandedMessage={this.toggleExpandedMessage} + showExpandedMessage={showExpandedMessage} /> )} {signType === 'eth' && ( @@ -956,6 +1016,8 @@ class Main extends PureComponent { onCancel={this.onSignAction} onConfirm={this.onSignAction} currentPageInformation={{ title: currentPageTitle, url: currentPageUrl }} + toggleExpandedMessage={this.toggleExpandedMessage} + showExpandedMessage={showExpandedMessage} /> )} @@ -1061,23 +1123,25 @@ class Main extends PureComponent { }; render() { + const { isPaymentChannelTransaction, isPaymentRequest } = this.props; const { forceReload } = this.state; - return ( - {!forceReload ? : this.renderLoader()} + {!forceReload ? ( + + ) : ( + this.renderLoader() + )} - + {this.renderSigningModal()} {this.renderWalletConnectSessionRequestModal()} - {this.renderPaymentChannelRequestApproval()} {this.renderWalletConnectReturnModal()} ); @@ -1092,12 +1156,17 @@ const mapStateToProps = state => ({ paymentChannelsEnabled: state.settings.paymentChannelsEnabled, providerType: state.engine.backgroundState.NetworkController.provider.type, allTokens: state.engine.backgroundState.AssetsController.allTokens, - contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances + contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances, + isPaymentChannelTransaction: state.transaction.paymentChannelTransaction, + isPaymentRequest: state.transaction.paymentRequest, + identities: state.engine.backgroundState.PreferencesController.identities }); const mapDispatchToProps = dispatch => ({ setEtherTransaction: transaction => dispatch(setEtherTransaction(transaction)), - setTransactionObject: transaction => dispatch(setTransactionObject(transaction)) + setTransactionObject: transaction => dispatch(setTransactionObject(transaction)), + showTransactionNotification: args => dispatch(showTransactionNotification(args)), + hideTransactionNotification: () => dispatch(hideTransactionNotification()) }); export default connect( diff --git a/app/components/UI/AccountInfoCard/__snapshots__/index.test.js.snap b/app/components/UI/AccountInfoCard/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..cbc478b3d33 --- /dev/null +++ b/app/components/UI/AccountInfoCard/__snapshots__/index.test.js.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountInfoCard should render correctly 1`] = ` + + + + + + 0x0...0x0 + + + ( + 0x0...0x0 + ) + + + + Balance: + + 0 undefined + + (< 0.00001 eth) + + + +`; diff --git a/app/components/UI/AccountInfoCard/index.js b/app/components/UI/AccountInfoCard/index.js new file mode 100644 index 00000000000..cfe9c83d2e9 --- /dev/null +++ b/app/components/UI/AccountInfoCard/index.js @@ -0,0 +1,132 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, Text } from 'react-native'; +import { colors, fontStyles } from '../../../styles/common'; +import { renderFromWei, weiToFiat, hexToBN } from '../../../util/number'; +import Identicon from '../Identicon'; +import { strings } from '../../../../locales/i18n'; +import { connect } from 'react-redux'; +import { renderAccountName, renderShortAddress } from '../../../util/address'; +import { getTicker } from '../../../util/transactions'; + +const styles = StyleSheet.create({ + accountInformation: { + flexDirection: 'row', + justifyContent: 'flex-start', + borderWidth: 1, + borderColor: colors.grey200, + borderRadius: 10, + padding: 16 + }, + identicon: { + marginRight: 8 + }, + accountInfoRow: { + flexGrow: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'flex-start' + }, + accountNameAndAddress: { + width: '100%', + flexDirection: 'row', + justifyContent: 'flex-start' + }, + accountName: { + maxWidth: '55%', + ...fontStyles.bold, + fontSize: 16, + marginRight: 2 + }, + accountAddress: { + flexGrow: 1, + ...fontStyles.bold, + fontSize: 16 + }, + balanceText: { + ...fontStyles.thin, + fontSize: 14, + alignSelf: 'flex-start' + } +}); + +class AccountInfoCard extends PureComponent { + static propTypes = { + /** + * Map of accounts to information objects including balances + */ + accounts: PropTypes.object, + /** + * List of accounts from the PreferencesController + */ + identities: PropTypes.object, + /** + * A string that represents the selected address + */ + selectedAddress: PropTypes.string, + /** + * A number that specifies the ETH/USD conversion rate + */ + conversionRate: PropTypes.number, + /** + * The selected currency + */ + currentCurrency: PropTypes.string, + /** + * Declares the operation being performed i.e. 'signing' + */ + operation: PropTypes.string, + /** + * Current selected ticker + */ + ticker: PropTypes.string + }; + + render() { + const { + accounts, + selectedAddress, + identities, + conversionRate, + currentCurrency, + operation, + ticker + } = this.props; + const weiBalance = hexToBN(accounts[selectedAddress].balance); + const balance = `(${renderFromWei(weiBalance)} ${getTicker(ticker)})`; + const accountLabel = renderAccountName(selectedAddress, identities); + const address = renderShortAddress(selectedAddress); + const dollarBalance = weiToFiat(weiBalance, conversionRate, currentCurrency); + return ( + + + + + + {accountLabel} + + + ({address}) + + + {operation === 'signing' ? null : ( + + {strings('signature_request.balance_title')} {dollarBalance} {balance} + + )} + + + ); + } +} + +const mapStateToProps = state => ({ + accounts: state.engine.backgroundState.AccountTrackerController.accounts, + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + identities: state.engine.backgroundState.PreferencesController.identities, + conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, + currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, + ticker: state.engine.backgroundState.NetworkController.provider.ticker +}); + +export default connect(mapStateToProps)(AccountInfoCard); diff --git a/app/components/UI/AccountInfoCard/index.test.js b/app/components/UI/AccountInfoCard/index.test.js new file mode 100644 index 00000000000..0f947b8750e --- /dev/null +++ b/app/components/UI/AccountInfoCard/index.test.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import configureMockStore from 'redux-mock-store'; +import AccountInfoCard from './'; + +const mockStore = configureMockStore(); + +describe('AccountInfoCard', () => { + it('should render correctly', () => { + const initialState = { + engine: { + backgroundState: { + AccountTrackerController: { + accounts: { + '0x0': { + balance: 200 + } + } + }, + PreferencesController: { + selectedAddress: '0x0', + identities: { + address: '0x0', + name: 'Account 1' + } + }, + CurrencyRateController: { + conversionRate: 10 + }, + NetworkController: { + provider: { + ticker: 'eth' + } + } + } + } + }; + + const wrapper = shallow(, { + context: { store: mockStore(initialState) } + }); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/AccountList/AccountElement/index.js b/app/components/UI/AccountList/AccountElement/index.js index 50b2044701e..9be650b9f2c 100644 --- a/app/components/UI/AccountList/AccountElement/index.js +++ b/app/components/UI/AccountList/AccountElement/index.js @@ -17,6 +17,9 @@ const styles = StyleSheet.create({ paddingVertical: 20, height: 80 }, + disabledAccount: { + opacity: 0.5 + }, accountInfo: { marginLeft: 15, marginRight: 0, @@ -28,12 +31,20 @@ const styles = StyleSheet.create({ color: colors.fontPrimary, ...fontStyles.normal }, + accountBalanceWrapper: { + display: 'flex', + flexDirection: 'row' + }, accountBalance: { paddingTop: 5, fontSize: 12, color: colors.fontSecondary, ...fontStyles.normal }, + accountBalanceError: { + color: colors.fontError, + marginLeft: 4 + }, importedView: { flex: 0.5, alignItems: 'center', @@ -79,6 +90,10 @@ export default class AccountElement extends PureComponent { * Current ticker */ ticker: PropTypes.string, + /** + * Whether the account element should be disabled (opaque and not clickable) + */ + disabled: PropTypes.bool, item: PropTypes.object }; @@ -95,7 +110,8 @@ export default class AccountElement extends PureComponent { }; render() { - const { address, balance, ticker, name, isSelected, isImported } = this.props.item; + const { disabled } = this.props; + const { address, balance, ticker, name, isSelected, isImported, balanceError } = this.props.item; const selected = isSelected ? : null; const imported = isImported ? ( @@ -106,10 +122,11 @@ export default class AccountElement extends PureComponent { ) : null; return ( @@ -117,9 +134,14 @@ export default class AccountElement extends PureComponent { {name} - - {renderFromWei(balance)} {getTicker(ticker)} - + + + {renderFromWei(balance)} {getTicker(ticker)} + + {!!balanceError && ( + {balanceError} + )} + {!!imported && {imported}} {selected} diff --git a/app/components/UI/AccountList/index.js b/app/components/UI/AccountList/index.js index ae30e686b4a..19bd4bf76f9 100644 --- a/app/components/UI/AccountList/index.js +++ b/app/components/UI/AccountList/index.js @@ -105,7 +105,11 @@ class AccountList extends PureComponent { /** * Whether it will show options to create or import accounts */ - enableAccountsAddition: PropTypes.bool + enableAccountsAddition: PropTypes.bool, + /** + * function to generate an error string based on a passed balance + */ + getBalanceError: PropTypes.func }; state = { @@ -268,12 +272,18 @@ class AccountList extends PureComponent { renderItem = ({ item }) => { const { ticker } = this.props; return ( - + ); }; getAccounts() { - const { accounts, identities, selectedAddress, keyrings } = this.props; + const { accounts, identities, selectedAddress, keyrings, getBalanceError } = this.props; // This is a temporary fix until we can read the state from GABA const allKeyrings = keyrings && keyrings.length ? keyrings : Engine.context.KeyringController.state.keyrings; @@ -291,7 +301,17 @@ class AccountList extends PureComponent { if (accounts[identityAddressChecksummed]) { balance = accounts[identityAddressChecksummed].balance; } - return { index, name, address: identityAddressChecksummed, balance, isSelected, isImported }; + + const balanceError = getBalanceError ? getBalanceError(balance) : null; + return { + index, + name, + address: identityAddressChecksummed, + balance, + isSelected, + isImported, + balanceError + }; }); } diff --git a/app/components/UI/ActionModal/ActionContent/index.js b/app/components/UI/ActionModal/ActionContent/index.js new file mode 100644 index 00000000000..763df49b8e8 --- /dev/null +++ b/app/components/UI/ActionModal/ActionContent/index.js @@ -0,0 +1,156 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View } from 'react-native'; +import { colors } from '../../../../styles/common'; +import StyledButton from '../../StyledButton'; +import { strings } from '../../../../../locales/i18n'; + +const styles = StyleSheet.create({ + viewWrapper: { + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + marginHorizontal: 24 + }, + viewContainer: { + width: '100%', + backgroundColor: colors.white, + borderRadius: 10 + }, + actionContainer: { + borderTopColor: colors.grey200, + borderTopWidth: 1, + flexDirection: 'row', + padding: 16 + }, + childrenContainer: { + minHeight: 250, + width: '100%', + + flexDirection: 'row', + alignItems: 'center' + }, + button: { + flex: 1 + }, + cancel: { + marginRight: 8 + }, + confirm: { + marginLeft: 8 + } +}); + +/** + * View that renders the content of an action modal + * The objective of this component is to reuse it in other places and not + * only on ActionModal component + */ +export default function ActionContent({ + cancelTestID, + confirmTestID, + cancelText, + children, + confirmText, + confirmDisabled, + cancelButtonMode, + confirmButtonMode, + displayCancelButton, + displayConfirmButton, + onCancelPress, + onConfirmPress +}) { + return ( + + + {children} + + {displayCancelButton && ( + + {cancelText} + + )} + {displayConfirmButton && ( + + {confirmText} + + )} + + + + ); +} + +ActionContent.defaultProps = { + cancelButtonMode: 'neutral', + confirmButtonMode: 'warning', + confirmTestID: '', + cancelTestID: '', + cancelText: strings('action_view.cancel'), + confirmText: strings('action_view.confirm'), + confirmDisabled: false, + displayCancelButton: true, + displayConfirmButton: true +}; + +ActionContent.propTypes = { + /** + * TestID for the cancel button + */ + cancelTestID: PropTypes.string, + /** + * TestID for the confirm button + */ + confirmTestID: PropTypes.string, + /** + * Text to show in the cancel button + */ + cancelText: PropTypes.string, + /** + * Content to display above the action buttons + */ + children: PropTypes.node, + /** + * Type of button to show as the cancel button + */ + cancelButtonMode: PropTypes.oneOf(['cancel', 'neutral', 'confirm', 'normal']), + /** + * Type of button to show as the confirm button + */ + confirmButtonMode: PropTypes.oneOf(['normal', 'confirm', 'warning']), + /** + * Whether confirm button is disabled + */ + confirmDisabled: PropTypes.bool, + /** + * Text to show in the confirm button + */ + confirmText: PropTypes.string, + /** + * Whether cancel button should be displayed + */ + displayCancelButton: PropTypes.bool, + /** + * Whether confirm button should be displayed + */ + displayConfirmButton: PropTypes.bool, + /** + * Called when the cancel button is clicked + */ + onCancelPress: PropTypes.func, + /** + * Called when the confirm button is clicked + */ + onConfirmPress: PropTypes.func +}; diff --git a/app/components/UI/ActionModal/__snapshots__/index.test.js.snap b/app/components/UI/ActionModal/__snapshots__/index.test.js.snap index 86d54eff6b6..2e7076fe32b 100644 --- a/app/components/UI/ActionModal/__snapshots__/index.test.js.snap +++ b/app/components/UI/ActionModal/__snapshots__/index.test.js.snap @@ -41,99 +41,16 @@ exports[`ActionModal should render correctly 1`] = ` swipeThreshold={100} useNativeDriver={false} > - - - - - - Cancel - - - Confirm - - - - + `; diff --git a/app/components/UI/ActionModal/index.js b/app/components/UI/ActionModal/index.js index 3098d9ffef4..0e6a5699f30 100644 --- a/app/components/UI/ActionModal/index.js +++ b/app/components/UI/ActionModal/index.js @@ -1,46 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { StyleSheet, View } from 'react-native'; +import { StyleSheet } from 'react-native'; import Modal from 'react-native-modal'; -import { colors } from '../../../styles/common'; -import StyledButton from '../StyledButton'; import { strings } from '../../../../locales/i18n'; +import ActionContent from './ActionContent'; const styles = StyleSheet.create({ modal: { margin: 0, width: '100%' - }, - modalView: { - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - marginHorizontal: 24 - }, - modalContainer: { - width: '100%', - backgroundColor: colors.white, - borderRadius: 10 - }, - actionContainer: { - borderTopColor: colors.grey200, - borderTopWidth: 1, - flexDirection: 'row', - padding: 16 - }, - childrenContainer: { - minHeight: 250, - flexDirection: 'row', - alignItems: 'center' - }, - button: { - flex: 1 - }, - cancel: { - marginRight: 8 - }, - confirm: { - marginLeft: 8 } }); @@ -72,34 +40,21 @@ export default function ActionModal({ onSwipeComplete={onRequestClose} swipeDirection={'down'} > - - - {children} - - {displayCancelButton && ( - - {cancelText} - - )} - {displayConfirmButton && ( - - {confirmText} - - )} - - - + + {children} + ); } diff --git a/app/components/UI/ActionView/__snapshots__/index.test.js.snap b/app/components/UI/ActionView/__snapshots__/index.test.js.snap index 7f6b72ee84a..8934c1f53ef 100644 --- a/app/components/UI/ActionView/__snapshots__/index.test.js.snap +++ b/app/components/UI/ActionView/__snapshots__/index.test.js.snap @@ -41,8 +41,6 @@ exports[`ActionView should render correctly 1`] = ` diff --git a/app/components/UI/AssetOverview/index.js b/app/components/UI/AssetOverview/index.js index 7b53dbdbccd..78c17c83e0b 100644 --- a/app/components/UI/AssetOverview/index.js +++ b/app/components/UI/AssetOverview/index.js @@ -11,7 +11,7 @@ import { connect } from 'react-redux'; import { renderFromTokenMinimalUnit, balanceToFiat, renderFromWei, weiToFiat, hexToBN } from '../../../util/number'; import { safeToChecksumAddress } from '../../../util/address'; import { getEther } from '../../../util/transactions'; -import { newAssetTransaction } from '../../../actions/newTransaction'; +import { newAssetTransaction } from '../../../actions/transaction'; const styles = StyleSheet.create({ wrapper: { diff --git a/app/components/UI/AssetSearch/__snapshots__/index.test.js.snap b/app/components/UI/AssetSearch/__snapshots__/index.test.js.snap index 2349527ce43..51870cfda42 100644 --- a/app/components/UI/AssetSearch/__snapshots__/index.test.js.snap +++ b/app/components/UI/AssetSearch/__snapshots__/index.test.js.snap @@ -34,11 +34,15 @@ exports[`AssetSearch should render correctly 1`] = ` placeholderTextColor="#d6d9dc" rejectResponderTermination={true} style={ - Object { - "flex": 1, - "fontFamily": "CircularStd-Medium", - "fontWeight": "400", - } + Array [ + Object { + "fontFamily": "CircularStd-Medium", + "fontWeight": "400", + }, + Object { + "width": "85%", + }, + ] } testID="input-search-asset" underlineColorAndroid="transparent" diff --git a/app/components/UI/AssetSearch/index.js b/app/components/UI/AssetSearch/index.js index ea222f5c7e4..a6828ff1b4a 100644 --- a/app/components/UI/AssetSearch/index.js +++ b/app/components/UI/AssetSearch/index.js @@ -20,7 +20,6 @@ const styles = StyleSheet.create({ borderColor: colors.grey100 }, textInput: { - flex: 1, ...fontStyles.normal }, icon: { @@ -50,7 +49,8 @@ const fuse = new Fuse(contractList, { */ export default class AssetSearch extends PureComponent { state = { - searchQuery: '' + searchQuery: '', + inputWidth: '85%' }; static propTypes = { @@ -60,6 +60,10 @@ export default class AssetSearch extends PureComponent { onSearch: PropTypes.func }; + componentDidMount() { + setTimeout(() => this.setState({ inputWidth: '86%' }), 100); + } + handleSearch = searchQuery => { this.setState({ searchQuery }); const fuseSearchResult = fuse.search(searchQuery); @@ -71,13 +75,13 @@ export default class AssetSearch extends PureComponent { }; render = () => { - const { searchQuery } = this.state; + const { searchQuery, inputWidth } = this.state; return ( - - - diff --git a/app/components/UI/EthInput/index.js b/app/components/UI/EthInput/index.js index 8ee41f57281..afc9b1e163d 100644 --- a/app/components/UI/EthInput/index.js +++ b/app/components/UI/EthInput/index.js @@ -21,7 +21,7 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import ElevatedView from 'react-native-elevated-view'; import CollectibleImage from '../CollectibleImage'; import SelectableAsset from './SelectableAsset'; -import { getTicker } from '../../../util/transactions'; +import { getTicker, getNormalizedTxState } from '../../../util/transactions'; import FontAwesome from 'react-native-vector-icons/FontAwesome'; import Device from '../../../util/Device'; @@ -695,7 +695,7 @@ const mapStateToProps = state => ({ tokens: state.engine.backgroundState.AssetsController.tokens, tokenBalances: state.engine.backgroundState.TokenBalancesController.contractBalances, collectibles: state.engine.backgroundState.AssetsController.collectibles, - transaction: state.transaction, + transaction: getNormalizedTxState(state), primaryCurrency: state.settings.primaryCurrency, ticker: state.engine.backgroundState.NetworkController.provider.ticker }); diff --git a/app/components/UI/EthereumAddress/index.js b/app/components/UI/EthereumAddress/index.js index f195bd0c74f..213ae91e4dc 100644 --- a/app/components/UI/EthereumAddress/index.js +++ b/app/components/UI/EthereumAddress/index.js @@ -72,7 +72,6 @@ class EthereumAddress extends PureComponent { formatAndResolveIfNeeded() { const { address, type } = this.props; const formattedAddress = this.formatAddress(address, type); - // eslint-disable-next-line react/no-did-update-set-state this.setState({ address: formattedAddress, ensName: null }); this.doReverseLookup(); } diff --git a/app/components/UI/MessageSign/__snapshots__/index.test.js.snap b/app/components/UI/MessageSign/__snapshots__/index.test.js.snap index 0f57b2b9df2..befb2011162 100644 --- a/app/components/UI/MessageSign/__snapshots__/index.test.js.snap +++ b/app/components/UI/MessageSign/__snapshots__/index.test.js.snap @@ -1,72 +1,31 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`MessageSign should render correctly 1`] = ` - - - - Signature Request - - - - - - Message: - - - message - - - - + message + + + `; diff --git a/app/components/UI/MessageSign/index.js b/app/components/UI/MessageSign/index.js index 7d90278d6b7..cfe97e510a7 100644 --- a/app/components/UI/MessageSign/index.js +++ b/app/components/UI/MessageSign/index.js @@ -1,42 +1,24 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { StyleSheet, View, Text } from 'react-native'; -import { colors, fontStyles } from '../../../styles/common'; +import { fontStyles } from '../../../styles/common'; import Engine from '../../../core/Engine'; import SignatureRequest from '../SignatureRequest'; -import { strings } from '../../../../locales/i18n'; -import Device from '../../../util/Device'; +import ExpandedMessage from '../SignatureRequest/ExpandedMessage'; const styles = StyleSheet.create({ - root: { - backgroundColor: colors.white, - minHeight: '90%', - borderTopLeftRadius: 10, - borderTopRightRadius: 10, - paddingBottom: Device.isIphoneX() ? 20 : 0 - }, - informationRow: { - borderBottomColor: colors.grey200, - borderBottomWidth: 1, - padding: 20 - }, - messageLabelText: { - ...fontStyles.normal, - margin: 5, - fontSize: 16 - }, - title: { + expandedMessage: { textAlign: 'center', - fontSize: 18, - marginVertical: 12, - marginHorizontal: 20, - color: colors.fontPrimary, - ...fontStyles.bold + ...fontStyles.regular, + fontSize: 14 + }, + messageWrapper: { + marginBottom: 4 } }); /** - * PureComponent that supports eth_sign + * Component that supports eth_sign */ export default class MessageSign extends PureComponent { static propTypes = { @@ -59,7 +41,19 @@ export default class MessageSign extends PureComponent { /** * Object containing current page title and url */ - currentPageInformation: PropTypes.object + currentPageInformation: PropTypes.object, + /** + * Hides or shows the expanded signing message + */ + toggleExpandedMessage: PropTypes.func, + /** + * Indicated whether or not the expanded message is shown + */ + showExpandedMessage: PropTypes.bool + }; + + state = { + truncateMessage: false }; signMessage = async () => { @@ -88,29 +82,56 @@ export default class MessageSign extends PureComponent { this.props.onConfirm(); }; + renderMessageText = () => { + const { messageParams, showExpandedMessage } = this.props; + const { truncateMessage } = this.state; + + let messageText; + if (showExpandedMessage) { + messageText = {messageParams.data}; + } else { + messageText = truncateMessage ? ( + + {messageParams.data} + + ) : ( + {messageParams.data} + ); + } + return messageText; + }; + + shouldTruncateMessage = e => { + if (e.nativeEvent.lines.length > 5) { + this.setState({ truncateMessage: true }); + return; + } + this.setState({ truncateMessage: false }); + }; + render() { - const { messageParams, currentPageInformation, navigation } = this.props; - return ( - - - - {strings('signature_request.title')} - - - - - {strings('signature_request.message')} - {messageParams.data} - - - + const { currentPageInformation, navigation, showExpandedMessage, toggleExpandedMessage } = this.props; + const rootView = showExpandedMessage ? ( + + ) : ( + + {this.renderMessageText()} + ); + return rootView; } } diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index c9cbb3ddf02..70e85da6fe7 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -236,7 +236,7 @@ export function getEditableOptions(title, navigation) { ), headerRight: !addMode ? ( - rightAction()} style={styles.backButton}> + {editMode ? strings('address_book.edit') : strings('address_book.cancel')} @@ -322,22 +322,47 @@ export function getPaymentRequestSuccessOptionsTitle(navigation) { * @param {string} title - Title in string format * @returns {Object} - Corresponding navbar options containing title and headerTitleStyle */ -export function getTransactionOptionsTitle(title, navigation) { +export function getTransactionOptionsTitle(_title, navigation) { const transactionMode = navigation.getParam('mode', ''); + const { routeName } = navigation.state; const leftText = transactionMode === 'edit' ? strings('transaction.cancel') : strings('transaction.edit'); - const toEditLeftAction = navigation.getParam('dispatch', () => { + const disableModeChange = navigation.getParam('disableModeChange'); + const modeChange = navigation.getParam('dispatch', () => { ''; }); - const leftAction = transactionMode === 'edit' ? () => navigation.pop() : () => toEditLeftAction('edit'); + const leftAction = () => modeChange('edit'); + const rightAction = () => navigation.pop(); + const rightText = strings('transaction.cancel'); + const title = transactionMode === 'edit' && routeName !== 'PaymentChannelDeposit' ? 'transaction.edit' : _title; return { headerTitle: , - headerLeft: ( - // eslint-disable-next-line react/jsx-no-bind - - {leftText} - - ), - headerRight: + headerLeft: + transactionMode !== 'edit' ? ( + // eslint-disable-next-line react/jsx-no-bind + + + {leftText} + + + ) : ( + + ), + headerRight: + routeName === 'Send' || routeName === 'PaymentChannelDeposit' ? ( + // eslint-disable-next-line react/jsx-no-bind + + {rightText} + + ) : ( + + ) }; } @@ -349,6 +374,12 @@ export function getApproveNavbar(title) { }; } +const sendTitleToPaymentChannelTitleMap = { + 'send.send_to': 'payment_channel.insta_pay_send_to', + 'send.amount': 'payment_channel.insta_pay_amount', + 'send.confirm': 'payment_channel.insta_pay_confirm' +}; + /** * Function that returns the navigation options * This is used by views in send flow @@ -356,7 +387,7 @@ export function getApproveNavbar(title) { * @param {string} title - Title in string format * @returns {Object} - Corresponding navbar options containing title and headerTitleStyle */ -export function getSendFlowTitle(title, navigation) { +export function getSendFlowTitle(title, navigation, screenProps) { const rightAction = () => { const providerType = navigation.getParam('providerType', ''); trackEventWithParameters(ANALYTICS_EVENT_OPTS.SEND_FLOW_CANCEL, { @@ -365,13 +396,20 @@ export function getSendFlowTitle(title, navigation) { }); navigation.dismiss(); }; - const leftAction = () => navigation.pop(); - const canGoBack = title !== 'send.send_to'; + const { routeName } = navigation.state; + const leftAction = + screenProps.isPaymentChannelTransaction && routeName === 'Confirm' + ? () => navigation.navigate('Amount') + : () => navigation.pop(); + const canGoBack = title !== 'send.send_to' && !screenProps.isPaymentRequest; + + const titleToRender = screenProps.isPaymentChannelTransaction ? sendTitleToPaymentChannelTitleMap[title] : title; + return { - headerTitle: , + headerTitle: , headerRight: ( // eslint-disable-next-line react/jsx-no-bind - + {strings('transaction.cancel')} ), diff --git a/app/components/UI/PaymentRequest/index.js b/app/components/UI/PaymentRequest/index.js index 6b4195ae2f7..7072ac62884 100644 --- a/app/components/UI/PaymentRequest/index.js +++ b/app/components/UI/PaymentRequest/index.js @@ -202,7 +202,7 @@ const defaultAssets = [ address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', decimals: 18, erc20: true, - logo: 'dai.svg', + logo: 'sai.svg', name: 'Sai Stablecoin v1.0', symbol: 'SAI' } diff --git a/app/components/UI/PersonalSign/__snapshots__/index.test.js.snap b/app/components/UI/PersonalSign/__snapshots__/index.test.js.snap index 21c7fdbee1e..df833bf5701 100644 --- a/app/components/UI/PersonalSign/__snapshots__/index.test.js.snap +++ b/app/components/UI/PersonalSign/__snapshots__/index.test.js.snap @@ -1,81 +1,46 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`PersonalSign should render correctly 1`] = ` - - - - Signature Request - - - - - - Message: - - - - + + + `; diff --git a/app/components/UI/PersonalSign/index.js b/app/components/UI/PersonalSign/index.js index c45b67eec26..9f5edf03caf 100644 --- a/app/components/UI/PersonalSign/index.js +++ b/app/components/UI/PersonalSign/index.js @@ -4,47 +4,26 @@ import { StyleSheet, View, Text } from 'react-native'; import { colors, fontStyles } from '../../../styles/common'; import Engine from '../../../core/Engine'; import SignatureRequest from '../SignatureRequest'; -import { strings } from '../../../../locales/i18n'; +import ExpandedMessage from '../SignatureRequest/ExpandedMessage'; import { util } from 'gaba'; -import Device from '../../../util/Device'; const styles = StyleSheet.create({ - root: { - backgroundColor: colors.white, - minHeight: '90%', - borderTopLeftRadius: 10, - borderTopRightRadius: 10, - paddingBottom: Device.isIphoneX() ? 20 : 0 - }, - informationRow: { - borderBottomColor: colors.grey200, - borderBottomWidth: 1, - padding: 20 - }, - messageLabelText: { - ...fontStyles.normal, - margin: 5, - fontSize: 16 - }, messageText: { - flex: 1, - margin: 5, fontSize: 14, color: colors.fontPrimary, - ...fontStyles.normal + ...fontStyles.normal, + textAlign: 'center' }, - title: { - textAlign: 'center', - fontSize: 18, - marginVertical: 12, - marginHorizontal: 20, - color: colors.fontPrimary, - ...fontStyles.bold + textLeft: { + textAlign: 'left' + }, + messageWrapper: { + marginBottom: 4 } }); /** - * PureComponent that supports personal_sign + * Component that supports personal_sign */ export default class PersonalSign extends PureComponent { static propTypes = { @@ -67,7 +46,19 @@ export default class PersonalSign extends PureComponent { /** * Object containing current page title and url */ - currentPageInformation: PropTypes.object + currentPageInformation: PropTypes.object, + /** + * Hides or shows the expanded signing message + */ + toggleExpandedMessage: PropTypes.func, + /** + * Indicated whether or not the expanded message is shown + */ + showExpandedMessage: PropTypes.bool + }; + + state = { + truncateMessage: false }; signMessage = async () => { @@ -96,35 +87,62 @@ export default class PersonalSign extends PureComponent { this.props.onConfirm(); }; + renderMessageText = () => { + const { messageParams, showExpandedMessage } = this.props; + const { truncateMessage } = this.state; + const textChild = util + .hexToText(messageParams.data) + .split('\n') + .map((line, i) => ( + + {line} + + )); + let messageText; + if (showExpandedMessage) { + messageText = textChild; + } else { + messageText = truncateMessage ? ( + + {textChild} + + ) : ( + {textChild} + ); + } + return messageText; + }; + + shouldTruncateMessage = e => { + if (e.nativeEvent.lines.length > 5) { + this.setState({ truncateMessage: true }); + return; + } + this.setState({ truncateMessage: false }); + }; + render() { - const { messageParams, currentPageInformation } = this.props; - return ( - - - - {strings('signature_request.title')} - - - - - {strings('signature_request.message')} - {util - .hexToText(messageParams.data) - .split('\n') - .map((line, i) => ( - - {line} - - ))} - - - + const { currentPageInformation, toggleExpandedMessage, showExpandedMessage } = this.props; + const rootView = showExpandedMessage ? ( + + ) : ( + + {this.renderMessageText()} + ); + return rootView; } } diff --git a/app/components/UI/SignatureRequest/ExpandedMessage/__snapshots__/index.test.js.snap b/app/components/UI/SignatureRequest/ExpandedMessage/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..b5a4bc7d3a1 --- /dev/null +++ b/app/components/UI/SignatureRequest/ExpandedMessage/__snapshots__/index.test.js.snap @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExpandedMessage should render correctly 1`] = ` + + + + + Message + + + + + + + Message from + + url + + + + + + + + +`; diff --git a/app/components/UI/SignatureRequest/ExpandedMessage/index.js b/app/components/UI/SignatureRequest/ExpandedMessage/index.js new file mode 100644 index 00000000000..660d51e6f75 --- /dev/null +++ b/app/components/UI/SignatureRequest/ExpandedMessage/index.js @@ -0,0 +1,105 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, Text, TouchableOpacity, ScrollView, TouchableWithoutFeedback } from 'react-native'; +import { colors, fontStyles, baseStyles } from '../../../../styles/common'; +import WebsiteIcon from '../../WebsiteIcon'; +import Ionicons from 'react-native-vector-icons/Ionicons'; +import { strings } from '../../../../../locales/i18n'; +import Device from '../../../../util/Device'; +import { getHost } from '../../../../util/browser'; + +const styles = StyleSheet.create({ + expandedRoot: { + backgroundColor: colors.white, + minHeight: Device.isIos() ? '70%' : '80%', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + padding: 24, + paddingBottom: Device.isIphoneX() ? 44 : 24 + }, + + expandedMessageHeader: { + width: '100%', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 20 + }, + arrowIcon: { + ...baseStyles.flexGrow, + color: colors.grey200 + }, + iconHidden: { + ...baseStyles.flexGrow + }, + messageLabelTextExpanded: { + ...baseStyles.flexGrow, + textAlign: 'center', + ...fontStyles.bold, + fontSize: 16 + }, + messageIntroWrapper: { + alignItems: 'center', + marginBottom: 20 + }, + domainLogo: { + width: 40, + height: 40, + borderRadius: 20, + marginBottom: 20 + }, + messageFromLabel: { + textAlign: 'center', + ...fontStyles.bold, + fontSize: 16 + }, + scrollView: { + ...baseStyles.flexGrow + } +}); + +/** + * Component that supports eth_signTypedData and eth_signTypedData_v3 + */ +export default class ExpandedMessage extends PureComponent { + static propTypes = { + /** + * Object containing current page title and url + */ + currentPageInformation: PropTypes.object, + /** + * Renders the message based on its type (parent) + */ + renderMessage: PropTypes.func, + /** + * Expands the message box on press. + */ + toggleExpandedMessage: PropTypes.func + }; + + render() { + const { currentPageInformation, renderMessage, toggleExpandedMessage } = this.props; + const url = currentPageInformation.url; + const title = getHost(url); + return ( + + + + {strings('signature_request.message')} + + + + + + {strings('signature_request.message_from')} {title} + + + + + {renderMessage()} + + + + ); + } +} diff --git a/app/components/UI/SignatureRequest/ExpandedMessage/index.test.js b/app/components/UI/SignatureRequest/ExpandedMessage/index.test.js new file mode 100644 index 00000000000..88845de0fe6 --- /dev/null +++ b/app/components/UI/SignatureRequest/ExpandedMessage/index.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ExpandedMessage from './'; + +const renderMessageMock = jest.fn(); +const toggleExpandedMessageMock = jest.fn(); + +describe('ExpandedMessage', () => { + it('should render correctly', () => { + const wrapper = shallow( + + ); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap b/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap index 122e4b0cae2..4ee360bc0da 100644 --- a/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap +++ b/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap @@ -3,220 +3,137 @@ exports[`SignatureRequest should render correctly 1`] = ` - + + + + Sign this message? + + + + + + - - Account: - + - - - - - - Account 1 - - + Message + : + - - - - Balance: - - - 0 - - ETH - - - - - - - title - - - url - - - - - You are signing: - - - - - - + /> + diff --git a/app/components/UI/SignatureRequest/index.js b/app/components/UI/SignatureRequest/index.js index 490bb34268d..a00857beabc 100644 --- a/app/components/UI/SignatureRequest/index.js +++ b/app/components/UI/SignatureRequest/index.js @@ -1,56 +1,47 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'; -import { colors, fontStyles, baseStyles } from '../../../styles/common'; +import { colors, fontStyles } from '../../../styles/common'; +import { getHost } from '../../../util/browser'; import { strings } from '../../../../locales/i18n'; import { connect } from 'react-redux'; -import ActionView from '../ActionView'; -import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; -import { renderFromWei } from '../../../util/number'; -import Identicon from '../Identicon'; +import Ionicons from 'react-native-vector-icons/Ionicons'; import WebsiteIcon from '../WebsiteIcon'; -import { renderAccountName } from '../../../util/address'; +import ActionView from '../ActionView'; +import AccountInfoCard from '../AccountInfoCard'; +import TransactionHeader from '../TransactionHeader'; +import WarningMessage from '../../Views/SendFlow/WarningMessage'; +import Device from '../../../util/Device'; import Analytics from '../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; -import { getHost } from '../../../util/browser'; const styles = StyleSheet.create({ - wrapper: { + root: { backgroundColor: colors.white, - flex: 1 + minHeight: Device.isIos() ? '70%' : '80%', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + paddingBottom: Device.isIphoneX() ? 20 : 0 }, - text: { - ...fontStyles.normal, - fontSize: 16, - padding: 5 - }, - accountInformation: { - flex: 1, - flexDirection: 'row', - justifyContent: 'space-between', - margin: 20, - marginBottom: 40 + expandedHeight2: { + minHeight: '80%' }, - accountInfoCol: { - flex: 1, - height: 40 + expandedHeight1: { + minHeight: '75%' }, signingInformation: { - margin: 10 + alignItems: 'center', + marginBottom: 20 }, - account: { - flex: 1, - flexDirection: 'row' + domainLogo: { + width: 40, + height: 40, + marginRight: 8, + borderRadius: 20 }, - identicon: { - padding: 5 - }, - warningText: { - ...fontStyles.normal, - color: colors.red, - textAlign: 'center', - paddingTop: 10, - paddingHorizontal: 10 + messageColumn: { + width: '75%', + justifyContent: 'space-between' }, warningLink: { ...fontStyles.normal, @@ -61,44 +52,48 @@ const styles = StyleSheet.create({ textDecorationLine: 'underline' }, signText: { - ...fontStyles.normal, - fontSize: 16, - padding: 5, + ...fontStyles.bold, + fontSize: 20, textAlign: 'center' }, - domainText: { - ...fontStyles.normal, - textAlign: 'center', - fontSize: 12, - padding: 5, - color: colors.black - }, - domainTitle: { + messageLabelText: { ...fontStyles.bold, - textAlign: 'center', fontSize: 16, - padding: 5, - color: colors.black + marginBottom: 4 }, - children: { - flex: 1, - borderTopColor: colors.grey200, - borderTopWidth: 1, - height: '100%' + readMore: { + color: colors.blue, + fontSize: 14, + ...fontStyles.bold }, - domainLogo: { - marginTop: 15, - width: 64, - height: 64, - borderRadius: 32 + warningWrapper: { + width: '100%', + paddingHorizontal: 24, + paddingTop: 24 }, - assetLogo: { - alignItems: 'center', - justifyContent: 'center', - borderRadius: 10 + actionViewChild: { + paddingHorizontal: 24 + }, + accountInfoCardWrapper: { + marginBottom: 20, + width: '100%' + }, + children: { + alignSelf: 'center', + flexDirection: 'row', + justifyContent: 'flex-start', + width: '100%', + borderWidth: 1, + borderColor: colors.grey200, + borderRadius: 10, + padding: 16 + }, + arrowIconWrapper: { + flexGrow: 1, + alignItems: 'flex-end' }, - domainWrapper: { - margin: 10 + arrowIcon: { + color: colors.grey200 } }); @@ -111,14 +106,6 @@ class SignatureRequest extends PureComponent { * Object representing the navigator */ navigation: PropTypes.object, - /** - * Map of accounts to information objects including balances - */ - accounts: PropTypes.object, - /** - * List of accounts from the PreferencesController - */ - identities: PropTypes.object, /** * Callback triggered when this message signature is rejected */ @@ -127,18 +114,10 @@ class SignatureRequest extends PureComponent { * Callback triggered when this message signature is approved */ onConfirm: PropTypes.func, - /** - * A string that represents the selected address - */ - selectedAddress: PropTypes.string, /** * Content to display above the action buttons */ children: PropTypes.node, - /** - * Object containing domain information for the signature request for EIP712 - */ - domain: PropTypes.object, /** * Object containing current page title and url */ @@ -148,31 +127,21 @@ class SignatureRequest extends PureComponent { */ type: PropTypes.string, /** - * String representing the selected the selected network + * String representing the selected network */ networkType: PropTypes.string, /** * Whether it should display the warning message */ - showWarning: PropTypes.bool - }; - - renderPageInformation = () => { - const { - domain, - currentPageInformation: { url }, - currentPageInformation - } = this.props; - const title = typeof currentPageInformation.title === 'string' ? currentPageInformation.title : getHost(url); - const name = domain && typeof domain.name === 'string'; - return ( - - - {title} - {url} - {!!name && {name}} - - ); + showWarning: PropTypes.bool, + /** + * Whether it should render the expand arrow icon + */ + truncateMessage: PropTypes.bool, + /** + * Expands the message box on press. + */ + toggleExpandedMessage: PropTypes.func }; /** @@ -218,52 +187,63 @@ class SignatureRequest extends PureComponent { }); }; - showWarning = () => ( - - - {strings('signature_request.eth_sign_warning')} - {` `} - {strings('signature_request.learn_more')} - - + renderWarning = () => ( + + {strings('signature_request.eth_sign_warning')} + {` `} + {strings('signature_request.learn_more')} + ); - render() { - const { children, showWarning, accounts, selectedAddress, identities } = this.props; - const balance = renderFromWei(accounts[selectedAddress].balance); - const accountLabel = renderAccountName(selectedAddress, identities); + renderActionViewChildren = () => { + const { children, currentPageInformation, truncateMessage, toggleExpandedMessage } = this.props; + const url = currentPageInformation.url; + const title = getHost(url); + const arrowIcon = truncateMessage ? this.renderArrowIcon() : null; return ( - - - - - {strings('signature_request.account_title')} - - - - - - - {accountLabel} - - - - - - {strings('signature_request.balance_title')} - - {balance} {strings('unit.eth')} - - - - {this.renderPageInformation()} - - {showWarning ? ( - this.showWarning() - ) : ( - {strings('signature_request.signing')} - )} + + + + + + + + {strings('signature_request.message')}: + {children} + {truncateMessage ? ( + {strings('signature_request.read_more')} + ) : null} + {arrowIcon} + + + ); + }; + + renderArrowIcon = () => ( + + + + ); + + render() { + const { showWarning, currentPageInformation, type } = this.props; + let expandedHeight; + if (Device.isMediumDevice()) { + expandedHeight = styles.expandedHeight2; + } else if (type === 'ethSign' && Device.isMediumDevice()) { + expandedHeight = styles.expandedHeight1; + } + return ( + + + + {strings('signature_request.signing')} + {showWarning ? ( + + + + ) : null} - - {children} - + {this.renderActionViewChildren()} ); @@ -283,9 +262,6 @@ class SignatureRequest extends PureComponent { } const mapStateToProps = state => ({ - accounts: state.engine.backgroundState.AccountTrackerController.accounts, - selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, - identities: state.engine.backgroundState.PreferencesController.identities, networkType: state.engine.backgroundState.NetworkController.provider.type }); diff --git a/app/components/UI/StyledButton/styledButtonStyles.js b/app/components/UI/StyledButton/styledButtonStyles.js index d7a5fbace69..6d47fc4425f 100644 --- a/app/components/UI/StyledButton/styledButtonStyles.js +++ b/app/components/UI/StyledButton/styledButtonStyles.js @@ -65,6 +65,14 @@ const styles = StyleSheet.create({ cancelText: { color: colors.grey400 }, + signingCancel: { + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.blue + }, + signingCancelText: { + color: colors.blue + }, warning: { backgroundColor: colors.red }, @@ -84,6 +92,14 @@ const styles = StyleSheet.create({ neutralText: { color: colors.grey500 }, + sign: { + backgroundColor: colors.blue, + borderWidth: 1, + borderColor: colors.blue + }, + signText: { + color: colors.white + }, danger: { backgroundColor: colors.red, borderColor: colors.red, @@ -122,6 +138,10 @@ function getStyles(type) { fontStyle = styles.cancelText; containerStyle = styles.cancel; break; + case 'signingCancel': + fontStyle = styles.signingCancelText; + containerStyle = styles.signingCancel; + break; case 'transparent': fontStyle = styles.whiteText; containerStyle = styles.transparent; @@ -150,6 +170,10 @@ function getStyles(type) { fontStyle = styles.confirmText; containerStyle = styles.danger; break; + case 'sign': + fontStyle = styles.signText; + containerStyle = styles.sign; + break; default: throw new Error('Unknown button type'); } diff --git a/app/components/UI/TransactionActionModal/TransactionActionContent/index.js b/app/components/UI/TransactionActionModal/TransactionActionContent/index.js new file mode 100644 index 00000000000..3cc0ae7b504 --- /dev/null +++ b/app/components/UI/TransactionActionModal/TransactionActionContent/index.js @@ -0,0 +1,105 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, Text } from 'react-native'; +import { colors, fontStyles } from '../../../../styles/common'; +import { strings } from '../../../../../locales/i18n'; + +const styles = StyleSheet.create({ + modalView: { + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + marginHorizontal: 24 + }, + feeWrapper: { + backgroundColor: colors.grey000, + textAlign: 'center', + padding: 16, + borderRadius: 8 + }, + fee: { + ...fontStyles.bold, + fontSize: 24, + textAlign: 'center' + }, + modalText: { + ...fontStyles.normal, + fontSize: 14, + textAlign: 'center', + paddingVertical: 8 + }, + modalTitle: { + ...fontStyles.bold, + fontSize: 22, + textAlign: 'center' + }, + gasTitle: { + ...fontStyles.bold, + fontSize: 16, + textAlign: 'center', + marginVertical: 8 + }, + warningText: { + ...fontStyles.normal, + fontSize: 12, + color: colors.red, + paddingVertical: 8, + textAlign: 'center' + } +}); + +/** + * View that renders a modal to be used for speed up or cancel transaction modal + */ +export default function TransactionActionContent({ + confirmDisabled, + feeText, + titleText, + gasTitleText, + descriptionText +}) { + return ( + + {titleText} + {gasTitleText} + + {feeText} + + {descriptionText} + {confirmDisabled && {strings('transaction.insufficient')}} + + ); +} + +TransactionActionContent.defaultProps = { + cancelButtonMode: 'neutral', + confirmButtonMode: 'warning', + cancelText: strings('action_view.cancel'), + confirmText: strings('action_view.confirm'), + confirmDisabled: false, + displayCancelButton: true, + displayConfirmButton: true +}; + +TransactionActionContent.propTypes = { + /** + * Whether confirm button is disabled + */ + confirmDisabled: PropTypes.bool, + /** + * Text to show as fee + */ + feeText: PropTypes.string, + /** + * Text to show as tit;e + */ + titleText: PropTypes.string, + /** + * Text to show as title of gas section + */ + gasTitleText: PropTypes.string, + /** + * Text to show as description + */ + descriptionText: PropTypes.string +}; diff --git a/app/components/UI/TransactionActionModal/index.js b/app/components/UI/TransactionActionModal/index.js new file mode 100644 index 00000000000..f566a7522f9 --- /dev/null +++ b/app/components/UI/TransactionActionModal/index.js @@ -0,0 +1,103 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { strings } from '../../../../locales/i18n'; +import ActionModal from '../ActionModal'; +import TransactionActionContent from './TransactionActionContent'; + +/** + * View that renders a modal to be used for speed up or cancel transaction modal + */ +export default function TransactionActionModal({ + isVisible, + confirmDisabled, + onCancelPress, + onConfirmPress, + confirmText, + cancelText, + feeText, + titleText, + gasTitleText, + descriptionText, + cancelButtonMode, + confirmButtonMode +}) { + return ( + + + + ); +} + +TransactionActionModal.defaultProps = { + cancelButtonMode: 'neutral', + confirmButtonMode: 'warning', + cancelText: strings('action_view.cancel'), + confirmText: strings('action_view.confirm'), + confirmDisabled: false, + displayCancelButton: true, + displayConfirmButton: true +}; + +TransactionActionModal.propTypes = { + isVisible: PropTypes.bool, + /** + * Text to show in the cancel button + */ + cancelText: PropTypes.string, + /** + * Whether confirm button is disabled + */ + confirmDisabled: PropTypes.bool, + /** + * Text to show in the confirm button + */ + confirmText: PropTypes.string, + /** + * Called when the cancel button is clicked + */ + onCancelPress: PropTypes.func, + /** + * Called when the confirm button is clicked + */ + onConfirmPress: PropTypes.func, + /** + * Cancel button enabled or disabled + */ + cancelButtonMode: PropTypes.string, + /** + * Confirm button enabled or disabled + */ + confirmButtonMode: PropTypes.string, + /** + * Text to show as fee + */ + feeText: PropTypes.string, + /** + * Text to show as tit;e + */ + titleText: PropTypes.string, + /** + * Text to show as title of gas section + */ + gasTitleText: PropTypes.string, + /** + * Text to show as description + */ + descriptionText: PropTypes.string +}; diff --git a/app/components/UI/TransactionEdit/index.js b/app/components/UI/TransactionEdit/index.js index 21df18fec6c..bba68f392f2 100644 --- a/app/components/UI/TransactionEdit/index.js +++ b/app/components/UI/TransactionEdit/index.js @@ -14,6 +14,7 @@ import { addHexPrefix } from 'ethereumjs-util'; import { getTransactionOptionsTitle } from '../../UI/Navbar'; import PaymentChannelsClient from '../../../core/PaymentChannelsClient'; import Device from '../../../util/Device'; +import { getNormalizedTxState } from '../../../util/transactions'; const styles = StyleSheet.create({ root: { @@ -468,7 +469,7 @@ const mapStateToProps = state => ({ accounts: state.engine.backgroundState.AccountTrackerController.accounts, contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances, showHexData: state.settings.showHexData, - transaction: state.transaction + transaction: getNormalizedTxState(state) }); export default connect(mapStateToProps)(TransactionEdit); diff --git a/app/components/UI/TransactionEditor/index.js b/app/components/UI/TransactionEditor/index.js index f6a652564d1..41130974e26 100644 --- a/app/components/UI/TransactionEditor/index.js +++ b/app/components/UI/TransactionEditor/index.js @@ -2,13 +2,14 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { StyleSheet, View } from 'react-native'; import { colors } from '../../../styles/common'; +import ConfirmSend from '../../Views/SendFlow/Confirm'; import TransactionReview from '../TransactionReview'; import TransactionEdit from '../TransactionEdit'; import { isBN, hexToBN, toBN, isDecimal } from '../../../util/number'; import { isValidAddress, toChecksumAddress, BN } from 'ethereumjs-util'; import { strings } from '../../../../locales/i18n'; import { connect } from 'react-redux'; -import { generateTransferData } from '../../../util/transactions'; +import { generateTransferData, getNormalizedTxState } from '../../../util/transactions'; import { setTransactionObject } from '../../../actions/transaction'; import Engine from '../../../core/Engine'; import collectiblesTransferInformation from '../../../util/collectibles-transfer'; @@ -569,10 +570,12 @@ class TransactionEditor extends PureComponent { }; render = () => { - const { mode, transactionConfirmed } = this.props; + const { mode, transactionConfirmed, transaction } = this.props; + return ( - {mode === EDIT && ( + {mode === EDIT && transaction.paymentChannelTransaction && } + {mode === EDIT && !transaction.paymentChannelTransaction && ( ({ networkType: state.engine.backgroundState.NetworkController.provider.type, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, tokens: state.engine.backgroundState.AssetsController.tokens, - transaction: state.transaction + transaction: getNormalizedTxState(state) }); const mapDispatchToProps = dispatch => ({ diff --git a/app/components/UI/TransactionEditor/index.test.js b/app/components/UI/TransactionEditor/index.test.js index 7f3e4eb880b..29f6f5866e2 100644 --- a/app/components/UI/TransactionEditor/index.test.js +++ b/app/components/UI/TransactionEditor/index.test.js @@ -30,7 +30,8 @@ describe('TransactionEditor', () => { } } } - } + }, + transaction: {} }; const wrapper = shallow( diff --git a/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap b/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap index c4485d565cd..69ce5a159dd 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap @@ -4,44 +4,31 @@ exports[`TransactionDetails should render correctly 1`] = ` - - - Hash - + "paddingVertical": 16, + }, + Object { + "flexDirection": "row", + }, + Object { + "borderBottomColor": "#d6d9dc", + "borderBottomWidth": 1, + }, + ] + } + > - + - 0x2 ... 0x2 - - + Status + + + Confirmed + + + - - + + Date + + + [missing "en.date.months.NaN" translation] NaN at NaN:NaNam + + - - From - - - - - - - - - To - - - - - - - - Details - - - - Amount - - + - 2 TKN - - - - + From + + - Gas Limit (Units) - - + + - 21000 - - - - - Gas Price (GWEI) - - + To + + - 2 - + } + } + type="short" + /> + - + - - Total - - - 2 TKN / 0.001 ETH - - + "marginVertical": 8, + }, + Object { + "marginVertical": 24, + }, + ] + } + > + `; diff --git a/app/components/UI/TransactionElement/TransactionDetails/index.js b/app/components/UI/TransactionElement/TransactionDetails/index.js index d6330030456..64f1420fe12 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/index.js +++ b/app/components/UI/TransactionElement/TransactionDetails/index.js @@ -1,86 +1,88 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { Clipboard, TouchableOpacity, StyleSheet, Text, View } from 'react-native'; -import { colors, fontStyles } from '../../../../styles/common'; +import { TouchableOpacity, StyleSheet, Text, View } from 'react-native'; +import { colors, fontStyles, baseStyles } from '../../../../styles/common'; import { strings } from '../../../../../locales/i18n'; -import Icon from 'react-native-vector-icons/FontAwesome'; -import { getNetworkTypeById, findBlockExplorerForRpc, getBlockExplorerName } from '../../../../util/networks'; +import NetworkList, { + getNetworkTypeById, + findBlockExplorerForRpc, + getBlockExplorerName +} from '../../../../util/networks'; import { getEtherscanTransactionUrl, getEtherscanBaseUrl } from '../../../../util/etherscan'; import Logger from '../../../../util/Logger'; import { connect } from 'react-redux'; import URL from 'url-parse'; -import Device from '../../../../util/Device'; import EthereumAddress from '../../EthereumAddress'; - -const HASH_LENGTH = Device.isSmallDevice() ? 18 : 20; +import TransactionSummary from '../../../Views/TransactionSummary'; +import { toDateFormat } from '../../../../util/date'; +import StyledButton from '../../StyledButton'; +import { safeToChecksumAddress } from '../../../../util/address'; +import AppConstants from '../../../../core/AppConstants'; const styles = StyleSheet.create({ detailRowWrapper: { - flex: 1, - backgroundColor: colors.grey000, - paddingVertical: 10, - paddingHorizontal: 15, - marginTop: 10 + paddingHorizontal: 15 }, detailRowTitle: { - flex: 1, - paddingVertical: 10, - fontSize: 15, - color: colors.fontPrimary, + fontSize: 10, + color: colors.grey500, + marginBottom: 8, ...fontStyles.normal }, - detailRowInfo: { - borderRadius: 5, - shadowColor: colors.grey400, - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.5, - shadowRadius: 3, - backgroundColor: colors.white, - padding: 10, - marginBottom: 5 + flexRow: { + flexDirection: 'row' + }, + section: { + paddingVertical: 16 + }, + sectionBorderBottom: { + borderBottomColor: colors.grey100, + borderBottomWidth: 1 }, - detailRowInfoItem: { + flexEnd: { flex: 1, - flexDirection: 'row', - borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: colors.grey100, - marginBottom: 10, - paddingBottom: 5 + alignItems: 'flex-end' }, - noBorderBottom: { - borderBottomWidth: 0 + textUppercase: { + textTransform: 'uppercase' }, detailRowText: { - flex: 1, fontSize: 12, - color: colors.fontSecondary, + color: colors.fontPrimary, ...fontStyles.normal }, - alignLeft: { - textAlign: 'left', - width: '40%' - }, - alignRight: { - textAlign: 'right', - width: '60%' - }, viewOnEtherscan: { - fontSize: 14, + fontSize: 16, color: colors.blue, ...fontStyles.normal, - textAlign: 'center', - marginTop: 15, - marginBottom: 10, - textTransform: 'uppercase' + textAlign: 'center' }, - hash: { - fontSize: 12 + touchableViewOnEtherscan: { + marginVertical: 24 }, - singleRow: { - flexDirection: 'row' + summaryWrapper: { + marginVertical: 8 + }, + statusText: { + fontSize: 12, + ...fontStyles.normal + }, + actionContainerStyle: { + height: 25, + width: 70, + padding: 0 }, - copyIcon: { - paddingRight: 5 + speedupActionContainerStyle: { + marginRight: 10 + }, + actionStyle: { + fontSize: 10, + padding: 0, + paddingHorizontal: 10 + }, + transactionActionsContainer: { + flexDirection: 'row', + paddingTop: 10 } }); @@ -104,26 +106,28 @@ class TransactionDetails extends PureComponent { */ transactionObject: PropTypes.object, /** - * Boolean to determine if this network supports a block explorer + * Object with information to render */ - blockExplorer: PropTypes.bool, + transactionDetails: PropTypes.object, /** - * Action that shows the global alert + * Frequent RPC list from PreferencesController */ - showAlert: PropTypes.func, + frequentRpcList: PropTypes.array, /** - * Object with information to render + * Callback to close the view */ - transactionDetails: PropTypes.object, + close: PropTypes.func, /** - * Frequent RPC list from PreferencesController + * A string representing the network name */ - frequentRpcList: PropTypes.array + providerType: PropTypes.string, + showSpeedUpModal: PropTypes.func, + showCancelModal: PropTypes.func }; state = { - cancelIsOpen: false, - rpcBlockExplorer: undefined + rpcBlockExplorer: undefined, + renderTxActions: true }; componentDidMount = () => { @@ -140,77 +144,14 @@ class TransactionDetails extends PureComponent { this.setState({ rpcBlockExplorer: blockExplorer }); }; - renderTxHash = transactionHash => { - if (!transactionHash) return null; - return ( - - {strings('transactions.hash')} - - {`${transactionHash.substr( - 0, - HASH_LENGTH - )} ... ${transactionHash.substr(-HASH_LENGTH)}`} - {this.renderCopyIcon()} - - - ); - }; - - copy = async () => { - await Clipboard.setString(this.props.transactionDetails.transactionHash); - this.props.showAlert({ - isVisible: true, - autodismiss: 1500, - content: 'clipboard-alert', - data: { msg: strings('transactions.hash_copied_to_clipboard') } - }); - }; - - copyFrom = async () => { - await Clipboard.setString(this.props.transactionDetails.renderFrom); - this.props.showAlert({ - isVisible: true, - autodismiss: 1500, - content: 'clipboard-alert', - data: { msg: strings('transactions.address_copied_to_clipboard') } - }); - }; - - copyTo = async () => { - await Clipboard.setString(this.props.transactionDetails.renderTo); - this.props.showAlert({ - isVisible: true, - autodismiss: 1500, - content: 'clipboard-alert', - data: { msg: strings('transactions.address_copied_to_clipboard') } - }); - }; - - renderCopyIcon = () => ( - - - - ); - - renderCopyToIcon = () => ( - - - - ); - - renderCopyFromIcon = () => ( - - - - ); - viewOnEtherscan = () => { const { transactionObject: { networkID }, transactionDetails: { transactionHash }, network: { provider: { type } - } + }, + close } = this.props; const { rpcBlockExplorer } = this.state; try { @@ -230,83 +171,125 @@ class TransactionDetails extends PureComponent { title: etherscan_url }); } + close && close(); } catch (e) { // eslint-disable-next-line no-console Logger.error(e, { message: `can't get a block explorer link for network `, networkID }); } }; - showCancelModal = () => { - this.setState({ cancelIsOpen: true }); + renderStatusText = status => { + status = status && status.charAt(0).toUpperCase() + status.slice(1); + switch (status) { + case 'Confirmed': + return {status}; + case 'Pending': + case 'Submitted': + return {status}; + case 'Failed': + case 'Cancelled': + return {status}; + } }; - hideCancelModal = () => { - this.setState({ cancelIsOpen: false }); - }; + renderSpeedUpButton = () => ( + + {strings('transaction.speedup')} + + ); + + renderCancelButton = () => ( + + {strings('transaction.cancel')} + + ); render = () => { - const { blockExplorer, transactionObject } = this.props; + const { + transactionObject, + transactionObject: { + status, + time, + transaction: { nonce, to } + }, + providerType + } = this.props; + const networkId = NetworkList[providerType].networkId; + const renderTxActions = status === 'submitted' || status === 'approved'; + const renderSpeedUpAction = safeToChecksumAddress(to) !== AppConstants.CONNEXT.CONTRACTS[networkId]; const { rpcBlockExplorer } = this.state; return ( - {this.renderTxHash(this.props.transactionDetails.transactionHash)} - {strings('transactions.from')} - - - {this.renderCopyFromIcon()} - - {strings('transactions.to')} - - - {this.renderCopyToIcon()} - - {strings('transactions.details')} - - - - {this.props.transactionDetails.valueLabel || strings('transactions.amount')} - - - {this.props.transactionDetails.renderValue} - - - - - {strings('transactions.gas_limit')} - - - {this.props.transactionDetails.renderGas} - + + + + {strings('transactions.status')} + {this.renderStatusText(status)} + {!!renderTxActions && ( + + {renderSpeedUpAction && this.renderSpeedUpButton()} + {this.renderCancelButton()} + + )} + + + {strings('transactions.date')} + {toDateFormat(time)} + - - - {strings('transactions.gas_price')} - - - {this.props.transactionDetails.renderGasPrice} - + + + + + {strings('transactions.from')} + + + + {strings('transactions.to')} + + - - {strings('transactions.total')} - - {this.props.transactionDetails.renderTotalValue} + + {!!nonce && ( + + + {strings('transactions.nonce')} + {`#${parseInt(nonce.replace(/^#/, ''), 16)}`} - {this.props.transactionDetails.renderTotalValueFiat ? ( - - - {this.props.transactionDetails.renderTotalValueFiat} - - - ) : null} + )} + + + {this.props.transactionDetails.transactionHash && transactionObject.status !== 'cancelled' && - blockExplorer && rpcBlockExplorer !== NO_RPC_BLOCK_EXPLORER && ( - + {(rpcBlockExplorer && `${strings('transactions.view_on')} ${getBlockExplorerName(rpcBlockExplorer)}`) || @@ -321,6 +304,7 @@ class TransactionDetails extends PureComponent { const mapStateToProps = state => ({ network: state.engine.backgroundState.NetworkController, - frequentRpcList: state.engine.backgroundState.PreferencesController.frequentRpcList + frequentRpcList: state.engine.backgroundState.PreferencesController.frequentRpcList, + providerType: state.engine.backgroundState.NetworkController.provider.type }); export default connect(mapStateToProps)(TransactionDetails); diff --git a/app/components/UI/TransactionElement/TransactionDetails/index.test.js b/app/components/UI/TransactionElement/TransactionDetails/index.test.js index 250c4770bb4..7de7b36a91e 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/index.test.js +++ b/app/components/UI/TransactionElement/TransactionDetails/index.test.js @@ -20,7 +20,7 @@ describe('TransactionDetails', () => { NetworkController: { provider: { rpcTarget: '', - type: '' + type: 'rpc' } } } @@ -30,7 +30,11 @@ describe('TransactionDetails', () => { const wrapper = shallow( - - -`; diff --git a/app/components/UI/TransactionElement/TransferElement/index.js b/app/components/UI/TransactionElement/TransferElement/index.js deleted file mode 100644 index 057bc76c23b..00000000000 --- a/app/components/UI/TransactionElement/TransferElement/index.js +++ /dev/null @@ -1,269 +0,0 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { TouchableOpacity, StyleSheet, View } from 'react-native'; -import { colors } from '../../../../styles/common'; -import { strings } from '../../../../../locales/i18n'; -import { - renderFromWei, - hexToBN, - renderFromTokenMinimalUnit, - fromTokenMinimalUnit, - balanceToFiat, - toBN, - isBN, - balanceToFiatNumber, - renderToGwei, - weiToFiatNumber -} from '../../../../util/number'; -import { getActionKey, decodeTransferData, isCollectibleAddress } from '../../../../util/transactions'; -import { renderFullAddress, safeToChecksumAddress } from '../../../../util/address'; - -const styles = StyleSheet.create({ - row: { - backgroundColor: colors.white, - flex: 1, - borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: colors.grey100 - }, - rowContent: { - padding: 0 - } -}); - -/** - * View that renders a transfer transaction item, part of transactions list - */ -export default class TransferElement extends PureComponent { - static propTypes = { - /** - * Transaction object - */ - tx: PropTypes.object, - /** - * Object containing token exchange rates in the format address => exchangeRate - */ - contractExchangeRates: PropTypes.object, - /** - * ETH to current currency conversion rate - */ - conversionRate: PropTypes.number, - /** - * Currency code of the currently-active currency - */ - currentCurrency: PropTypes.string, - /** - * Callback function that will adjust the scroll - * position once the transaction detail is visible - */ - selected: PropTypes.bool, - /** - * String of selected address - */ - selectedAddress: PropTypes.string, - /** - * Callback to render transaction details view - */ - onPressItem: PropTypes.func, - /** - * An array that represents the user tokens - */ - tokens: PropTypes.object, - /** - * Current element of the list index - */ - i: PropTypes.number, - /** - * Callback to render corresponding transaction element - */ - renderTxElement: PropTypes.func, - /** - * Callback to render corresponding transaction element - */ - renderTxDetails: PropTypes.func, - /** - * An array that represents the user collectible contracts - */ - collectibleContracts: PropTypes.array - }; - - state = { - actionKey: undefined, - addressTo: '', - encodedAmount: '', - isCollectible: false - }; - - mounted = false; - - componentDidMount = async () => { - this.mounted = true; - const { - tx, - tx: { - transaction: { data, to } - }, - selectedAddress - } = this.props; - const actionKey = await getActionKey(tx, selectedAddress); - const [addressTo, encodedAmount] = decodeTransferData('transfer', data); - const isCollectible = await isCollectibleAddress(to, encodedAmount); - this.mounted && this.setState({ actionKey, addressTo, encodedAmount, isCollectible }); - }; - - componentWillUnmount() { - this.mounted = false; - } - - onPressItem = () => { - const { tx, i, onPressItem } = this.props; - onPressItem(tx.id, i); - }; - - getTokenTransfer = totalGas => { - const { - tx: { - transaction: { to } - }, - conversionRate, - currentCurrency, - tokens, - contractExchangeRates - } = this.props; - - const { actionKey, encodedAmount } = this.state; - - const amount = toBN(encodedAmount); - - const userHasToken = safeToChecksumAddress(to) in tokens; - const token = userHasToken ? tokens[safeToChecksumAddress(to)] : null; - const renderActionKey = token ? strings('transactions.sent') + ' ' + token.symbol : actionKey; - const renderTokenAmount = token - ? renderFromTokenMinimalUnit(amount, token.decimals) + ' ' + token.symbol - : undefined; - const exchangeRate = token ? contractExchangeRates[token.address] : undefined; - let renderTokenFiatAmount, renderTokenFiatNumber; - if (exchangeRate) { - renderTokenFiatAmount = - '- ' + - balanceToFiat( - fromTokenMinimalUnit(amount, token.decimals) || 0, - conversionRate, - exchangeRate, - currentCurrency - ).toUpperCase(); - renderTokenFiatNumber = balanceToFiatNumber( - fromTokenMinimalUnit(amount, token.decimals) || 0, - conversionRate, - exchangeRate - ); - } - - const renderToken = token - ? renderFromTokenMinimalUnit(amount, token.decimals) + ' ' + token.symbol - : strings('transaction.value_not_available'); - const totalFiatNumber = renderTokenFiatNumber - ? weiToFiatNumber(totalGas, conversionRate) + renderTokenFiatNumber - : undefined; - - const transactionDetails = { - renderValue: renderToken, - renderTotalValue: `${renderToken} ${strings('unit.divisor')} ${renderFromWei(totalGas)} ${strings( - 'unit.eth' - )}`, - renderTotalValueFiat: totalFiatNumber ? `${totalFiatNumber} ${currentCurrency}` : undefined - }; - - const transactionElement = { - actionKey: renderActionKey, - value: !renderTokenAmount ? strings('transaction.value_not_available') : renderTokenAmount, - fiatValue: renderTokenFiatAmount - }; - - return [transactionElement, transactionDetails]; - }; - - getCollectibleTransfer = totalGas => { - const { - tx: { - transaction: { to } - }, - collectibleContracts - } = this.props; - const { encodedAmount } = this.state; - let actionKey; - const tokenId = encodedAmount; - const collectible = collectibleContracts.find( - collectible => collectible.address.toLowerCase() === to.toLowerCase() - ); - if (collectible) { - actionKey = strings('transactions.sent') + ' ' + collectible.name; - } else { - actionKey = strings('transactions.sent_collectible'); - } - - const renderCollectible = collectible - ? strings('unit.token_id') + tokenId + ' ' + collectible.symbol - : strings('unit.token_id') + tokenId; - - const transactionDetails = { - renderValue: renderCollectible, - renderTotalValue: - renderCollectible + - ' ' + - strings('unit.divisor') + - ' ' + - renderFromWei(totalGas) + - ' ' + - strings('unit.eth'), - renderTotalValueFiat: undefined - }; - - const transactionElement = { - actionKey, - value: `${strings('unit.token_id')}${tokenId}`, - fiatValue: collectible ? collectible.symbol : undefined - }; - - return [transactionElement, transactionDetails]; - }; - - render = () => { - const { - selected, - tx, - tx: { - transaction: { from, gas, gasPrice }, - transactionHash - } - } = this.props; - const { addressTo } = this.state; - const gasBN = hexToBN(gas); - const gasPriceBN = hexToBN(gasPrice); - const totalGas = isBN(gasBN) && isBN(gasPriceBN) ? gasBN.mul(gasPriceBN) : toBN('0x0'); - const renderGas = parseInt(gas, 16).toString(); - const renderGasPrice = renderToGwei(gasPrice); - - let [transactionElement, transactionDetails] = this.state.isCollectible - ? this.getCollectibleTransfer(totalGas) - : this.getTokenTransfer(totalGas); - transactionElement = { ...transactionElement, renderTo: addressTo }; - transactionDetails = { - ...transactionDetails, - ...{ - renderFrom: renderFullAddress(from), - renderTo: renderFullAddress(addressTo), - transactionHash, - renderGas, - renderGasPrice - } - }; - return ( - - - {this.props.renderTxElement(transactionElement)} - {this.props.renderTxDetails(selected, tx, transactionDetails)} - - - ); - }; -} diff --git a/app/components/UI/TransactionElement/TransferElement/index.test.js b/app/components/UI/TransactionElement/TransferElement/index.test.js deleted file mode 100644 index 2c20f5b0d05..00000000000 --- a/app/components/UI/TransactionElement/TransferElement/index.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import TransferElement from './'; -import configureMockStore from 'redux-mock-store'; -import { shallow } from 'enzyme'; - -const mockStore = configureMockStore(); - -describe('TransferElement', () => { - it('should render correctly', () => { - const initialState = {}; - - const wrapper = shallow( - ''} - // eslint-disable-next-line react/jsx-no-bind - renderTxDetails={() => ''} - />, - { - context: { store: mockStore(initialState) } - } - ); - expect(wrapper.dive()).toMatchSnapshot(); - }); -}); diff --git a/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap b/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap index 8c2257d5f93..27335c29c4c 100644 --- a/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap @@ -1,140 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TransactionElement should render correctly 1`] = ` - - - - - #1 - - Invalid Date Invalid Date - - - - - - - CONFIRMED - - - - - 0 ETH - - - 0 USD - - - - - - -`; +exports[`TransactionElement should render correctly 1`] = ``; diff --git a/app/components/UI/TransactionElement/index.js b/app/components/UI/TransactionElement/index.js index 0592c5f7352..ea40543050b 100644 --- a/app/components/UI/TransactionElement/index.js +++ b/app/components/UI/TransactionElement/index.js @@ -3,22 +3,21 @@ import PropTypes from 'prop-types'; import { TouchableHighlight, StyleSheet, Text, View, Image } from 'react-native'; import { colors, fontStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; -import { toLocaleDateTime } from '../../../util/date'; -import { renderFromWei, weiToFiat, hexToBN, toBN, isBN, renderToGwei, balanceToFiat } from '../../../util/number'; +import { toDateFormat } from '../../../util/date'; import Identicon from '../Identicon'; -import { getActionKey, decodeTransferData, getTicker } from '../../../util/transactions'; import TransactionDetails from './TransactionDetails'; -import { renderFullAddress, safeToChecksumAddress } from '../../../util/address'; +import { safeToChecksumAddress } from '../../../util/address'; import FadeIn from 'react-native-fade-in-image'; import TokenImage from '../TokenImage'; import contractMap from 'eth-contract-metadata'; -import TransferElement from './TransferElement'; import { connect } from 'react-redux'; import AppConstants from '../../../core/AppConstants'; import Ionicons from 'react-native-vector-icons/Ionicons'; import StyledButton from '../StyledButton'; import Networks from '../../../util/networks'; import Device from '../../../util/Device'; +import Modal from 'react-native-modal'; +import decodeTransaction from './utils'; const { CONNEXT: { CONTRACTS } @@ -141,7 +140,36 @@ const styles = StyleSheet.create({ flexDirection: 'row', paddingTop: 10, paddingLeft: 40 - } + }, + modalContainer: { + width: '90%', + backgroundColor: colors.white, + borderRadius: 10 + }, + modal: { + margin: 0, + width: '100%' + }, + modalView: { + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center' + }, + titleWrapper: { + borderBottomWidth: StyleSheet.hairlineWidth, + borderColor: colors.grey100, + flexDirection: 'row' + }, + title: { + flex: 1, + textAlign: 'center', + fontSize: 18, + marginVertical: 12, + marginHorizontal: 24, + color: colors.fontPrimary, + ...fontStyles.bold + }, + closeIcon: { paddingTop: 4, position: 'absolute', right: 16 } }); const ethLogo = require('../../../images/eth-logo.png'); // eslint-disable-line @@ -162,20 +190,18 @@ class TransactionElement extends PureComponent { /** * Object containing token exchange rates in the format address => exchangeRate */ + // eslint-disable-next-line react/no-unused-prop-types contractExchangeRates: PropTypes.object, /** * ETH to current currency conversion rate */ + // eslint-disable-next-line react/no-unused-prop-types conversionRate: PropTypes.number, /** * Currency code of the currently-active currency */ + // eslint-disable-next-line react/no-unused-prop-types currentCurrency: PropTypes.string, - /** - * Callback function that will adjust the scroll - * position once the transaction detail is visible - */ - selected: PropTypes.bool, /** * String of selected address */ @@ -191,26 +217,22 @@ class TransactionElement extends PureComponent { /** * An array that represents the user tokens */ + // eslint-disable-next-line react/no-unused-prop-types tokens: PropTypes.object, /** * An array that represents the user collectible contracts */ + // eslint-disable-next-line react/no-unused-prop-types collectibleContracts: PropTypes.array, - /** - * Boolean to determine if this network supports a block explorer - */ - blockExplorer: PropTypes.bool, - /** - * Action that shows the global alert - */ - showAlert: PropTypes.func, /** * Current provider ticker */ + // eslint-disable-next-line react/no-unused-prop-types ticker: PropTypes.string, /** * Current exchange rate */ + // eslint-disable-next-line react/no-unused-prop-types exchangeRate: PropTypes.number, /** * Callback to speed up tx @@ -223,27 +245,30 @@ class TransactionElement extends PureComponent { /** * A string representing the network name */ - providerType: PropTypes.string + providerType: PropTypes.string, + /** + * Primary currency, either ETH or Fiat + */ + // eslint-disable-next-line react/no-unused-prop-types + primaryCurrency: PropTypes.string }; state = { actionKey: undefined, cancelIsOpen: false, - speedUpIsOpen: false + speedUpIsOpen: false, + detailsModalVisible: false, + transactionGas: { gasBN: undefined, gasPriceBN: undefined, gasTotal: undefined }, + transactionElement: undefined, + transactionDetails: undefined }; mounted = false; componentDidMount = async () => { + const [transactionElement, transactionDetails] = await decodeTransaction(this.props); this.mounted = true; - const { - tx, - tx: { paymentChannelTransaction }, - selectedAddress, - ticker - } = this.props; - const actionKey = tx.actionKey || (await getActionKey(tx, selectedAddress, ticker, paymentChannelTransaction)); - this.mounted && this.setState({ actionKey }); + this.mounted && this.setState({ transactionElement, transactionDetails }); }; componentWillUnmount() { @@ -264,6 +289,11 @@ class TransactionElement extends PureComponent { onPressItem = () => { const { tx, i, onPressItem } = this.props; onPressItem(tx.id, i); + this.setState({ detailsModalVisible: true }); + }; + + onCloseDetailsModal = () => { + this.setState({ detailsModalVisible: false }); }; renderTxTime = () => { @@ -273,24 +303,11 @@ class TransactionElement extends PureComponent { return ( {(!incoming || selfSent) && tx.transaction.nonce && `#${parseInt(tx.transaction.nonce, 16)} - `} - {`${toLocaleDateTime(tx.time)}`} + {`${toDateFormat(tx.time)}`} ); }; - renderTxDetails = (selected, tx, transactionDetails) => { - const { showAlert, blockExplorer } = this.props; - return selected ? ( - - ) : null; - }; - renderTxElementImage = transactionElement => { const { renderTo, @@ -367,11 +384,7 @@ class TransactionElement extends PureComponent { }, providerType } = this.props; - const { renderTo, actionKey, value, fiatValue = false } = transactionElement; - let symbol; - if (renderTo in contractMap) { - symbol = contractMap[renderTo].symbol; - } + const { value, fiatValue = false, actionKey } = transactionElement; const networkId = Networks[providerType].networkId; const renderTxActions = status === 'submitted' || status === 'approved'; const renderSpeedUpAction = safeToChecksumAddress(to) !== AppConstants.CONNEXT.CONTRACTS[networkId]; @@ -382,7 +395,7 @@ class TransactionElement extends PureComponent { {this.renderTxElementImage(transactionElement)} - {symbol ? symbol + ' ' + actionKey : actionKey} + {actionKey} {status} @@ -401,202 +414,9 @@ class TransactionElement extends PureComponent { ); }; - decodeTransferFromTx = () => { - const { - tx: { - transaction: { gas, gasPrice, data, to }, - transactionHash - }, - collectibleContracts - } = this.props; - let { actionKey } = this.state; - const [addressFrom, addressTo, tokenId] = decodeTransferData('transferFrom', data); - const collectible = collectibleContracts.find( - collectible => collectible.address.toLowerCase() === to.toLowerCase() - ); - if (collectible) { - actionKey = strings('transactions.sent') + ' ' + collectible.name; - } - - const gasBN = hexToBN(gas); - const gasPriceBN = hexToBN(gasPrice); - const totalGas = isBN(gasBN) && isBN(gasPriceBN) ? gasBN.mul(gasPriceBN) : toBN('0x0'); - const renderCollectible = collectible - ? strings('unit.token_id') + tokenId + ' ' + collectible.symbol - : strings('unit.token_id') + tokenId; - - const renderFrom = renderFullAddress(addressFrom); - const renderTo = renderFullAddress(addressTo); - - const transactionDetails = { - renderFrom, - renderTo, - transactionHash, - renderValue: renderCollectible, - renderGas: parseInt(gas, 16).toString(), - renderGasPrice: renderToGwei(gasPrice), - renderTotalValue: - renderCollectible + - ' ' + - strings('unit.divisor') + - ' ' + - renderFromWei(totalGas) + - ' ' + - strings('unit.eth'), - renderTotalValueFiat: undefined - }; - - const transactionElement = { - renderTo, - renderFrom, - actionKey, - value: `${strings('unit.token_id')}${tokenId}`, - fiatValue: collectible ? collectible.symbol : undefined - }; - - return [transactionElement, transactionDetails]; - }; - - decodeConfirmTx = () => { - const { - tx: { - transaction: { value, gas, gasPrice, from, to }, - transactionHash - }, - conversionRate, - currentCurrency - } = this.props; - const ticker = getTicker(this.props.ticker); - const { actionKey } = this.state; - const totalEth = hexToBN(value); - const renderTotalEth = renderFromWei(totalEth) + ' ' + ticker; - const renderTotalEthFiat = weiToFiat(totalEth, conversionRate, currentCurrency); - - const gasBN = hexToBN(gas); - const gasPriceBN = hexToBN(gasPrice); - const totalGas = isBN(gasBN) && isBN(gasPriceBN) ? gasBN.mul(gasPriceBN) : toBN('0x0'); - const totalValue = isBN(totalEth) ? totalEth.add(totalGas) : totalGas; - - const renderFrom = renderFullAddress(from); - const renderTo = renderFullAddress(to); - - const transactionDetails = { - renderFrom, - renderTo, - transactionHash, - renderValue: renderFromWei(value) + ' ' + ticker, - renderGas: parseInt(gas, 16).toString(), - renderGasPrice: renderToGwei(gasPrice), - renderTotalValue: renderFromWei(totalValue) + ' ' + ticker, - renderTotalValueFiat: weiToFiat(totalValue, conversionRate, currentCurrency) - }; - - const transactionElement = { - renderTo, - renderFrom, - actionKey, - value: renderTotalEth, - fiatValue: renderTotalEthFiat - }; - - return [transactionElement, transactionDetails]; - }; - - decodeDeploymentTx = () => { - const { - tx: { - transaction: { value, gas, gasPrice, from }, - transactionHash - }, - conversionRate, - currentCurrency - } = this.props; - const ticker = getTicker(this.props.ticker); - const { actionKey } = this.state; - const gasBN = hexToBN(gas); - const gasPriceBN = hexToBN(gasPrice); - const totalGas = isBN(gasBN) && isBN(gasPriceBN) ? gasBN.mul(gasPriceBN) : toBN('0x0'); - - const renderTotalEth = renderFromWei(totalGas) + ' ' + ticker; - const renderTotalEthFiat = weiToFiat(totalGas, conversionRate, currentCurrency); - const totalEth = isBN(value) ? value.add(totalGas) : totalGas; - - const renderFrom = renderFullAddress(from); - const renderTo = strings('transactions.to_contract'); - - const transactionElement = { - renderTo, - renderFrom, - actionKey, - value: renderTotalEth, - fiatValue: renderTotalEthFiat, - contractDeployment: true - }; - const transactionDetails = { - renderFrom, - renderTo, - transactionHash, - renderValue: renderFromWei(value) + ' ' + ticker, - renderGas: parseInt(gas, 16).toString(), - renderGasPrice: renderToGwei(gasPrice), - renderTotalValue: renderFromWei(totalEth) + ' ' + ticker, - renderTotalValueFiat: weiToFiat(totalEth, conversionRate, currentCurrency) - }; - - return [transactionElement, transactionDetails]; - }; - - decodePaymentChannelTx = () => { - const { - tx: { - networkID, - transactionHash, - transaction: { value, gas, gasPrice, from, to } - }, - conversionRate, - currentCurrency, - exchangeRate - } = this.props; - const { actionKey } = this.state; - const contract = CONTRACTS[networkID]; - const isDeposit = contract && to.toLowerCase() === contract.toLowerCase(); - const totalEth = hexToBN(value); - const totalEthFiat = weiToFiat(totalEth, conversionRate, currentCurrency); - const readableTotalEth = renderFromWei(totalEth); - const renderTotalEth = readableTotalEth + ' ' + (isDeposit ? strings('unit.eth') : strings('unit.sai')); - const renderTotalEthFiat = isDeposit - ? totalEthFiat - : balanceToFiat(parseFloat(readableTotalEth), conversionRate, exchangeRate, currentCurrency); - - const renderFrom = renderFullAddress(from); - const renderTo = renderFullAddress(to); - - const transactionDetails = { - renderFrom, - renderTo, - transactionHash, - renderGas: gas ? parseInt(gas, 16).toString() : strings('transactions.tx_details_not_available'), - renderGasPrice: gasPrice ? renderToGwei(gasPrice) : strings('transactions.tx_details_not_available'), - renderValue: renderTotalEth, - renderTotalValue: renderTotalEth, - renderTotalValueFiat: isDeposit && totalEthFiat - }; - - const transactionElement = { - renderFrom, - renderTo, - actionKey, - value: renderTotalEth, - fiatValue: renderTotalEthFiat, - paymentChannelTransaction: true - }; - - return [transactionElement, transactionDetails]; - }; - renderCancelButton = () => ( - ); - } - if (paymentChannelTransaction) { - [transactionElement, transactionDetails] = this.decodePaymentChannelTx(); - } else { - switch (actionKey) { - case strings('transactions.sent_collectible'): - [transactionElement, transactionDetails] = this.decodeTransferFromTx(totalGas); - break; - case strings('transactions.contract_deploy'): - [transactionElement, transactionDetails] = this.decodeDeploymentTx(totalGas); - break; - default: - [transactionElement, transactionDetails] = this.decodeConfirmTx(totalGas); - } - } + const { tx } = this.props; + const { detailsModalVisible, transactionElement, transactionDetails } = this.state; + + if (!transactionElement || !transactionDetails) return ; return ( - - - {this.renderTxElement(transactionElement)} - {this.renderTxDetails(selected, tx, transactionDetails)} - - + + + {this.renderTxElement(transactionElement)} + + + + + + + {transactionElement.actionKey} + + + + + + + + ); } } const mapStateToProps = state => ({ ticker: state.engine.backgroundState.NetworkController.provider.ticker, - providerType: state.engine.backgroundState.NetworkController.provider.type + providerType: state.engine.backgroundState.NetworkController.provider.type, + primaryCurrency: state.settings.primaryCurrency }); export default connect(mapStateToProps)(TransactionElement); diff --git a/app/components/UI/TransactionElement/index.test.js b/app/components/UI/TransactionElement/index.test.js index 096028e73e6..de7ee3ba3d5 100644 --- a/app/components/UI/TransactionElement/index.test.js +++ b/app/components/UI/TransactionElement/index.test.js @@ -21,6 +21,9 @@ describe('TransactionElement', () => { } } } + }, + settings: { + primaryCurrency: 'ETH' } }; diff --git a/app/components/UI/TransactionElement/utils.js b/app/components/UI/TransactionElement/utils.js new file mode 100644 index 00000000000..cf3daeb363c --- /dev/null +++ b/app/components/UI/TransactionElement/utils.js @@ -0,0 +1,505 @@ +import AppConstants from '../../../core/AppConstants'; +import { + hexToBN, + weiToFiat, + renderFromWei, + balanceToFiat, + renderToGwei, + isBN, + renderFromTokenMinimalUnit, + fromTokenMinimalUnit, + balanceToFiatNumber, + weiToFiatNumber, + addCurrencySymbol, + toBN +} from '../../../util/number'; +import { strings } from '../../../../locales/i18n'; +import { renderFullAddress, safeToChecksumAddress } from '../../../util/address'; +import { decodeTransferData, isCollectibleAddress, getTicker, getActionKey } from '../../../util/transactions'; +import contractMap from 'eth-contract-metadata'; + +const { + CONNEXT: { CONTRACTS } +} = AppConstants; + +function decodePaymentChannelTx(args) { + const { + tx: { + networkID, + transaction: { to } + } + } = args; + const contract = CONTRACTS[networkID]; + const isDeposit = contract && to && to.toLowerCase() === contract.toLowerCase(); + if (isDeposit) return decodeConfirmTx(args, true); + return decodeTransferPaymentChannel(args); +} + +function decodeTransferPaymentChannel(args) { + const { + tx: { + transaction: { value, from, to } + }, + conversionRate, + currentCurrency, + exchangeRate, + actionKey, + primaryCurrency + } = args; + const totalSAI = hexToBN(value); + const readableTotalSAI = renderFromWei(totalSAI); + const renderTotalSAI = `${readableTotalSAI} ${strings('unit.sai')}`; + const renderTotalSAIFiat = balanceToFiat(parseFloat(renderTotalSAI), conversionRate, exchangeRate, currentCurrency); + + const renderFrom = renderFullAddress(from); + const renderTo = renderFullAddress(to); + + let transactionDetails = { + renderFrom, + renderTo, + renderValue: renderTotalSAI + }; + + if (primaryCurrency === 'ETH') { + transactionDetails = { + ...transactionDetails, + summaryAmount: renderTotalSAI, + summaryTotalAmount: renderTotalSAI, + summarySecondaryTotalAmount: renderTotalSAIFiat + }; + } else { + transactionDetails = { + ...transactionDetails, + summaryAmount: renderTotalSAIFiat, + summaryTotalAmount: renderTotalSAIFiat, + summarySecondaryTotalAmount: renderTotalSAI + }; + } + + const transactionElement = { + renderFrom, + renderTo, + actionKey, + value: renderTotalSAI, + fiatValue: renderTotalSAIFiat, + paymentChannelTransaction: true + }; + + return [transactionElement, transactionDetails]; +} + +function getTokenTransfer(args) { + const { + tx: { + transaction: { to, data } + }, + conversionRate, + currentCurrency, + tokens, + contractExchangeRates, + totalGas, + actionKey, + primaryCurrency + } = args; + + const [, encodedAmount] = decodeTransferData('transfer', data); + const amount = toBN(encodedAmount); + const userHasToken = safeToChecksumAddress(to) in tokens; + const token = userHasToken ? tokens[safeToChecksumAddress(to)] : null; + const renderActionKey = token ? `${strings('transactions.sent')} ${token.symbol}` : actionKey; + const renderTokenAmount = token + ? `${renderFromTokenMinimalUnit(amount, token.decimals)} ${token.symbol}` + : undefined; + const exchangeRate = token ? contractExchangeRates[token.address] : undefined; + let renderTokenFiatAmount, renderTokenFiatNumber; + if (exchangeRate) { + renderTokenFiatAmount = balanceToFiat( + fromTokenMinimalUnit(amount, token.decimals) || 0, + conversionRate, + exchangeRate, + currentCurrency + ); + renderTokenFiatNumber = balanceToFiatNumber( + fromTokenMinimalUnit(amount, token.decimals) || 0, + conversionRate, + exchangeRate + ); + } + + const renderToken = token + ? `${renderFromTokenMinimalUnit(amount, token.decimals)} ${token.symbol}` + : strings('transaction.value_not_available'); + const totalFiatNumber = renderTokenFiatNumber + ? weiToFiatNumber(totalGas, conversionRate) + renderTokenFiatNumber + : weiToFiatNumber(totalGas, conversionRate); + + const ticker = getTicker(args.ticker); + + let transactionDetails = { + renderTotalGas: `${renderFromWei(totalGas)} ${ticker}`, + renderValue: renderToken + }; + if (primaryCurrency === 'ETH') { + transactionDetails = { + ...transactionDetails, + summaryAmount: renderToken, + summaryFee: `${renderFromWei(totalGas)} ${ticker}`, + summaryTotalAmount: `${renderToken} ${strings('unit.divisor')} ${renderFromWei(totalGas)} ${ticker}`, + summarySecondaryTotalAmount: totalFiatNumber + ? `${addCurrencySymbol(totalFiatNumber, currentCurrency)}` + : undefined + }; + } else { + transactionDetails = { + ...transactionDetails, + summaryAmount: renderTokenFiatAmount + ? `${renderTokenFiatAmount}` + : `${addCurrencySymbol(0, currentCurrency)}`, + summaryFee: weiToFiat(totalGas, conversionRate, currentCurrency), + summaryTotalAmount: totalFiatNumber ? `${addCurrencySymbol(totalFiatNumber, currentCurrency)}` : undefined, + summarySecondaryTotalAmount: `${renderToken} ${strings('unit.divisor')} ${renderFromWei( + totalGas + )} ${ticker}` + }; + } + + const transactionElement = { + actionKey: renderActionKey, + value: !renderTokenAmount ? strings('transaction.value_not_available') : renderTokenAmount, + fiatValue: `- ${renderTokenFiatAmount}` + }; + + return [transactionElement, transactionDetails]; +} + +function getCollectibleTransfer(args) { + const { + tx: { + transaction: { to, data } + }, + collectibleContracts, + totalGas, + conversionRate, + currentCurrency, + primaryCurrency + } = args; + let actionKey; + const [, tokenId] = decodeTransferData('transfer', data); + const ticker = getTicker(args.ticker); + const collectible = collectibleContracts.find( + collectible => collectible.address.toLowerCase() === to.toLowerCase() + ); + if (collectible) { + actionKey = `${strings('transactions.sent')} ${collectible.name}`; + } else { + actionKey = strings('transactions.sent_collectible'); + } + + const renderCollectible = collectible + ? `${strings('unit.token_id')} ${tokenId} ${collectible.symbol}` + : `${strings('unit.token_id')} ${tokenId}`; + + let transactionDetails = { renderValue: renderCollectible }; + + if (primaryCurrency === 'ETH') { + transactionDetails = { + ...transactionDetails, + summaryAmount: renderCollectible, + summaryFee: `${renderFromWei(totalGas)} ${ticker}`, + summaryTotalAmount: `${renderCollectible} ${strings('unit.divisor')} ${renderFromWei(totalGas)} ${strings( + 'unit.eth' + )}`, + summarySecondaryTotalAmount: weiToFiat(totalGas, conversionRate, currentCurrency) + }; + } else { + transactionDetails = { + ...transactionDetails, + summaryAmount: renderCollectible, + summaryFee: weiToFiat(totalGas, conversionRate, currentCurrency), + summaryTotalAmount: weiToFiat(totalGas, conversionRate, currentCurrency), + summarySecondaryTotalAmount: `${renderCollectible} ${strings('unit.divisor')} ${renderFromWei( + totalGas + )} ${strings('unit.eth')}` + }; + } + + const transactionElement = { + actionKey, + value: `${strings('unit.token_id')}${tokenId}`, + fiatValue: collectible ? collectible.symbol : undefined + }; + + return [transactionElement, transactionDetails]; +} + +async function decodeTransferTx(args) { + const { + tx: { + transaction: { from, gas, gasPrice, data, to }, + transactionHash + } + } = args; + + const decodedData = decodeTransferData('transfer', data); + const addressTo = decodedData[0]; + const isCollectible = await isCollectibleAddress(to, decodedData[1]); + + const gasBN = hexToBN(gas); + const gasPriceBN = hexToBN(gasPrice); + const totalGas = isBN(gasBN) && isBN(gasPriceBN) ? gasBN.mul(gasPriceBN) : toBN('0x0'); + const renderGas = parseInt(gas, 16).toString(); + const renderGasPrice = renderToGwei(gasPrice); + + let [transactionElement, transactionDetails] = isCollectible + ? getCollectibleTransfer({ ...args, totalGas }) + : getTokenTransfer({ ...args, totalGas }); + transactionElement = { ...transactionElement, renderTo: addressTo }; + transactionDetails = { + ...transactionDetails, + ...{ + renderFrom: renderFullAddress(from), + renderTo: renderFullAddress(addressTo), + transactionHash, + renderGas, + renderGasPrice + } + }; + return [transactionElement, transactionDetails]; +} + +function decodeTransferFromTx(args) { + const { + tx: { + transaction: { gas, gasPrice, data, to }, + transactionHash + }, + collectibleContracts, + conversionRate, + currentCurrency, + primaryCurrency + } = args; + const [addressFrom, addressTo, tokenId] = decodeTransferData('transferFrom', data); + const collectible = collectibleContracts.find( + collectible => collectible.address.toLowerCase() === to.toLowerCase() + ); + let actionKey = args.actionKey; + if (collectible) { + actionKey = `${strings('transactions.sent')} ${collectible.name}`; + } + + const gasBN = hexToBN(gas); + const gasPriceBN = hexToBN(gasPrice); + const totalGas = isBN(gasBN) && isBN(gasPriceBN) ? gasBN.mul(gasPriceBN) : toBN('0x0'); + const renderCollectible = collectible + ? `${strings('unit.token_id')}${tokenId} ${collectible.symbol}` + : `${strings('unit.token_id')}${tokenId}`; + + const renderFrom = renderFullAddress(addressFrom); + const renderTo = renderFullAddress(addressTo); + const ticker = getTicker(args.ticker); + + let transactionDetails = { + renderFrom, + renderTo, + transactionHash, + renderValue: renderCollectible, + renderGas: parseInt(gas, 16).toString(), + renderGasPrice: renderToGwei(gasPrice), + renderTotalGas: `${renderFromWei(totalGas)} ${ticker}` + }; + + if (primaryCurrency === 'ETH') { + transactionDetails = { + ...transactionDetails, + summaryAmount: renderCollectible, + summaryFee: `${renderFromWei(totalGas)} ${ticker}`, + summarySecondaryTotalAmount: weiToFiat(totalGas, conversionRate, currentCurrency), + summaryTotalAmount: `${renderCollectible} ${strings('unit.divisor')} ${renderFromWei(totalGas)} ${ticker}` + }; + } else { + transactionDetails = { + ...transactionDetails, + summaryAmount: renderCollectible, + summaryFee: weiToFiat(totalGas, conversionRate, currentCurrency), + summarySecondaryTotalAmount: `${renderCollectible} ${strings('unit.divisor')} ${renderFromWei( + totalGas + )} ${ticker}`, + summaryTotalAmount: weiToFiat(totalGas, conversionRate, currentCurrency) + }; + } + + const transactionElement = { + renderTo, + renderFrom, + actionKey, + value: `${strings('unit.token_id')}${tokenId}`, + fiatValue: collectible ? collectible.symbol : undefined + }; + + return [transactionElement, transactionDetails]; +} + +function decodeDeploymentTx(args) { + const { + tx: { + transaction: { value, gas, gasPrice, from }, + transactionHash + }, + conversionRate, + currentCurrency, + actionKey, + primaryCurrency + } = args; + const ticker = getTicker(args.ticker); + const gasBN = hexToBN(gas); + const gasPriceBN = hexToBN(gasPrice); + const totalGas = isBN(gasBN) && isBN(gasPriceBN) ? gasBN.mul(gasPriceBN) : toBN('0x0'); + + const renderTotalEth = `${renderFromWei(totalGas)} ${ticker}`; + const renderTotalEthFiat = weiToFiat(totalGas, conversionRate, currentCurrency); + const totalEth = isBN(value) ? value.add(totalGas) : totalGas; + + const renderFrom = renderFullAddress(from); + const renderTo = strings('transactions.to_contract'); + + const transactionElement = { + renderTo, + renderFrom, + actionKey, + value: renderTotalEth, + fiatValue: renderTotalEthFiat, + contractDeployment: true + }; + let transactionDetails = { + renderFrom, + renderTo, + transactionHash, + renderValue: `${renderFromWei(value)} ${ticker}`, + renderGas: parseInt(gas, 16).toString(), + renderGasPrice: renderToGwei(gasPrice), + renderTotalGas: `${renderFromWei(totalGas)} ${ticker}` + }; + + if (primaryCurrency === 'ETH') { + transactionDetails = { + ...transactionDetails, + summaryAmount: `${renderFromWei(value)} ${ticker}`, + summaryFee: `${renderFromWei(totalGas)} ${ticker}`, + summarySecondaryTotalAmount: weiToFiat(totalEth, conversionRate, currentCurrency), + summaryTotalAmount: `${renderFromWei(totalEth)} ${ticker}` + }; + } else { + transactionDetails = { + ...transactionDetails, + summaryAmount: weiToFiat(value, conversionRate, currentCurrency), + summaryFee: weiToFiat(totalGas, conversionRate, currentCurrency), + summarySecondaryTotalAmount: `${renderFromWei(totalEth)} ${ticker}`, + summaryTotalAmount: weiToFiat(totalEth, conversionRate, currentCurrency) + }; + } + + return [transactionElement, transactionDetails]; +} + +function decodeConfirmTx(args, paymentChannelTransaction) { + const { + tx: { + transaction: { value, gas, gasPrice, from, to }, + transactionHash + }, + conversionRate, + currentCurrency, + actionKey, + primaryCurrency + } = args; + const ticker = getTicker(args.ticker); + const totalEth = hexToBN(value); + const renderTotalEth = `${renderFromWei(totalEth)} ${ticker}`; + const renderTotalEthFiat = weiToFiat(totalEth, conversionRate, currentCurrency); + + const gasBN = hexToBN(gas); + const gasPriceBN = hexToBN(gasPrice); + const totalGas = isBN(gasBN) && isBN(gasPriceBN) ? gasBN.mul(gasPriceBN) : toBN('0x0'); + const totalValue = isBN(totalEth) ? totalEth.add(totalGas) : totalGas; + + const renderFrom = renderFullAddress(from); + const renderTo = renderFullAddress(to); + let transactionDetails = { + renderFrom, + renderTo, + transactionHash, + renderValue: `${renderFromWei(value)} ${ticker}`, + renderGas: parseInt(gas, 16).toString(), + renderGasPrice: renderToGwei(gasPrice), + renderTotalGas: `${renderFromWei(totalGas)} ${ticker}` + }; + + if (primaryCurrency === 'ETH') { + transactionDetails = { + ...transactionDetails, + summaryAmount: renderTotalEth, + summaryFee: `${renderFromWei(totalGas)} ${ticker}`, + summarySecondaryTotalAmount: weiToFiat(totalValue, conversionRate, currentCurrency), + summaryTotalAmount: `${renderFromWei(totalValue)} ${ticker}` + }; + } else { + transactionDetails = { + ...transactionDetails, + summaryAmount: weiToFiat(totalEth, conversionRate, currentCurrency), + summaryFee: weiToFiat(totalGas, conversionRate, currentCurrency), + summarySecondaryTotalAmount: `${renderFromWei(totalValue)} ${ticker}`, + summaryTotalAmount: weiToFiat(totalValue, conversionRate, currentCurrency) + }; + } + + let symbol; + if (renderTo in contractMap) { + symbol = contractMap[renderTo].symbol; + } + + const transactionElement = { + renderTo, + renderFrom, + actionKey: symbol ? `${symbol} ${actionKey}` : actionKey, + value: renderTotalEth, + fiatValue: renderTotalEthFiat, + paymentChannelTransaction + }; + + return [transactionElement, transactionDetails]; +} + +/** + * Parse transaction with wallet information to render + * + * @param {*} args - Should contain tx, selectedAddress, ticker, conversionRate, + * currentCurrency, exchangeRate, contractExchangeRates, collectibleContracts, tokens + */ +export default async function decodeTransaction(args) { + const { + tx, + tx: { paymentChannelTransaction }, + selectedAddress, + ticker + } = args; + const actionKey = tx.actionKey || (await getActionKey(tx, selectedAddress, ticker, paymentChannelTransaction)); + let transactionElement, transactionDetails; + if (paymentChannelTransaction) { + [transactionElement, transactionDetails] = decodePaymentChannelTx({ ...args, actionKey }); + } else { + switch (actionKey) { + case strings('transactions.sent_tokens'): + [transactionElement, transactionDetails] = await decodeTransferTx({ ...args, actionKey }); + break; + case strings('transactions.sent_collectible'): + [transactionElement, transactionDetails] = decodeTransferFromTx({ ...args, actionKey }); + break; + case strings('transactions.contract_deploy'): + [transactionElement, transactionDetails] = decodeDeploymentTx({ ...args, actionKey }); + break; + default: + [transactionElement, transactionDetails] = decodeConfirmTx({ ...args, actionKey }); + } + } + return [transactionElement, transactionDetails]; +} diff --git a/app/components/UI/TransactionHeader/__snapshots__/index.test.js.snap b/app/components/UI/TransactionHeader/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..8eed7275293 --- /dev/null +++ b/app/components/UI/TransactionHeader/__snapshots__/index.test.js.snap @@ -0,0 +1,105 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TransactionHeader should render correctly 1`] = ` + + + + + + url + + + + + + Ropsten + + + +`; diff --git a/app/components/UI/TransactionHeader/index.js b/app/components/UI/TransactionHeader/index.js new file mode 100644 index 00000000000..58d5e6667da --- /dev/null +++ b/app/components/UI/TransactionHeader/index.js @@ -0,0 +1,139 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, Text } from 'react-native'; +import { colors, fontStyles } from '../../../styles/common'; +import { connect } from 'react-redux'; +import WebsiteIcon from '../WebsiteIcon'; +import { getHost, getUrlObj } from '../../../util/browser'; +import networkList from '../../../util/networks'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; + +const styles = StyleSheet.create({ + transactionHeader: { + paddingVertical: 20, + justifyContent: 'center', + alignItems: 'center' + }, + domainLogo: { + width: 64, + height: 64, + borderRadius: 32 + }, + assetLogo: { + alignItems: 'center', + justifyContent: 'center', + borderRadius: 10 + }, + domanUrlContainer: { + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + marginTop: 10 + }, + secureIcon: { + marginRight: 5 + }, + domainUrl: { + ...fontStyles.bold, + textAlign: 'center', + fontSize: 14, + color: colors.black + }, + networkContainer: { + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row' + }, + networkStatusIndicator: { + borderRadius: 2.5, + height: 5, + width: 5 + }, + network: { + ...fontStyles.normal, + textAlign: 'center', + fontSize: 12, + padding: 5, + color: colors.black, + textTransform: 'capitalize' + } +}); + +/** + * PureComponent that renders the transaction header used for signing, granting permissions and sending + */ +class TransactionHeader extends PureComponent { + static propTypes = { + /** + * Object containing current page title and url + */ + currentPageInformation: PropTypes.object, + /** + * String representing the selected network + */ + networkType: PropTypes.string, + /** + * Object representing the status of infura networks + */ + networkStatus: PropTypes.object + }; + + /** + * Returns a small circular indicator, red if the current selected network is offline, green if it's online. + *= + * @return {element} - JSX view element + */ + renderNetworkStatusIndicator = () => { + const { networkType, networkStatus } = this.props; + const networkStatusIndicatorColor = networkStatus[networkType] === 'ok' ? 'green' : 'red'; + const networkStatusIndicator = ( + + ); + return networkStatusIndicator; + }; + + /** + * Returns a secure icon next to the dApp URL. Lock for https protocol, warning sign otherwise. + *= + * @return {element} - JSX image element + */ + renderSecureIcon = () => { + const { url } = this.props.currentPageInformation; + const secureIcon = + getUrlObj(url).protocol === 'https:' ? ( + + ) : ( + + ); + return secureIcon; + }; + + render() { + const { + currentPageInformation: { url }, + networkType + } = this.props; + const title = getHost(url); + const networkName = networkList[networkType].shortName; + return ( + + + + {this.renderSecureIcon()} + {title} + + + {this.renderNetworkStatusIndicator()} + {networkName} + + + ); + } +} + +const mapStateToProps = state => ({ + networkType: state.engine.backgroundState.NetworkController.provider.type, + networkStatus: state.engine.backgroundState.NetworkStatusController.networkStatus.infura +}); + +export default connect(mapStateToProps)(TransactionHeader); diff --git a/app/components/UI/TransactionHeader/index.test.js b/app/components/UI/TransactionHeader/index.test.js new file mode 100644 index 00000000000..774a0e792d1 --- /dev/null +++ b/app/components/UI/TransactionHeader/index.test.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import TransactionHeader from './'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); + +describe('TransactionHeader', () => { + it('should render correctly', () => { + const initialState = { + engine: { + backgroundState: { + NetworkStatusController: { + networkStatus: { + infura: { + ropsten: 'ok' + } + } + }, + NetworkController: { + provider: { + type: 'ropsten' + } + } + } + } + }; + + const wrapper = shallow( + , + { + context: { store: mockStore(initialState) } + } + ); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/TransactionNotification/index.js b/app/components/UI/TransactionNotification/index.js index 60fb05060dc..472b501da4b 100644 --- a/app/components/UI/TransactionNotification/index.js +++ b/app/components/UI/TransactionNotification/index.js @@ -1,27 +1,23 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import { TouchableOpacity, StyleSheet, View, Text } from 'react-native'; import PropTypes from 'prop-types'; -import { colors, baseStyles, fontStyles } from '../../../styles/common'; -import ElevatedView from 'react-native-elevated-view'; -import Icon from 'react-native-vector-icons/Ionicons'; +import { colors, fontStyles, baseStyles } from '../../../styles/common'; import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons'; -import Device from '../../../util/Device'; import AnimatedSpinner from '../AnimatedSpinner'; -import { hideMessage } from 'react-native-flash-message'; import { strings } from '../../../../locales/i18n'; -import GestureRecognizer from 'react-native-swipe-gestures'; +import IonicIcon from 'react-native-vector-icons/Ionicons'; const styles = StyleSheet.create({ defaultFlashFloating: { + flex: 1, backgroundColor: colors.normalAlert, - padding: 15, - marginTop: 10, - marginLeft: 0, - marginRight: 0, - height: Device.isIphoneX() ? 90 : 70, - flexDirection: 'row' + padding: 16, + marginHorizontal: 8, + flexDirection: 'row', + borderRadius: 8 }, flashLabel: { + flex: 1, flexDirection: 'column', color: colors.white }, @@ -31,6 +27,7 @@ const styles = StyleSheet.create({ color: colors.white }, flashTitle: { + flex: 1, fontSize: 14, marginBottom: 2, lineHeight: 18, @@ -39,6 +36,17 @@ const styles = StyleSheet.create({ }, flashIcon: { marginRight: 15 + }, + closeTouchable: { + flex: 0.1, + flexDirection: 'column', + alignItems: 'flex-end' + }, + closeIcon: { + flex: 1, + color: colors.white, + alignItems: 'flex-start', + marginTop: -8 } }); @@ -46,18 +54,12 @@ const styles = StyleSheet.create({ * TransactionNotification component used to render * in-app notifications for the transctions */ -// eslint-disable-next-line import/prefer-default-export -export const TransactionNotification = props => { - const { - message: { - type, - message: { transaction, callback } - } - } = props; - +export default function TransactionNotification(props) { + const { status, transaction, onPress, onHide } = props; + console.log('TransactionNotification', status); // eslint-disable-next-line _getIcon = () => { - switch (type) { + switch (status) { case 'pending': case 'pending_withdrawal': case 'pending_deposit': @@ -68,7 +70,7 @@ export const TransactionNotification = props => { case 'success': case 'received': case 'received_payment': - return ; + return ; case 'cancelled': case 'error': return ( @@ -79,7 +81,7 @@ export const TransactionNotification = props => { // eslint-disable-next-line no-undef _getTitle = () => { - switch (type) { + switch (status) { case 'pending': return strings('notifications.pending_title'); case 'pending_deposit': @@ -87,7 +89,7 @@ export const TransactionNotification = props => { case 'pending_withdrawal': return strings('notifications.pending_withdrawal_title'); case 'success': - return strings('notifications.success_title', { nonce: transaction.nonce }); + return strings('notifications.success_title', { nonce: parseInt(transaction.nonce) }); case 'success_deposit': return strings('notifications.success_deposit_title'); case 'success_withdrawal': @@ -98,7 +100,7 @@ export const TransactionNotification = props => { assetType: transaction.assetType }); case 'speedup': - return strings('notifications.speedup_title', { nonce: transaction.nonce }); + return strings('notifications.speedup_title', { nonce: parseInt(transaction.nonce) }); case 'received_payment': return strings('notifications.received_payment_title'); case 'cancelled': @@ -111,59 +113,37 @@ export const TransactionNotification = props => { // eslint-disable-next-line no-undef _getDescription = () => { if (transaction && transaction.amount) { - return strings(`notifications.${type}_message`, { amount: transaction.amount }); - } - return strings(`notifications.${type}_message`); - }; - - // eslint-disable-next-line - _getContent = () => ( - - {this._getIcon()} - - - {this._getTitle()} - - {this._getDescription()} - - - ); - - // eslint-disable-next-line - _onPress = () => { - if (callback) { - hideMessage(); - setTimeout(() => { - callback(); - }, 300); + return strings(`notifications.${status}_message`, { amount: transaction.amount }); } + return strings(`notifications.${status}_message`); }; return ( - - hideMessage()} - config={{ - velocityThreshold: 0.2, - directionalOffsetThreshold: 50 - }} - style={baseStyles.flex} + + - - {this._getContent()} + {this._getIcon()} + + + {this._getTitle()} + + {this._getDescription()} + + + - - + + ); -}; +} TransactionNotification.propTypes = { - message: PropTypes.object + status: PropTypes.string, + transaction: PropTypes.object, + onPress: PropTypes.func, + onHide: PropTypes.func }; diff --git a/app/components/UI/TransactionReview/TransactionReviewInformation/index.js b/app/components/UI/TransactionReview/TransactionReviewInformation/index.js index 5313345d1cd..0255076348b 100644 --- a/app/components/UI/TransactionReview/TransactionReviewInformation/index.js +++ b/app/components/UI/TransactionReview/TransactionReviewInformation/index.js @@ -14,7 +14,7 @@ import { renderFromWei } from '../../../../util/number'; import { strings } from '../../../../../locales/i18n'; -import { getTicker } from '../../../../util/transactions'; +import { getTicker, getNormalizedTxState } from '../../../../util/transactions'; const styles = StyleSheet.create({ overview: { @@ -277,7 +277,7 @@ const mapStateToProps = state => ({ conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, contractExchangeRates: state.engine.backgroundState.TokenRatesController.contractExchangeRates, - transaction: state.transaction, + transaction: getNormalizedTxState(state), ticker: state.engine.backgroundState.NetworkController.provider.ticker }); diff --git a/app/components/UI/TransactionReview/TransactionReviewSummary/index.js b/app/components/UI/TransactionReview/TransactionReviewSummary/index.js index 119783ce8f3..e7db15148ad 100644 --- a/app/components/UI/TransactionReview/TransactionReviewSummary/index.js +++ b/app/components/UI/TransactionReview/TransactionReviewSummary/index.js @@ -11,7 +11,12 @@ import { import { colors, fontStyles, baseStyles } from '../../../../styles/common'; import { strings } from '../../../../../locales/i18n'; import { connect } from 'react-redux'; -import { APPROVE_FUNCTION_SIGNATURE, decodeTransferData, getTicker } from '../../../../util/transactions'; +import { + APPROVE_FUNCTION_SIGNATURE, + decodeTransferData, + getTicker, + getNormalizedTxState +} from '../../../../util/transactions'; import contractMap from 'eth-contract-metadata'; import IonicIcon from 'react-native-vector-icons/Ionicons'; import { safeToChecksumAddress } from '../../../../util/address'; @@ -200,7 +205,7 @@ const mapStateToProps = state => ({ currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, contractExchangeRates: state.engine.backgroundState.TokenRatesController.contractExchangeRates, tokens: state.engine.backgroundState.AssetsController.tokens, - transaction: state.transaction, + transaction: getNormalizedTxState(state), ticker: state.engine.backgroundState.NetworkController.provider.ticker }); diff --git a/app/components/UI/TransactionReview/index.js b/app/components/UI/TransactionReview/index.js index b6ee20eac05..afa2d31aca7 100644 --- a/app/components/UI/TransactionReview/index.js +++ b/app/components/UI/TransactionReview/index.js @@ -5,7 +5,7 @@ import { StyleSheet, Text, View, InteractionManager } from 'react-native'; import { colors, fontStyles } from '../../../styles/common'; import { connect } from 'react-redux'; import { strings } from '../../../../locales/i18n'; -import { getTransactionReviewActionKey } from '../../../util/transactions'; +import { getTransactionReviewActionKey, getNormalizedTxState } from '../../../util/transactions'; import ScrollableTabView from 'react-native-scrollable-tab-view'; import DefaultTabBar from 'react-native-scrollable-tab-view/DefaultTabBar'; import TransactionReviewInformation from './TransactionReviewInformation'; @@ -178,7 +178,7 @@ class TransactionReview extends PureComponent { const mapStateToProps = state => ({ accounts: state.engine.backgroundState.AccountTrackerController.accounts, showHexData: state.settings.showHexData, - transaction: state.transaction + transaction: getNormalizedTxState(state) }); export default connect(mapStateToProps)(TransactionReview); diff --git a/app/components/UI/Transactions/index.js b/app/components/UI/Transactions/index.js index 4e1778a083e..e03d52551b9 100644 --- a/app/components/UI/Transactions/index.js +++ b/app/components/UI/Transactions/index.js @@ -16,16 +16,15 @@ import { colors, fontStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import TransactionElement from '../TransactionElement'; import Engine from '../../../core/Engine'; -import { hasBlockExplorer } from '../../../util/networks'; import { showAlert } from '../../../actions/alert'; import TransactionsNotificationManager from '../../../core/TransactionsNotificationManager'; -import ActionModal from '../ActionModal'; import { CANCEL_RATE, SPEED_UP_RATE } from 'gaba'; import { renderFromWei } from '../../../util/number'; import { safeToChecksumAddress } from '../../../util/address'; import Device from '../../../util/Device'; import { hexToBN } from 'gaba/dist/util'; import { BN } from 'ethereumjs-util'; +import TransactionActionModal from '../TransactionActionModal'; const styles = StyleSheet.create({ wrapper: { @@ -46,47 +45,6 @@ const styles = StyleSheet.create({ fontSize: 20, color: colors.fontTertiary, ...fontStyles.normal - }, - modalView: { - alignItems: 'stretch', - flex: 1, - flexDirection: 'column', - justifyContent: 'space-between', - padding: 20 - }, - modalText: { - ...fontStyles.normal, - fontSize: 14, - textAlign: 'center', - paddingVertical: 8 - }, - modalTitle: { - ...fontStyles.bold, - fontSize: 22, - textAlign: 'center' - }, - gasTitle: { - ...fontStyles.bold, - fontSize: 16, - textAlign: 'center', - marginVertical: 8 - }, - cancelFeeWrapper: { - backgroundColor: colors.grey000, - textAlign: 'center', - padding: 15 - }, - cancelFee: { - ...fontStyles.bold, - fontSize: 24, - textAlign: 'center' - }, - warningText: { - ...fontStyles.normal, - fontSize: 12, - color: colors.red, - paddingVertical: 8, - textAlign: 'center' } }); @@ -133,10 +91,6 @@ class Transactions extends PureComponent { * A string that represents the selected address */ selectedAddress: PropTypes.string, - /** - * String representing the selected the selected network - */ - networkType: PropTypes.string, /** * ETH to current currency conversion rate */ @@ -145,10 +99,6 @@ class Transactions extends PureComponent { * Currency code of the currently-active currency */ currentCurrency: PropTypes.string, - /** - * Action that shows the global alert - */ - showAlert: PropTypes.func, /** * Loading flag from an external call */ @@ -281,8 +231,6 @@ class Transactions extends PureComponent { keyExtractor = item => item.id.toString(); - blockExplorer = () => hasBlockExplorer(this.props.networkType); - validateBalance = (tx, rate) => { const { accounts } = this.props; try { @@ -320,7 +268,7 @@ class Transactions extends PureComponent { onCancelAction = (cancelAction, existingGasPriceDecimal, tx) => { this.existingGasPriceDecimal = existingGasPriceDecimal; this.cancelTxId = tx.id; - if (this.validateBalance(tx, SPEED_UP_RATE)) { + if (this.validateBalance(tx, CANCEL_RATE)) { this.setState({ cancelIsOpen: cancelAction, cancelConfirmDisabled: true }); } else { this.setState({ cancelIsOpen: cancelAction, cancelConfirmDisabled: false }); @@ -361,17 +309,14 @@ class Transactions extends PureComponent { onSpeedUpAction={this.onSpeedUpAction} onCancelAction={this.onCancelAction} testID={'txn-item'} - selectedAddress={this.props.selectedAddress} - selected={!!this.state.selectedTx.get(item.id)} onPressItem={this.toggleDetailsView} - blockExplorer + selectedAddress={this.props.selectedAddress} tokens={this.props.tokens} collectibleContracts={this.props.collectibleContracts} contractExchangeRates={this.props.contractExchangeRates} exchangeRate={this.props.exchangeRate} conversionRate={this.props.conversionRate} currentCurrency={this.props.currentCurrency} - showAlert={this.props.showAlert} navigation={this.props.navigation} /> ); @@ -406,57 +351,36 @@ class Transactions extends PureComponent { onEndReachedThreshold={0.5} ListHeaderComponent={header} /> - - - {strings('transaction.cancel_tx_title')} - {strings('transaction.gas_cancel_fee')} - - - {`${renderFromWei(Math.floor(this.existingGasPriceDecimal * CANCEL_RATE))} ${strings( - 'unit.eth' - )}`} - - - {strings('transaction.cancel_tx_message')} - {cancelConfirmDisabled && ( - {strings('transaction.insufficient')} - )} - - - - + + - - {strings('transaction.speedup_tx_title')} - {strings('transaction.gas_speedup_fee')} - - - {`${renderFromWei(Math.floor(this.existingGasPriceDecimal * SPEED_UP_RATE))} ${strings( - 'unit.eth' - )}`} - - - {strings('transaction.speedup_tx_message')} - {speedUpConfirmDisabled && ( - {strings('transaction.insufficient')} - )} - - + confirmText={strings('transaction.lets_try')} + cancelText={strings('transaction.nevermind')} + feeText={`${renderFromWei(Math.floor(this.existingGasPriceDecimal * SPEED_UP_RATE))} ${strings( + 'unit.eth' + )}`} + titleText={strings('transaction.speedup_tx_title')} + gasTitleText={strings('transaction.gas_speedup_fee')} + descriptionText={strings('transaction.speedup_tx_message')} + /> ); }; diff --git a/app/components/UI/TxNotification/index.js b/app/components/UI/TxNotification/index.js new file mode 100644 index 00000000000..eb46b81e613 --- /dev/null +++ b/app/components/UI/TxNotification/index.js @@ -0,0 +1,501 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, Text, Dimensions, InteractionManager } from 'react-native'; +import { hideTransactionNotification } from '../../../actions/transactionNotification'; +import { connect } from 'react-redux'; +import { colors, fontStyles } from '../../../styles/common'; +import Ionicons from 'react-native-vector-icons/Ionicons'; +import TransactionDetails from '../TransactionElement/TransactionDetails'; +import decodeTransaction from '../TransactionElement/utils'; +import TransactionNotification from '../TransactionNotification'; +import Device from '../../../util/Device'; +import Animated, { Easing } from 'react-native-reanimated'; +import ElevatedView from 'react-native-elevated-view'; +import { strings } from '../../../../locales/i18n'; +import { CANCEL_RATE, SPEED_UP_RATE, BN } from 'gaba'; +import ActionContent from '../ActionModal/ActionContent'; +import TransactionActionContent from '../TransactionActionModal/TransactionActionContent'; +import { renderFromWei } from '../../../util/number'; +import Engine from '../../../core/Engine'; +import { safeToChecksumAddress } from '../../../util/address'; +import { hexToBN } from 'gaba/dist/util'; + +const BROWSER_ROUTE = 'BrowserView'; + +const styles = StyleSheet.create({ + modalView: { + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + paddingBottom: 200, + marginBottom: -300 + }, + modalContainer: { + width: '90%', + borderRadius: 10, + backgroundColor: colors.white + }, + titleWrapper: { + borderBottomWidth: StyleSheet.hairlineWidth, + borderColor: colors.grey100, + flexDirection: 'row' + }, + title: { + flex: 1, + textAlign: 'center', + fontSize: 18, + marginVertical: 12, + marginHorizontal: 24, + color: colors.fontPrimary, + ...fontStyles.bold + }, + modalTypeView: { + position: 'absolute', + bottom: 0, + paddingBottom: Device.isIphoneX() ? 20 : 10, + left: 0, + right: 0, + backgroundColor: colors.transparent + }, + modalViewInBrowserView: { + paddingBottom: Device.isIos() ? 130 : 120 + }, + modalTypeViewDetailsVisible: { + height: '100%', + backgroundColor: colors.greytransparent + }, + modalTypeViewBrowser: { + bottom: Device.isIphoneX() ? 70 : 60 + }, + closeIcon: { + paddingTop: 4, + position: 'absolute', + right: 16 + }, + notificationContainer: { + flex: 0.1, + flexDirection: 'row', + alignItems: 'flex-end' + }, + notificationWrapper: { + height: 70, + width: '100%' + }, + detailsContainer: { + flex: 1, + width: '200%', + flexDirection: 'row' + }, + transactionAction: { + width: '100%' + } +}); + +const WINDOW_WIDTH = Dimensions.get('window').width; +const ACTION_CANCEL = 'cancel'; +const ACTION_SPEEDUP = 'speedup'; + +/** + * Wrapper component for a global alert + * connected to redux + */ +class TxNotification extends PureComponent { + static propTypes = { + /** + * Map of accounts to information objects including balances + */ + accounts: PropTypes.object, + /** + /* navigation object required to push new views + */ + navigation: PropTypes.object, + /** + * Boolean that determines if the modal should be shown + */ + isVisible: PropTypes.bool.isRequired, + /** + * Number that determines when it should be autodismissed (in miliseconds) + */ + autodismiss: PropTypes.number, + /** + * function that dismisses de modal + */ + hideTransactionNotification: PropTypes.func, + /** + * An array that represents the user transactions on chain + */ + transactions: PropTypes.array, + /** + * Corresponding transaction can contain id, nonce and amount + */ + transaction: PropTypes.object, + /** + * String of selected address + */ + // eslint-disable-next-line react/no-unused-prop-types + selectedAddress: PropTypes.string, + /** + * Current provider ticker + */ + // eslint-disable-next-line react/no-unused-prop-types + ticker: PropTypes.string, + /** + * ETH to current currency conversion rate + */ + // eslint-disable-next-line react/no-unused-prop-types + conversionRate: PropTypes.number, + /** + * Currency code of the currently-active currency + */ + // eslint-disable-next-line react/no-unused-prop-types + currentCurrency: PropTypes.string, + /** + * Current exchange rate + */ + // eslint-disable-next-line react/no-unused-prop-types + exchangeRate: PropTypes.number, + /** + * Object containing token exchange rates in the format address => exchangeRate + */ + // eslint-disable-next-line react/no-unused-prop-types + contractExchangeRates: PropTypes.object, + /** + * An array that represents the user collectible contracts + */ + // eslint-disable-next-line react/no-unused-prop-types + collectibleContracts: PropTypes.array, + /** + * An array that represents the user tokens + */ + // eslint-disable-next-line react/no-unused-prop-types + tokens: PropTypes.object, + /** + * Transaction status + */ + status: PropTypes.string, + /** + * Primary currency, either ETH or Fiat + */ + // eslint-disable-next-line react/no-unused-prop-types + primaryCurrency: PropTypes.string + }; + + state = { + transactionDetails: undefined, + transactionElement: undefined, + tx: {}, + transactionDetailsIsVisible: false, + internalIsVisible: true, + inBrowserView: false + }; + + notificationAnimated = new Animated.Value(100); + detailsYAnimated = new Animated.Value(0); + actionXAnimated = new Animated.Value(0); + detailsAnimated = new Animated.Value(0); + + existingGasPriceDecimal = '0x0'; + + animatedTimingStart = (animatedRef, toValue) => { + Animated.timing(animatedRef, { + toValue, + duration: 500, + easing: Easing.linear, + useNativeDriver: true + }).start(); + }; + + detailsFadeIn = async () => { + await this.setState({ transactionDetailsIsVisible: true }); + this.animatedTimingStart(this.detailsAnimated, 1); + }; + + componentDidMount = () => { + this.props.hideTransactionNotification(); + // To get the notificationAnimated ref when component mounts + setTimeout(() => this.setState({ internalIsVisible: this.props.isVisible }), 100); + }; + + isInBrowserView = () => { + const currentRouteName = this.findRouteNameFromNavigatorState(this.props.navigation.state); + return currentRouteName === BROWSER_ROUTE; + }; + + componentDidUpdate = async prevProps => { + // Check whether current view is browser + if (this.props.isVisible && prevProps.navigation.state !== this.props.navigation.state) { + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ inBrowserView: this.isInBrowserView(prevProps) }); + } + if (!prevProps.isVisible && this.props.isVisible) { + // Auto dismiss notification in case of confirmations + this.props.autodismiss && + setTimeout(() => { + this.props.hideTransactionNotification(); + }, this.props.autodismiss); + const { paymentChannelTransaction } = this.props.transaction; + const tx = paymentChannelTransaction + ? { paymentChannelTransaction, transaction: {} } + : this.props.transactions.find(({ id }) => id === this.props.transaction.id); + const [transactionElement, transactionDetails] = await decodeTransaction({ ...this.props, tx }); + const existingGasPrice = tx.transaction ? tx.transaction.gasPrice : '0x0'; + this.existingGasPriceDecimal = parseInt(existingGasPrice === undefined ? '0x0' : existingGasPrice, 16); + // eslint-disable-next-line react/no-did-update-set-state + await this.setState({ + tx, + transactionElement, + transactionDetails, + internalIsVisible: true, + transactionDetailsIsVisible: false, + inBrowserView: this.isInBrowserView(prevProps) + }); + + setTimeout(() => this.animatedTimingStart(this.notificationAnimated, 0), 100); + } else if (prevProps.isVisible && !this.props.isVisible) { + this.animatedTimingStart(this.notificationAnimated, 200); + this.animatedTimingStart(this.detailsAnimated, 0); + // eslint-disable-next-line react/no-did-update-set-state + setTimeout( + () => + this.setState({ + internalIsVisible: false, + tx: undefined, + transactionElement: undefined, + transactionDetails: undefined + }), + 500 + ); + } + }; + + findRouteNameFromNavigatorState({ routes }) { + let route = routes[routes.length - 1]; + while (route.index !== undefined) route = route.routes[route.index]; + return route.routeName; + } + + componentWillUnmount = () => { + this.props.hideTransactionNotification(); + }; + + onClose = () => { + this.onCloseDetails(); + this.props.hideTransactionNotification(); + }; + + onCloseDetails = () => { + this.animatedTimingStart(this.detailsAnimated, 0); + setTimeout(() => this.setState({ transactionDetailsIsVisible: false }), 1000); + }; + + onPress = () => { + this.setState({ transactionDetailsIsVisible: true }); + }; + + onNotificationPress = () => { + const { + tx: { paymentChannelTransaction } + } = this.state; + if (paymentChannelTransaction) { + this.props.navigation.navigate('PaymentChannelHome'); + } else { + this.detailsFadeIn(); + } + }; + + onSpeedUpPress = () => { + const transactionActionDisabled = this.validateBalance(this.state.tx, SPEED_UP_RATE); + this.setState({ transactionAction: ACTION_SPEEDUP, transactionActionDisabled }); + this.animateActionTo(-WINDOW_WIDTH); + }; + + onCancelPress = () => { + const transactionActionDisabled = this.validateBalance(this.state.tx, CANCEL_RATE); + this.setState({ transactionAction: ACTION_CANCEL, transactionActionDisabled }); + this.animateActionTo(-WINDOW_WIDTH); + }; + + onActionFinish = () => this.animateActionTo(0); + + validateBalance = (tx, rate) => { + const { accounts } = this.props; + try { + const checksummedFrom = safeToChecksumAddress(tx.transaction.from); + const balance = accounts[checksummedFrom].balance; + return hexToBN(balance).lt( + hexToBN(tx.transaction.gasPrice) + .mul(new BN(rate * 10)) + .mul(new BN(10)) + .mul(hexToBN(tx.transaction.gas)) + .add(hexToBN(tx.transaction.value)) + ); + } catch (e) { + return false; + } + }; + + animateActionTo = position => { + this.animatedTimingStart(this.detailsYAnimated, position); + this.animatedTimingStart(this.actionXAnimated, position); + }; + + speedUpTransaction = () => { + InteractionManager.runAfterInteractions(() => { + try { + Engine.context.TransactionController.speedUpTransaction(this.state.tx.id); + } catch (e) { + // ignore because transaction already went through + } + this.onActionFinish(); + }); + }; + + cancelTransaction = () => { + InteractionManager.runAfterInteractions(() => { + try { + Engine.context.TransactionController.stopTransaction(this.state.tx.id); + } catch (e) { + // ignore because transaction already went through + } + this.onActionFinish(); + }); + }; + + render = () => { + const { navigation, status } = this.props; + const { + transactionElement, + transactionDetails, + tx, + transactionDetailsIsVisible, + internalIsVisible, + inBrowserView, + transactionAction, + transactionActionDisabled + } = this.state; + + if (!internalIsVisible) return null; + const { paymentChannelTransaction } = tx; + const isActionCancel = transactionAction === ACTION_CANCEL; + return ( + + {transactionDetailsIsVisible && !paymentChannelTransaction && ( + + + + + + {transactionElement.actionKey} + + + + + + + + + + + + + + + + )} + + + + + + + + ); + }; +} + +const mapStateToProps = state => ({ + accounts: state.engine.backgroundState.AccountTrackerController.accounts, + isVisible: state.transactionNotification.isVisible, + autodismiss: state.transactionNotification.autodismiss, + transaction: state.transactionNotification.transaction, + status: state.transactionNotification.status, + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + transactions: state.engine.backgroundState.TransactionController.transactions, + ticker: state.engine.backgroundState.NetworkController.provider.ticker, + tokens: state.engine.backgroundState.AssetsController.tokens.reduce((tokens, token) => { + tokens[token.address] = token; + return tokens; + }, {}), + collectibleContracts: state.engine.backgroundState.AssetsController.collectibleContracts, + contractExchangeRates: state.engine.backgroundState.TokenRatesController.contractExchangeRates, + conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, + currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, + primaryCurrency: state.settings.primaryCurrency +}); + +const mapDispatchToProps = dispatch => ({ + hideTransactionNotification: () => dispatch(hideTransactionNotification()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(TxNotification); diff --git a/app/components/UI/TypedSign/__snapshots__/index.test.js.snap b/app/components/UI/TypedSign/__snapshots__/index.test.js.snap index 355f6c73339..302c7015a06 100644 --- a/app/components/UI/TypedSign/__snapshots__/index.test.js.snap +++ b/app/components/UI/TypedSign/__snapshots__/index.test.js.snap @@ -1,68 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TypedSign should render correctly 1`] = ` - - - - Signature Request - - - - - - Message: - - - - + + `; diff --git a/app/components/UI/TypedSign/index.js b/app/components/UI/TypedSign/index.js index 23ad803324b..80816997079 100644 --- a/app/components/UI/TypedSign/index.js +++ b/app/components/UI/TypedSign/index.js @@ -4,43 +4,27 @@ import { StyleSheet, View, Text } from 'react-native'; import { colors, fontStyles } from '../../../styles/common'; import Engine from '../../../core/Engine'; import SignatureRequest from '../SignatureRequest'; -import { strings } from '../../../../locales/i18n'; +import ExpandedMessage from '../SignatureRequest/ExpandedMessage'; import Device from '../../../util/Device'; const styles = StyleSheet.create({ - root: { - backgroundColor: colors.white, - minHeight: '90%', - borderTopLeftRadius: 10, - borderTopRightRadius: 10, - paddingBottom: Device.isIphoneX() ? 20 : 0 - }, - informationRow: { - borderBottomColor: colors.grey200, - borderBottomWidth: 1, - padding: 20 - }, - messageLabelText: { - ...fontStyles.normal, - margin: 5, - fontSize: 16 - }, messageText: { - margin: 5, color: colors.black, ...fontStyles.normal, fontFamily: Device.isIos() ? 'Courier' : 'Roboto' }, - title: { - textAlign: 'center', - fontSize: 18, - marginVertical: 12, - marginHorizontal: 20, - color: colors.fontPrimary, - ...fontStyles.bold - }, message: { - marginLeft: 20 + marginLeft: 10 + }, + truncatedMessageWrapper: { + marginBottom: 4, + overflow: 'hidden' + }, + iosHeight: { + height: 70 + }, + androidHeight: { + height: 97 }, msgKey: { fontWeight: 'bold' @@ -48,7 +32,7 @@ const styles = StyleSheet.create({ }); /** - * PureComponent that supports eth_signTypedData and eth_signTypedData_v3 + * Component that supports eth_signTypedData and eth_signTypedData_v3 */ export default class TypedSign extends PureComponent { static propTypes = { @@ -71,7 +55,19 @@ export default class TypedSign extends PureComponent { /** * Object containing current page title and url */ - currentPageInformation: PropTypes.object + currentPageInformation: PropTypes.object, + /** + * Hides or shows the expanded signing message + */ + toggleExpandedMessage: PropTypes.func, + /** + * Indicated whether or not the expanded message is shown + */ + showExpandedMessage: PropTypes.bool + }; + + state = { + truncateMessage: false }; signMessage = async () => { @@ -101,6 +97,17 @@ export default class TypedSign extends PureComponent { this.props.onConfirm(); }; + shouldTruncateMessage = e => { + if ( + (Device.isIos() && e.nativeEvent.layout.height > 70) || + (Device.isAndroid() && e.nativeEvent.layout.height > 100) + ) { + this.setState({ truncateMessage: true }); + return; + } + this.setState({ truncateMessage: false }); + }; + renderTypedMessageV3 = obj => Object.keys(obj).map(key => ( @@ -141,32 +148,44 @@ export default class TypedSign extends PureComponent { }; render() { - const { messageParams, currentPageInformation } = this.props; + const { messageParams, currentPageInformation, showExpandedMessage, toggleExpandedMessage } = this.props; + const { truncateMessage } = this.state; + const messageWrapperStyles = []; let domain; if (messageParams.version === 'V3') { domain = JSON.parse(messageParams.data).domain; } - return ( - - - - {strings('signature_request.title')} - + if (truncateMessage) { + messageWrapperStyles.push(styles.truncatedMessageWrapper); + if (Device.isIos()) { + messageWrapperStyles.push(styles.iosHeight); + } else { + messageWrapperStyles.push(styles.androidHeight); + } + } + + const rootView = showExpandedMessage ? ( + + ) : ( + + + {this.renderTypedMessage()} - - - {strings('signature_request.message')} - {this.renderTypedMessage()} - - - + ); + return rootView; } } diff --git a/app/components/Views/AccountBackupStep4/index.js b/app/components/Views/AccountBackupStep4/index.js index c64a5071141..f84934142c7 100644 --- a/app/components/Views/AccountBackupStep4/index.js +++ b/app/components/Views/AccountBackupStep4/index.js @@ -7,7 +7,8 @@ import { View, SafeAreaView, StyleSheet, - TextInput + TextInput, + InteractionManager } from 'react-native'; import PropTypes from 'prop-types'; @@ -17,6 +18,7 @@ import StyledButton from '../../UI/StyledButton'; import { strings } from '../../../../locales/i18n'; import Engine from '../../../core/Engine'; import SecureKeychain from '../../../core/SecureKeychain'; +import PreventScreenshot from '../../../core/PreventScreenshot'; const styles = StyleSheet.create({ mainWrapper: { @@ -180,8 +182,13 @@ export default class AccountBackupStep4 extends PureComponent { } } this.setState({ ready: true }); + InteractionManager.runAfterInteractions(() => PreventScreenshot.forbid()); } + componentWillUnmount = () => { + InteractionManager.runAfterInteractions(() => PreventScreenshot.allow()); + }; + dismiss = () => { this.props.navigation.goBack(); }; diff --git a/app/components/Views/Approval/index.js b/app/components/Views/Approval/index.js index c3558d74133..a04a294c1e2 100644 --- a/app/components/Views/Approval/index.js +++ b/app/components/Views/Approval/index.js @@ -6,12 +6,12 @@ import TransactionEditor from '../../UI/TransactionEditor'; import { BNToHex, hexToBN } from '../../../util/number'; import { getTransactionOptionsTitle } from '../../UI/Navbar'; import { colors } from '../../../styles/common'; -import { newTransaction } from '../../../actions/transaction'; +import { resetTransaction } from '../../../actions/transaction'; import { connect } from 'react-redux'; import TransactionsNotificationManager from '../../../core/TransactionsNotificationManager'; import Analytics from '../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; -import { getTransactionReviewActionKey } from '../../../util/transactions'; +import { getTransactionReviewActionKey, getNormalizedTxState } from '../../../util/transactions'; import { strings } from '../../../../locales/i18n'; import { safeToChecksumAddress } from '../../../util/address'; @@ -40,7 +40,7 @@ class Approval extends PureComponent { /** * Action that cleans transaction state */ - newTransaction: PropTypes.func.isRequired, + resetTransaction: PropTypes.func.isRequired, /** * Transaction state */ @@ -137,7 +137,7 @@ class Approval extends PureComponent { * Transaction state is erased, ready to create a new clean transaction */ clear = () => { - this.props.newTransaction(); + this.props.resetTransaction(); }; onCancel = () => { @@ -259,13 +259,13 @@ class Approval extends PureComponent { } const mapStateToProps = state => ({ - transaction: state.transaction, + transaction: getNormalizedTxState(state), transactions: state.engine.backgroundState.TransactionController.transactions, networkType: state.engine.backgroundState.NetworkController.provider.type }); const mapDispatchToProps = dispatch => ({ - newTransaction: () => dispatch(newTransaction()) + resetTransaction: () => dispatch(resetTransaction()) }); export default connect( diff --git a/app/components/Views/ApproveView/Approve/index.js b/app/components/Views/ApproveView/Approve/index.js index 42653999c40..b8e4cc1c8e3 100644 --- a/app/components/Views/ApproveView/Approve/index.js +++ b/app/components/Views/ApproveView/Approve/index.js @@ -29,7 +29,12 @@ import { strings } from '../../../../../locales/i18n'; import { setTransactionObject } from '../../../../actions/transaction'; import { BNToHex, hexToBN } from 'gaba/dist/util'; import { renderFromWei, weiToFiatNumber, isBN, renderFromTokenMinimalUnit, isDecimal } from '../../../../util/number'; -import { getTicker, decodeTransferData, generateApproveData } from '../../../../util/transactions'; +import { + getTicker, + decodeTransferData, + generateApproveData, + getNormalizedTxState +} from '../../../../util/transactions'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import ErrorMessage from '../../SendFlow/ErrorMessage'; import { showAlert } from '../../../../actions/alert'; @@ -894,7 +899,7 @@ const mapStateToProps = state => ({ currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, identities: state.engine.backgroundState.PreferencesController.identities, ticker: state.engine.backgroundState.NetworkController.provider.ticker, - transaction: state.transaction, + transaction: getNormalizedTxState(state), transactions: state.engine.backgroundState.TransactionController.transactions, accountsLength: Object.keys(state.engine.backgroundState.AccountTrackerController.accounts).length, tokensLength: state.engine.backgroundState.AssetsController.tokens.length, diff --git a/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap index bc16374cdb1..3c591fe602d 100644 --- a/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap +++ b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap @@ -91,6 +91,7 @@ exports[`Browser should render correctly 1`] = ` autoCapitalize="none" autoCorrect={false} clearButtonMode="while-editing" + keyboardType="web-search" onChangeText={[Function]} onSubmitEditing={[Function]} placeholder="Search or Type URL" diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js index 929dbfedc78..6bb7b2a2a93 100644 --- a/app/components/Views/BrowserTab/index.js +++ b/app/components/Views/BrowserTab/index.js @@ -1167,11 +1167,6 @@ export class BrowserTab extends PureComponent { addBookmark = () => { this.toggleOptionsIfNeeded(); - // Check it doesn't exist already - if (this.props.bookmarks.filter(i => i.url === this.state.inputValue).length) { - Alert.alert(strings('browser.error'), strings('browser.bookmark_already_exists')); - return false; - } this.checkForPageMeta(() => this.props.navigation.push('AddBookmarkView', { title: this.state.currentPageTitle || '', @@ -1498,14 +1493,16 @@ export class BrowserTab extends PureComponent { {strings('browser.reload')} - + {!this.isBookmark() && ( + + )}