diff --git a/android/app/src/main/java/io/metamask/MainApplication.java b/android/app/src/main/java/io/metamask/MainApplication.java index 8782cc9fca1..d5bf78bdca3 100644 --- a/android/app/src/main/java/io/metamask/MainApplication.java +++ b/android/app/src/main/java/io/metamask/MainApplication.java @@ -1,6 +1,7 @@ package io.metamask; import com.facebook.react.ReactApplication; +import com.reactnativecommunity.webviewforked.RNCWebViewForkedPackage; import com.cmcewen.blurview.BlurViewPackage; import android.content.Context; import com.facebook.react.PackageList; diff --git a/android/settings.gradle b/android/settings.gradle index b5e08ef3e18..f5b78e3e1af 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'MetaMask' +include ':react-native-webview-forked' +project(':react-native-webview-forked').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview-forked/android') include ':@react-native-community_blur' project(':@react-native-community_blur').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/blur/android') include ':lottie-react-native' diff --git a/app/__mocks__/@exodus/react-native-payments.js b/app/__mocks__/@exodus/react-native-payments.js new file mode 100644 index 00000000000..b69bc6d5c9f --- /dev/null +++ b/app/__mocks__/@exodus/react-native-payments.js @@ -0,0 +1 @@ +export default from '@exodus/react-native-payments/lib/js/__mocks__'; diff --git a/app/components/Base/DetailsModal.js b/app/components/Base/DetailsModal.js new file mode 100644 index 00000000000..df042dff0bf --- /dev/null +++ b/app/components/Base/DetailsModal.js @@ -0,0 +1,127 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { View, StyleSheet, TouchableOpacity } from 'react-native'; +import Ionicons from 'react-native-vector-icons/Ionicons'; +import { colors, fontStyles } from '../../styles/common'; + +import Text from './Text'; + +const styles = StyleSheet.create({ + modalContainer: { + width: '100%', + backgroundColor: colors.white, + borderRadius: 10 + }, + modalView: { + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center' + }, + header: { + 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 }, + body: { + paddingHorizontal: 15 + }, + section: { + paddingVertical: 16, + flexDirection: 'row' + }, + sectionBorderBottom: { + borderBottomColor: colors.grey100, + borderBottomWidth: 1 + }, + column: { + flex: 1 + }, + columnEnd: { + alignItems: 'flex-end' + }, + sectionTitle: { + ...fontStyles.normal, + fontSize: 10, + color: colors.grey500, + marginBottom: 8 + } +}); +const DetailsModal = ({ children }) => ( + + {children} + +); + +const DetailsModalHeader = ({ style, ...props }) => ; +const DetailsModalTitle = ({ style, ...props }) => ; +const DetailsModalCloseIcon = ({ style, ...props }) => ( + + + +); +const DetailsModalBody = ({ style, ...props }) => ; +const DetailsModalSection = ({ style, borderBottom, ...props }) => ( + +); +const DetailsModalSectionTitle = ({ style, ...props }) => ; +const DetailsModalColumn = ({ style, end, ...props }) => ( + +); + +DetailsModal.Header = DetailsModalHeader; +DetailsModal.Title = DetailsModalTitle; +DetailsModal.CloseIcon = DetailsModalCloseIcon; +DetailsModal.Body = DetailsModalBody; +DetailsModal.Section = DetailsModalSection; +DetailsModal.SectionTitle = DetailsModalSectionTitle; +DetailsModal.Column = DetailsModalColumn; + +/** + * Any other external style defined in props will be applied + */ +const stylePropType = PropTypes.oneOfType([PropTypes.object, PropTypes.array]); + +DetailsModal.propTypes = { + children: PropTypes.node +}; + +DetailsModalHeader.propTypes = { + style: stylePropType +}; +DetailsModalTitle.propTypes = { + style: stylePropType +}; +DetailsModalCloseIcon.propTypes = { + style: stylePropType +}; +DetailsModalBody.propTypes = { + style: stylePropType +}; +DetailsModalSection.propTypes = { + style: stylePropType, + /** + * Adds a border to the bottom of the section + */ + borderBottom: PropTypes.bool +}; +DetailsModalSectionTitle.propTypes = { + style: stylePropType +}; +DetailsModalColumn.propTypes = { + style: stylePropType, + /** + * Aligns column content to flex-end + */ + end: PropTypes.bool +}; +export default DetailsModal; diff --git a/app/components/Base/ListItem.js b/app/components/Base/ListItem.js new file mode 100644 index 00000000000..a2294a45722 --- /dev/null +++ b/app/components/Base/ListItem.js @@ -0,0 +1,112 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View } from 'react-native'; +import Device from '../../util/Device'; +import { colors, fontStyles } from '../../styles/common'; +import Text from './Text'; + +const styles = StyleSheet.create({ + wrapper: { + padding: 15, + minHeight: Device.isIos() ? 95 : 100 + }, + date: { + color: colors.fontSecondary, + fontSize: 12, + marginBottom: 10, + ...fontStyles.normal + }, + content: { + flexDirection: 'row' + }, + actions: { + flexDirection: 'row', + paddingTop: 10, + paddingLeft: 40 + }, + icon: { + flexDirection: 'row', + alignItems: 'center' + }, + body: { + flex: 1, + marginLeft: 15 + }, + amounts: { + flex: 0.6, + alignItems: 'flex-end' + }, + title: { + fontSize: 15, + color: colors.fontPrimary + }, + amount: { + fontSize: 15, + color: colors.fontPrimary + }, + fiatAmount: { + fontSize: 12, + color: colors.fontSecondary, + textTransform: 'uppercase' + } +}); + +const ListItem = ({ style, ...props }) => ; + +const ListItemDate = ({ style, ...props }) => ; +const ListItemContent = ({ style, ...props }) => ; +const ListItemActions = ({ style, ...props }) => ; +const ListItemIcon = ({ style, ...props }) => ; +const ListItemBody = ({ style, ...props }) => ; +const ListItemTitle = ({ style, ...props }) => ; +const ListItemAmounts = ({ style, ...props }) => ; +const ListItemAmount = ({ style, ...props }) => ; +const ListItemFiatAmount = ({ style, ...props }) => ; + +ListItem.Date = ListItemDate; +ListItem.Content = ListItemContent; +ListItem.Actions = ListItemActions; +ListItem.Icon = ListItemIcon; +ListItem.Body = ListItemBody; +ListItem.Title = ListItemTitle; +ListItem.Amounts = ListItemAmounts; +ListItem.Amount = ListItemAmount; +ListItem.FiatAmount = ListItemFiatAmount; + +export default ListItem; + +/** + * Any other external style defined in props will be applied + */ +const stylePropType = PropTypes.oneOfType([PropTypes.object, PropTypes.array]); + +ListItem.propTypes = { + style: stylePropType +}; +ListItemDate.propTypes = { + style: stylePropType +}; +ListItemContent.propTypes = { + style: stylePropType +}; +ListItemActions.propTypes = { + style: stylePropType +}; +ListItemIcon.propTypes = { + style: stylePropType +}; +ListItemBody.propTypes = { + style: stylePropType +}; +ListItemTitle.propTypes = { + style: stylePropType +}; +ListItemAmounts.propTypes = { + style: stylePropType +}; +ListItemAmount.propTypes = { + style: stylePropType +}; +ListItemFiatAmount.propTypes = { + style: stylePropType +}; diff --git a/app/components/Base/ModalHandler.js b/app/components/Base/ModalHandler.js new file mode 100644 index 00000000000..417697a090c --- /dev/null +++ b/app/components/Base/ModalHandler.js @@ -0,0 +1,17 @@ +import { useState } from 'react'; + +function ModalHandler({ children }) { + const [isVisible, setVisible] = useState(false); + + const showModal = () => setVisible(true); + const hideModal = () => setVisible(true); + const toggleModal = () => setVisible(!isVisible); + + if (typeof children === 'function') { + return children({ isVisible, toggleModal, showModal, hideModal }); + } + + return children; +} + +export default ModalHandler; diff --git a/app/components/Base/StatusText.js b/app/components/Base/StatusText.js new file mode 100644 index 00000000000..53d2850fac8 --- /dev/null +++ b/app/components/Base/StatusText.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Text from './Text'; +import { colors } from '../../styles/common'; +import { StyleSheet } from 'react-native'; +import { FIAT_ORDER_STATES } from '../../reducers/fiatOrders'; +import { strings } from '../../../locales/i18n'; + +const styles = StyleSheet.create({ + status: { + marginTop: 4, + fontSize: 12, + letterSpacing: 0.5 + } +}); + +export const ConfirmedText = props => ; +export const PendingText = props => ; +export const FailedText = props => ; + +function StatusText({ status, context, ...props }) { + switch (status) { + case 'Confirmed': + case 'confirmed': + return {strings(`${context}.${status}`)}; + case 'Pending': + case 'pending': + case 'Submitted': + case 'submitted': + return {strings(`${context}.${status}`)}; + case 'Failed': + case 'Cancelled': + case 'failed': + case 'cancelled': + return {strings(`${context}.${status}`)}; + + case FIAT_ORDER_STATES.COMPLETED: + return {strings(`${context}.completed`)}; + case FIAT_ORDER_STATES.PENDING: + return {strings(`${context}.pending`)}; + case FIAT_ORDER_STATES.FAILED: + return {strings(`${context}.failed`)}; + case FIAT_ORDER_STATES.CANCELLED: + return {strings(`${context}.cancelled`)}; + + default: + return ( + + {status} + + ); + } +} + +StatusText.defaultProps = { + context: 'transaction' +}; + +StatusText.propTypes = { + status: PropTypes.string.isRequired, + context: PropTypes.string +}; + +export default StatusText; diff --git a/app/components/Base/Summary.js b/app/components/Base/Summary.js new file mode 100644 index 00000000000..9112ac081c7 --- /dev/null +++ b/app/components/Base/Summary.js @@ -0,0 +1,77 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { View, StyleSheet } from 'react-native'; +import { colors } from '../../styles/common'; + +const styles = StyleSheet.create({ + wrapper: { + borderWidth: 1, + borderColor: colors.grey050, + borderRadius: 8, + padding: 16 + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + marginVertical: 6 + }, + rowEnd: { + justifyContent: 'flex-end' + }, + rowLast: { + marginBottom: 0, + marginTop: 6 + }, + col: { + flexDirection: 'row', + flex: 1, + flexWrap: 'wrap' + }, + separator: { + borderBottomWidth: 1, + borderBottomColor: colors.grey050, + marginVertical: 6 + } +}); + +const Summary = ({ style, ...props }) => ; +const SummaryRow = ({ style, end, last, ...props }) => ( + +); +const SummaryCol = ({ style, end, ...props }) => ; +const SummarySeparator = ({ style, ...props }) => ; + +Summary.Row = SummaryRow; +Summary.Col = SummaryCol; +Summary.Separator = SummarySeparator; +export default Summary; + +/** + * Any other external style defined in props will be applied + */ +const stylePropType = PropTypes.oneOfType([PropTypes.object, PropTypes.array]); + +Summary.propTypes = { + style: stylePropType +}; +SummaryRow.propTypes = { + style: stylePropType, + /** + * Aligns content to the end of the row + */ + end: PropTypes.bool, + /** + * Add style to the last row of the summary + */ + last: PropTypes.bool +}; +SummaryCol.propTypes = { + style: stylePropType, + /** + * Aligns content to the end of the row + */ + end: PropTypes.bool +}; +SummarySeparator.propTypes = { + style: stylePropType +}; diff --git a/app/components/Base/TabBar.js b/app/components/Base/TabBar.js new file mode 100644 index 00000000000..ef2884a379d --- /dev/null +++ b/app/components/Base/TabBar.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import DefaultTabBar from 'react-native-scrollable-tab-view/DefaultTabBar'; + +import { colors, fontStyles } from '../../styles/common'; + +const styles = StyleSheet.create({ + tabUnderlineStyle: { + height: 2, + backgroundColor: colors.blue + }, + tabStyle: { + paddingVertical: 8 + }, + textStyle: { + ...fontStyles.normal, + fontSize: 14 + } +}); + +function TabBar({ ...props }) { + return ( + + ); +} + +export default TabBar; diff --git a/app/components/Base/Text.js b/app/components/Base/Text.js new file mode 100644 index 00000000000..43f26fdd610 --- /dev/null +++ b/app/components/Base/Text.js @@ -0,0 +1,158 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Text as RNText, StyleSheet } from 'react-native'; +import { fontStyles, colors } from '../../styles/common'; + +const style = StyleSheet.create({ + text: { + ...fontStyles.normal, + color: colors.grey600, + marginVertical: 2, + fontSize: 14 + }, + centered: { + textAlign: 'center' + }, + right: { + textAlign: 'right' + }, + bold: fontStyles.bold, + green: { + color: colors.green400 + }, + primary: { + color: colors.fontPrimary + }, + small: { + fontSize: 12 + }, + upper: { + textTransform: 'uppercase' + }, + disclaimer: { + fontStyle: 'italic', + letterSpacing: 0.15 + }, + modal: { + color: colors.fontPrimary, + fontSize: 16, + lineHeight: 30 + }, + link: { + color: colors.blue + }, + strikethrough: { + textDecorationLine: 'line-through' + } +}); + +const Text = ({ + reset, + centered, + right, + bold, + green, + primary, + small, + upper, + modal, + disclaimer, + link, + strikethrough, + style: externalStyle, + ...props +}) => ( + +); + +Text.defaultProps = { + reset: false, + centered: false, + right: false, + bold: false, + green: false, + primary: false, + disclaimer: false, + modal: false, + small: false, + upper: false, + link: false, + strikethrough: false, + style: undefined +}; + +Text.propTypes = { + /** + * Removes teh default style + */ + reset: PropTypes.bool, + /** + * Align text to center + */ + centered: PropTypes.bool, + /** + * Align text to right + */ + right: PropTypes.bool, + /** + * Makes text bold + */ + bold: PropTypes.bool, + /** + * Makes text green + */ + green: PropTypes.bool, + /** + * Makes text fontPrimary color + */ + primary: PropTypes.bool, + /** + * Makes text italic and tight + * used in disclaimers + */ + disclaimer: PropTypes.bool, + /** + * Makes text black and bigger + * Used in modals + */ + modal: PropTypes.bool, + /** + * Makes text small + */ + small: PropTypes.bool, + /** + * Makes text uppercase + */ + upper: PropTypes.bool, + /** + * Applies a link style + */ + link: PropTypes.bool, + /** + * Applies a strikethrough decoration + */ + strikethrough: PropTypes.bool, + /** + * Any other external style defined in props will be applied + */ + style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]) +}; + +export default Text; diff --git a/app/components/Base/Title.js b/app/components/Base/Title.js new file mode 100644 index 00000000000..8a015378fbf --- /dev/null +++ b/app/components/Base/Title.js @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet } from 'react-native'; +import { fontStyles, colors } from '../../styles/common'; +import Text from './Text.js'; + +const style = StyleSheet.create({ + text: { + fontSize: 18, + marginVertical: 3, + color: colors.fontPrimary, + ...fontStyles.bold + }, + hero: { + fontSize: 22 + }, + centered: { + textAlign: 'center' + } +}); + +const Title = ({ centered, hero, style: externalStyle, ...props }) => ( + +); + +Title.defaultProps = { + centered: false, + hero: false, + style: undefined +}; + +Title.propTypes = { + /** + * Aligns title to center + */ + centered: PropTypes.bool, + /** + * Makes title bigger + */ + hero: PropTypes.bool, + /** + * Any other external style defined in props will be applied + */ + style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]) +}; + +export default Title; diff --git a/app/components/Nav/App/__snapshots__/index.test.js.snap b/app/components/Nav/App/__snapshots__/index.test.js.snap index 2d67e59a81b..399463fa8a0 100644 --- a/app/components/Nav/App/__snapshots__/index.test.js.snap +++ b/app/components/Nav/App/__snapshots__/index.test.js.snap @@ -61,6 +61,20 @@ exports[`App should render correctly 1`] = ` "getScreenOptions": [Function], "getStateForAction": [Function], }, + "FiatOnRamp": Object { + "childRouters": Object { + "PaymentMethodApplePay": null, + "PaymentMethodSelector": null, + "TransakFlow": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "Home": Object { "childRouters": Object { "BrowserTabHome": Object { @@ -516,6 +530,20 @@ exports[`App should render correctly 1`] = ` "getScreenOptions": [Function], "getStateForAction": [Function], }, + "FiatOnRamp": Object { + "childRouters": Object { + "PaymentMethodApplePay": null, + "PaymentMethodSelector": null, + "TransakFlow": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "Home": Object { "childRouters": Object { "BrowserTabHome": Object { diff --git a/app/components/Nav/Main/__snapshots__/index.test.js.snap b/app/components/Nav/Main/__snapshots__/index.test.js.snap index 77acb0ad3d9..eefb84996d2 100644 --- a/app/components/Nav/Main/__snapshots__/index.test.js.snap +++ b/app/components/Nav/Main/__snapshots__/index.test.js.snap @@ -69,6 +69,20 @@ exports[`Main should render correctly 1`] = ` "getScreenOptions": [Function], "getStateForAction": [Function], }, + "FiatOnRamp": Object { + "childRouters": Object { + "PaymentMethodApplePay": null, + "PaymentMethodSelector": null, + "TransakFlow": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "Home": Object { "childRouters": Object { "BrowserTabHome": Object { @@ -400,6 +414,20 @@ exports[`Main should render correctly 1`] = ` "getScreenOptions": [Function], "getStateForAction": [Function], }, + "FiatOnRamp": Object { + "childRouters": Object { + "PaymentMethodApplePay": null, + "PaymentMethodSelector": null, + "TransakFlow": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "Home": Object { "childRouters": Object { "BrowserTabHome": Object { diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index f8628997d75..a3749e25cef 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -31,7 +31,6 @@ import NetworkSettings from '../../Views/Settings/NetworksSettings/NetworkSettin import AppInformation from '../../Views/Settings/AppInformation'; import Contacts from '../../Views/Settings/Contacts'; import Wallet from '../../Views/Wallet'; -import TransactionsView from '../../Views/TransactionsView'; import SyncWithExtension from '../../Views/SyncWithExtension'; import Asset from '../../Views/Asset'; import AddAsset from '../../Views/AddAsset'; @@ -96,6 +95,10 @@ import ContactForm from '../../Views/Settings/Contacts/ContactForm'; import TransactionTypes from '../../../core/TransactionTypes'; import BackupAlert from '../../UI/BackupAlert'; import Notification from '../../UI/Notification'; +import FiatOrders from '../../UI/FiatOrders'; +import PaymentMethodSelector from '../../UI/FiatOrders/PaymentMethodSelector'; +import PaymentMethodApplePay from '../../UI/FiatOrders/PaymentMethodApplePay'; +import TransakWebView from '../../UI/FiatOrders/TransakWebView'; import { showTransactionNotification, hideTransactionNotification, @@ -103,6 +106,7 @@ import { } from '../../../actions/notification'; import { toggleDappTransactionModal, toggleApproveModal } from '../../../actions/modals'; import AccountApproval from '../../UI/AccountApproval'; +import ActivityView from '../../Views/ActivityView'; import ProtectYourWalletModal from '../../UI/ProtectYourWalletModal'; const styles = StyleSheet.create({ @@ -170,7 +174,7 @@ const MainNavigator = createStackNavigator( }), TransactionsHome: createStackNavigator({ TransactionsView: { - screen: TransactionsView + screen: ActivityView } }), PaymentChannelHome: createStackNavigator({ @@ -339,6 +343,15 @@ const MainNavigator = createStackNavigator( } ) }, + + FiatOnRamp: { + screen: createStackNavigator({ + PaymentMethodSelector: { screen: PaymentMethodSelector }, + PaymentMethodApplePay: { screen: PaymentMethodApplePay }, + TransakFlow: { screen: TransakWebView } + }) + }, + SetPasswordFlow: { screen: createStackNavigator( { @@ -1097,6 +1110,7 @@ class Main extends PureComponent { + diff --git a/app/components/UI/FiatOrders/PaymentMethodApplePay/index.js b/app/components/UI/FiatOrders/PaymentMethodApplePay/index.js new file mode 100644 index 00000000000..96eaa15752a --- /dev/null +++ b/app/components/UI/FiatOrders/PaymentMethodApplePay/index.js @@ -0,0 +1,500 @@ +import React, { useContext, useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { View, StyleSheet, Image, TouchableOpacity, InteractionManager } from 'react-native'; +import { NavigationContext } from 'react-navigation'; +import { connect } from 'react-redux'; +import IonicIcon from 'react-native-vector-icons/Ionicons'; +import NotificationManager from '../../../../core/NotificationManager'; +import Device from '../../../../util/Device'; +import Logger from '../../../../util/Logger'; +import { setLockTime } from '../../../../actions/settings'; +import { strings } from '../../../../../locales/i18n'; +import { getNotificationDetails } from '..'; + +import { + useWyreTerms, + useWyreRates, + useWyreApplePay, + WyreException, + WYRE_IS_PROMOTION, + WYRE_FEE_PERCENT +} from '../orderProcessor/wyreApplePay'; + +import ScreenView from '../components/ScreenView'; +import { getPaymentMethodApplePayNavbar } from '../../Navbar'; +import AccountBar from '../components/AccountBar'; +import Text from '../../../Base/Text'; +import StyledButton from '../../StyledButton'; +import { colors, fontStyles } from '../../../../styles/common'; +import { protectWalletModalVisible } from '../../../../actions/user'; + +//* styles and components */ + +const styles = StyleSheet.create({ + screen: { + flexGrow: 1, + justifyContent: 'space-between' + }, + amountContainer: { + margin: Device.isIphone5() ? 0 : 10, + padding: Device.isMediumDevice() ? (Device.isIphone5() ? 5 : 10) : 15, + alignItems: 'center', + justifyContent: 'center' + }, + amount: { + ...fontStyles.light, + color: colors.black, + fontSize: Device.isIphone5() ? 48 : 48, + height: Device.isIphone5() ? 50 : 60 + }, + amountError: { + color: colors.red + }, + content: { + flexGrow: 1, + justifyContent: 'space-around' + }, + quickAmounts: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + marginHorizontal: 70 + }, + quickAmount: { + borderRadius: 18, + borderColor: colors.grey200, + borderWidth: 1, + paddingVertical: 5, + paddingHorizontal: 8, + alignItems: 'center', + minWidth: 49 + }, + quickAmountSelected: { + backgroundColor: colors.blue, + borderColor: colors.blue + }, + quickAmountSelectedText: { + color: colors.white + }, + keypad: { + paddingHorizontal: 25 + }, + keypadRow: { + flexDirection: 'row', + justifyContent: 'space-around' + }, + keypadButton: { + paddingHorizontal: 20, + paddingVertical: Device.isMediumDevice() ? (Device.isIphone5() ? 5 : 10) : 15, + flex: 1, + justifyContent: 'center', + alignItems: 'center' + }, + keypadButtonText: { + color: colors.black, + textAlign: 'center', + fontSize: 30 + }, + deleteIcon: { + fontSize: 25, + marginTop: 5 + }, + buttonContainer: { + paddingBottom: 20 + }, + applePayButton: { + backgroundColor: colors.black, + padding: 10, + margin: Device.isIphone5() ? 5 : 10, + marginHorizontal: 25, + alignItems: 'center' + }, + applePayButtonText: { + color: colors.white + }, + applePayButtonContentDisabled: { + opacity: 0.6 + }, + applePayLogo: { + marginLeft: 4 + } +}); + +/* eslint-disable import/no-commonjs */ +const ApplePayLogo = require('../../../../images/ApplePayLogo.png'); +const ApplePay = ({ disabled }) => ( + +); + +ApplePay.propTypes = { + disabled: PropTypes.bool +}; + +const Keypad = props => ; +Keypad.Row = function Row(props) { + return ; +}; +Keypad.Button = function KeypadButton({ children, ...props }) { + return ( + + {children} + + ); +}; + +Keypad.Button.propTypes = { + children: PropTypes.node +}; + +Keypad.DeleteButton = function DeleteButton(props) { + return ( + + + + ); +}; + +const QuickAmount = ({ amount, current, ...props }) => { + const selected = amount === current; + return ( + + + ${amount} + + + ); +}; + +QuickAmount.propTypes = { + amount: PropTypes.string, + current: PropTypes.string +}; + +//* Constants */ + +const quickAmounts = ['50', '100', '250']; +const minAmount = 5; +const maxAmount = 250; + +const hasTwoDecimals = /^\d+\.\d{2}$/; +const hasZeroAsFirstDecimal = /^\d+\.0$/; +const hasZerosAsDecimals = /^\d+\.00$/; +const hasOneDigit = /^\d$/; +const hasPeriodWithoutDecimal = /^\d+\.$/; +const avoidZerosAsDecimals = false; + +//* Handlers + +const handleNewAmountInput = (currentAmount, newInput) => { + switch (newInput) { + case 'PERIOD': { + if (currentAmount === '0') { + return `${currentAmount}.`; + } + if (currentAmount.includes('.')) { + // TODO: throw error for feedback? + return currentAmount; + } + + return `${currentAmount}.`; + } + case 'BACK': { + if (currentAmount === '0') { + return currentAmount; + } + if (hasOneDigit.test(currentAmount)) { + return '0'; + } + + return currentAmount.slice(0, -1); + } + case '0': { + if (currentAmount === '0') { + return currentAmount; + } + if (hasTwoDecimals.test(currentAmount)) { + return currentAmount; + } + if (avoidZerosAsDecimals && hasZeroAsFirstDecimal.test(currentAmount)) { + return currentAmount; + } + return `${currentAmount}${newInput}`; + } + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': { + if (currentAmount === '0') { + return newInput; + } + if (hasTwoDecimals.test(currentAmount)) { + return currentAmount; + } + + return `${currentAmount}${newInput}`; + } + default: { + return currentAmount; + } + } +}; + +function PaymentMethodApplePay({ + lockTime, + setLockTime, + selectedAddress, + network, + addOrder, + protectWalletModalVisible +}) { + const navigation = useContext(NavigationContext); + const [amount, setAmount] = useState('0'); + const roundAmount = + hasZerosAsDecimals.test(amount) || hasZeroAsFirstDecimal.test(amount) || hasPeriodWithoutDecimal.test(amount) + ? amount.split('.')[0] + : amount; + const isUnderMinimum = (amount !== '0' || Number(roundAmount) !== 0) && Number(roundAmount) < minAmount; + + const isOverMaximum = Number(roundAmount) > maxAmount; + const disabledButton = amount === '0' || isUnderMinimum || isOverMaximum; + + const handleWyreTerms = useWyreTerms(navigation); + const rates = useWyreRates(network, 'USDETH'); + const [pay, ABORTED, percentFee, flatFee, , fee] = useWyreApplePay(roundAmount, selectedAddress, network); + + const handlePressApplePay = useCallback(async () => { + const prevLockTime = lockTime; + setLockTime(-1); + try { + const order = await pay(); + if (order !== ABORTED) { + if (order) { + addOrder(order); + navigation.dismiss(); + protectWalletModalVisible(); + InteractionManager.runAfterInteractions(() => + NotificationManager.showSimpleNotification(getNotificationDetails(order)) + ); + } else { + Logger.error('FiatOrders::WyreApplePayProcessor empty order response', order); + } + } + } catch (error) { + NotificationManager.showSimpleNotification({ + duration: 5000, + title: strings('fiat_on_ramp.notifications.purchase_failed_title', { + currency: 'ETH' + }), + description: `${error instanceof WyreException ? 'Wyre: ' : ''}${error.message}`, + status: 'error' + }); + Logger.error(error, 'FiatOrders::WyreApplePayProcessor Error'); + } finally { + setLockTime(prevLockTime); + } + }, [ABORTED, addOrder, lockTime, navigation, pay, setLockTime, protectWalletModalVisible]); + + const handleQuickAmountPress = useCallback(amount => setAmount(amount), []); + const handleKeypadPress = useCallback( + newInput => { + if (isOverMaximum && newInput !== 'BACK') { + return; + } + const newAmount = handleNewAmountInput(amount, newInput); + if (newAmount === amount) { + return; + } + + setAmount(newAmount); + }, + [amount, isOverMaximum] + ); + const handleKeypadPress1 = useCallback(() => handleKeypadPress('1'), [handleKeypadPress]); + const handleKeypadPress2 = useCallback(() => handleKeypadPress('2'), [handleKeypadPress]); + const handleKeypadPress3 = useCallback(() => handleKeypadPress('3'), [handleKeypadPress]); + const handleKeypadPress4 = useCallback(() => handleKeypadPress('4'), [handleKeypadPress]); + const handleKeypadPress5 = useCallback(() => handleKeypadPress('5'), [handleKeypadPress]); + const handleKeypadPress6 = useCallback(() => handleKeypadPress('6'), [handleKeypadPress]); + const handleKeypadPress7 = useCallback(() => handleKeypadPress('7'), [handleKeypadPress]); + const handleKeypadPress8 = useCallback(() => handleKeypadPress('8'), [handleKeypadPress]); + const handleKeypadPress9 = useCallback(() => handleKeypadPress('9'), [handleKeypadPress]); + const handleKeypadPress0 = useCallback(() => handleKeypadPress('0'), [handleKeypadPress]); + const handleKeypadPressPeriod = useCallback(() => handleKeypadPress('PERIOD'), [handleKeypadPress]); + const handleKeypadPressBack = useCallback(() => handleKeypadPress('BACK'), [handleKeypadPress]); + + return ( + + + + + + ${amount} + + {!(isUnderMinimum || isOverMaximum) && + (rates ? ( + + {roundAmount === '0' ? ( + `$${rates.USD.toFixed(2)} ≈ 1 ETH` + ) : ( + <> + {strings('fiat_on_ramp.wyre_estimated', { + currency: 'ETH', + amount: (amount * rates.ETH).toFixed(5) + })} + + )} + + ) : ( + {strings('fiat_on_ramp.wyre_loading_rates')} + ))} + {isUnderMinimum && ( + {strings('fiat_on_ramp.wyre_minimum_deposit', { amount: `$${minAmount}` })} + )} + {isOverMaximum && ( + + {strings('fiat_on_ramp.wyre_maximum_deposit', { amount: `$${maxAmount}` })} + + )} + + {quickAmounts.length > 0 && ( + + {quickAmounts.map(quickAmount => ( + handleQuickAmountPress(quickAmount)} + /> + ))} + + )} + + + + + 1 + 2 + 3 + + + 4 + 5 + 6 + + + 7 + 8 + 9 + + + . + 0 + + + + + + + + {strings('fiat_on_ramp.buy_with')} + + + + + {WYRE_IS_PROMOTION && ( + + {WYRE_FEE_PERCENT}% {strings('fiat_on_ramp.fee')} ( + {strings('fiat_on_ramp.limited_time')}) + + )} + {!WYRE_IS_PROMOTION && ( + <> + {disabledButton ? ( + + + {strings('fiat_on_ramp.Fee')} ~{percentFee}% + ${flatFee} + + + ) : ( + + {strings('fiat_on_ramp.plus_fee', { fee: `$${fee}` })} + + )} + + )} + + + + {strings('fiat_on_ramp.wyre_terms_of_service')} + + + + + + ); +} + +PaymentMethodApplePay.propTypes = { + /** + * Current time to lock screen set in settings + */ + lockTime: PropTypes.number.isRequired, + /** + * Function to change lock screen time setting + */ + setLockTime: PropTypes.func.isRequired, + /** + * Currently selected wallet address + */ + selectedAddress: PropTypes.string.isRequired, + /** + * Currently selected network + */ + network: PropTypes.string.isRequired, + /** + * Function to dispatch adding a new fiat order to the state + */ + addOrder: PropTypes.func.isRequired, + /** + * Prompts protect wallet modal + */ + protectWalletModalVisible: PropTypes.func +}; + +PaymentMethodApplePay.navigationOptions = ({ navigation }) => getPaymentMethodApplePayNavbar(navigation); + +const mapStateToProps = state => ({ + lockTime: state.settings.lockTime, + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + network: state.engine.backgroundState.NetworkController.network +}); + +const mapDispatchToProps = dispatch => ({ + setLockTime: time => dispatch(setLockTime(time)), + addOrder: order => dispatch({ type: 'FIAT_ADD_ORDER', payload: order }), + protectWalletModalVisible: () => dispatch(protectWalletModalVisible()) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(PaymentMethodApplePay); diff --git a/app/components/UI/FiatOrders/PaymentMethodSelector/index.android.js b/app/components/UI/FiatOrders/PaymentMethodSelector/index.android.js new file mode 100644 index 00000000000..dd8e8d5d90c --- /dev/null +++ b/app/components/UI/FiatOrders/PaymentMethodSelector/index.android.js @@ -0,0 +1,49 @@ +import React, { useContext, useCallback } from 'react'; +import { InteractionManager } from 'react-native'; +import PropTypes from 'prop-types'; +import { NavigationContext } from 'react-navigation'; +import { connect } from 'react-redux'; +import { strings } from '../../../../../locales/i18n'; +import Analytics from '../../../../core/Analytics'; +import { ANALYTICS_EVENT_OPTS } from '../../../../util/analytics'; + +import { useTransakFlowURL } from '../orderProcessor/transak'; +import { getPaymentSelectorMethodNavbar } from '../../Navbar'; +import ScreenView from '../components/ScreenView'; +import Title from '../components/Title'; + +import TransakPaymentMethod from './transak'; + +function PaymentMethodSelectorView({ selectedAddress, ...props }) { + const navigation = useContext(NavigationContext); + const transakURL = useTransakFlowURL(selectedAddress); + + const onPressTransak = useCallback(() => { + navigation.navigate('TransakFlow', { + url: transakURL, + title: strings('fiat_on_ramp.transak_webview_title') + }); + InteractionManager.runAfterInteractions(() => { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.PAYMENTS_SELECTS_DEBIT_OR_ACH); + }); + }, [navigation, transakURL]); + + return ( + + + <TransakPaymentMethod onPress={onPressTransak} /> + </ScreenView> + ); +} + +PaymentMethodSelectorView.propTypes = { + selectedAddress: PropTypes.string.isRequired +}; + +PaymentMethodSelectorView.navigationOptions = ({ navigation }) => getPaymentSelectorMethodNavbar(navigation); + +const mapStateToProps = state => ({ + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress +}); + +export default connect(mapStateToProps)(PaymentMethodSelectorView); diff --git a/app/components/UI/FiatOrders/PaymentMethodSelector/index.ios.js b/app/components/UI/FiatOrders/PaymentMethodSelector/index.ios.js new file mode 100644 index 00000000000..3f2c2d98aa1 --- /dev/null +++ b/app/components/UI/FiatOrders/PaymentMethodSelector/index.ios.js @@ -0,0 +1,86 @@ +import React, { useContext, useCallback } from 'react'; +import { InteractionManager } from 'react-native'; +import PropTypes from 'prop-types'; +import { NavigationContext } from 'react-navigation'; +import { connect } from 'react-redux'; +import { strings } from '../../../../../locales/i18n'; +import Analytics from '../../../../core/Analytics'; +import { ANALYTICS_EVENT_OPTS } from '../../../../util/analytics'; + +import { useTransakFlowURL } from '../orderProcessor/transak'; +import { WYRE_IS_PROMOTION } from '../orderProcessor/wyreApplePay'; +import { getPaymentSelectorMethodNavbar } from '../../Navbar'; + +import ScreenView from '../components/ScreenView'; +import Heading from '../components/Heading'; + +import Text from '../../../Base/Text'; +import Title from '../components/Title'; +import SubHeader from '../components/SubHeader'; + +import TransakPaymentMethod from './transak'; +import WyreApplePayPaymentMethod from './wyreApplePay'; + +function PaymentMethodSelectorView({ selectedAddress, network, ...props }) { + const navigation = useContext(NavigationContext); + const transakURL = useTransakFlowURL(selectedAddress); + + const onPressWyreApplePay = useCallback(() => { + navigation.navigate('PaymentMethodApplePay'); + + InteractionManager.runAfterInteractions(() => { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.PAYMENTS_SELECTS_APPLE_PAY); + }); + }, [navigation]); + const onPressTransak = useCallback(() => { + navigation.navigate('TransakFlow', { + url: transakURL, + title: strings('fiat_on_ramp.transak_webview_title') + }); + InteractionManager.runAfterInteractions(() => { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.PAYMENTS_SELECTS_DEBIT_OR_ACH); + }); + }, [navigation, transakURL]); + + return ( + <ScreenView> + <Heading> + <Title centered hero> + {WYRE_IS_PROMOTION ? ( + <> + <Text reset>{strings('fiat_on_ramp.purchase_method_title.wyre_first_line')}</Text> + {'\n'} + <Text reset>{strings('fiat_on_ramp.purchase_method_title.wyre_second_line')}</Text> + </> + ) : ( + <> + <Text reset>{strings('fiat_on_ramp.purchase_method_title.first_line')}</Text> + {'\n'} + <Text reset>{strings('fiat_on_ramp.purchase_method_title.second_line')}</Text> + </> + )} + + {WYRE_IS_PROMOTION && ( + {strings('fiat_on_ramp.purchase_method_title.wyre_sub_header')} + )} + + + + {network === '1' && } + + ); +} + +PaymentMethodSelectorView.propTypes = { + selectedAddress: PropTypes.string.isRequired, + network: PropTypes.string.isRequired +}; + +PaymentMethodSelectorView.navigationOptions = ({ navigation }) => getPaymentSelectorMethodNavbar(navigation); + +const mapStateToProps = state => ({ + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + network: state.engine.backgroundState.NetworkController.network +}); + +export default connect(mapStateToProps)(PaymentMethodSelectorView); diff --git a/app/components/UI/FiatOrders/PaymentMethodSelector/index.js b/app/components/UI/FiatOrders/PaymentMethodSelector/index.js new file mode 100644 index 00000000000..99f858cbe76 --- /dev/null +++ b/app/components/UI/FiatOrders/PaymentMethodSelector/index.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line import/no-unresolved +import PaymentMethodSelector from './PaymentMethodSelector'; + +export default PaymentMethodSelector; diff --git a/app/components/UI/FiatOrders/PaymentMethodSelector/transak.js b/app/components/UI/FiatOrders/PaymentMethodSelector/transak.js new file mode 100644 index 00000000000..50b96ead956 --- /dev/null +++ b/app/components/UI/FiatOrders/PaymentMethodSelector/transak.js @@ -0,0 +1,128 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { TouchableOpacity, Image, StyleSheet, ScrollView, View } from 'react-native'; +import { strings } from '../../../../../locales/i18n'; + +import PaymentMethod from '../components/PaymentMethod'; +import Title from '../../../Base/Title'; +import Text from '../../../Base/Text'; +import ModalHandler from '../../../Base/ModalHandler'; +import StyledButton from '../../StyledButton'; +import Device from '../../../../util/Device'; + +const style = StyleSheet.create({ + logo: { + marginVertical: 5, + aspectRatio: 95 / 25, + width: Device.isIphone5() ? 80 : 95, + height: Device.isIphone5() ? 20 : 25 + }, + cta: { + marginTop: 25, + marginBottom: 5 + }, + countryList: { + flexDirection: 'row' + }, + countryCol: { + width: '50%' + } +}); + +// eslint-disable-next-line import/no-commonjs +const TransakLogoIcon = require('../../../../images/TransakLogo.png'); + +const TransakLogo = () => ; + +const TransakPaymentMethod = ({ onPress }) => ( + + + + {strings('fiat_on_ramp.bank_transfer_debit')} + {strings('fiat_on_ramp.requires_registration')} + {strings('fiat_on_ramp.options_fees_vary')} + + + + + {({ isVisible, toggleModal }) => ( + <> + + + + 33 {strings('fiat_on_ramp.countries')} + + + + + + {strings('fiat_on_ramp.transak_modal_text')} + + + + Austria 🇦🇹 + Australia 🇦🇺 + Belgium 🇧🇪 + Canada 🇨🇦 + Cyprus 🇨🇾 + Czechia 🇨🇿 + Denmark 🇩🇰 + Estonia 🇪🇪 + Finland 🇫🇮 + France 🇫🇷 + Germany 🇩🇪 + Greece 🇬🇷 + Hong Kong 🇭🇰 + Ireland 🇮🇪 + Italy 🇮🇹 + India 🇮🇳 + Latvia 🇱🇻 + + + Luxembourg 🇱🇺 + Malta 🇲🇹 + Mexico 🇲🇽 + Romania 🇷🇴 + Netherlands 🇳🇱 + New Zealand 🇳🇿 + Norway 🇳🇴 + Poland 🇵🇱 + Portugal 🇵🇹 + Slovakia 🇸🇰 + Slovenia 🇸🇮 + Spain 🇪🇸 + Sweden 🇸🇪 + Switzerland 🇨🇭 + United Kingdom 🇬🇧 + USA 🇺🇸 + + + + + + )} + + + + {Device.isAndroid() && ( + + + {strings('fiat_on_ramp.transak_cta')} + + + )} + +); + +TransakPaymentMethod.propTypes = { + onPress: PropTypes.func +}; +TransakPaymentMethod.defaultProps = { + onPress: undefined +}; + +export default TransakPaymentMethod; diff --git a/app/components/UI/FiatOrders/PaymentMethodSelector/wyreApplePay.js b/app/components/UI/FiatOrders/PaymentMethodSelector/wyreApplePay.js new file mode 100644 index 00000000000..5b8159d3cad --- /dev/null +++ b/app/components/UI/FiatOrders/PaymentMethodSelector/wyreApplePay.js @@ -0,0 +1,124 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, Image, TouchableOpacity } from 'react-native'; +import { NavigationContext } from 'react-navigation'; +import { strings } from '../../../../../locales/i18n'; +import { + useWyreTerms, + WYRE_IS_PROMOTION, + WYRE_FEE_PERCENT, + WYRE_FEE_FLAT, + WYRE_REGULAR_FEE_PERCENT, + WYRE_REGULAR_FEE_FLAT +} from '../orderProcessor/wyreApplePay'; + +import PaymentMethod from '../components/PaymentMethod'; + +import ModalHandler from '../../../Base/ModalHandler'; +import Text from '../../../Base/Text'; +import Title from '../components/Title'; + +const logosStyle = StyleSheet.create({ + applePay: { + marginVertical: 3 + } +}); + +/* eslint-disable import/no-commonjs */ +const ApplePayMarkIcon = require('../../../../images/ApplePayMark.png'); +const WyreLogoIcon = require('../../../../images/WyreLogo.png'); +/* eslint-enable import/no-commonjs */ + +const ApplePayMark = () => ; +const WyreLogo = () => ; + +const WyreApplePayPaymentMethod = ({ onPress }) => { + const navigation = useContext(NavigationContext); + const handleWyreTerms = useWyreTerms(navigation); + + return ( + + {strings('fiat_on_ramp.best_deal')} + + + + {strings('fiat_on_ramp.apple_pay')} {strings('fiat_on_ramp.via')}{' '} + + + + {WYRE_IS_PROMOTION ? ( + <> + + ${WYRE_REGULAR_FEE_PERCENT.toFixed(2)} + ${WYRE_REGULAR_FEE_FLAT.toFixed(2)} + {' '} + + {WYRE_FEE_PERCENT}% {strings('fiat_on_ramp.fee')} + + {'\n'} + {strings('fiat_on_ramp.limited_time')} + + ) : ( + + {strings('fiat_on_ramp.Fee')} ~{WYRE_FEE_PERCENT.toFixed(2)}% + $ + {WYRE_FEE_FLAT.toFixed(2)} + + )} + + {strings('fiat_on_ramp.wyre_minutes')} + {strings('fiat_on_ramp.wyre_max')} + {strings('fiat_on_ramp.wyre_requires_debit_card')} + + + + + {({ isVisible, toggleModal }) => ( + <> + + + + {strings('fiat_on_ramp.wyre_us_only')} + + + + + + {strings('fiat_on_ramp.some_states_excluded')} + + + + + + {strings('fiat_on_ramp.wyre_modal_text')}{' '} + { + toggleModal(); + handleWyreTerms(); + }} + > + {strings('fiat_on_ramp.wyre_modal_terms_of_service_apply')} + + + + + )} + + + + + ); +}; + +WyreApplePayPaymentMethod.propTypes = { + onPress: PropTypes.func +}; +WyreApplePayPaymentMethod.defaultProps = { + onPress: undefined +}; +export default WyreApplePayPaymentMethod; diff --git a/app/components/UI/FiatOrders/TransakWebView/index.js b/app/components/UI/FiatOrders/TransakWebView/index.js new file mode 100644 index 00000000000..74a8500c2cc --- /dev/null +++ b/app/components/UI/FiatOrders/TransakWebView/index.js @@ -0,0 +1,70 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { View, InteractionManager } from 'react-native'; +import { connect } from 'react-redux'; +import { WebView } from 'react-native-webview'; +import NotificationManager from '../../../../core/NotificationManager'; +import { handleTransakRedirect } from '../orderProcessor/transak'; +import AppConstants from '../../../../core/AppConstants'; +import { getNotificationDetails } from '..'; + +import { getTransakWebviewNavbar } from '../../../UI/Navbar'; +import { baseStyles } from '../../../../styles/common'; +import { protectWalletModalVisible } from '../../../../actions/user'; + +class TransakWebView extends PureComponent { + static navigationOptions = ({ navigation }) => getTransakWebviewNavbar(navigation); + + static propTypes = { + navigation: PropTypes.object, + /** + * Currently selected network + */ + network: PropTypes.string, + /** + * Function to dispatch adding a new fiat order to the state + */ + addOrder: PropTypes.func, + /** + * Prompts protect wallet modal + */ + protectWalletModalVisible: PropTypes.func + }; + + handleNavigationStateChange = async navState => { + if (navState.url.indexOf(AppConstants.FIAT_ORDERS.TRANSAK_REDIRECT_URL) > -1) { + const order = handleTransakRedirect(navState.url, this.props.network); + this.props.addOrder(order); + this.props.protectWalletModalVisible(); + this.props.navigation.dismiss(); + InteractionManager.runAfterInteractions(() => + NotificationManager.showSimpleNotification(getNotificationDetails(order)) + ); + } + }; + + render() { + const uri = this.props.navigation.getParam('url', null); + if (uri) { + return ( + + + + ); + } + } +} + +const mapStateToProps = state => ({ + network: state.engine.backgroundState.NetworkController.network +}); + +const mapDispatchToProps = dispatch => ({ + addOrder: order => dispatch({ type: 'FIAT_ADD_ORDER', payload: order }), + protectWalletModalVisible: () => dispatch(protectWalletModalVisible()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(TransakWebView); diff --git a/app/components/UI/FiatOrders/components/AccountBar.js b/app/components/UI/FiatOrders/components/AccountBar.js new file mode 100644 index 00000000000..e1f5557bd7c --- /dev/null +++ b/app/components/UI/FiatOrders/components/AccountBar.js @@ -0,0 +1,74 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { View, TouchableOpacity, StyleSheet } from 'react-native'; +import Icon from 'react-native-vector-icons/FontAwesome'; +import { connect } from 'react-redux'; +import { strings } from '../../../../../locales/i18n'; + +import { colors, fontStyles } from '../../../../styles/common'; +import { toggleAccountsModal } from '../../../../actions/modals'; +import EthereumAddress from '../../EthereumAddress'; +import Identicon from '../../Identicon'; +import Text from '../../../Base/Text'; + +const styles = StyleSheet.create({ + container: { + backgroundColor: colors.grey000, + padding: 15, + alignItems: 'center' + }, + addressContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center' + }, + depositingText: { + ...fontStyles.thin, + color: colors.fontPrimary + }, + accountText: { + ...fontStyles.bold, + color: colors.fontPrimary, + marginVertical: 3, + marginHorizontal: 5 + }, + caretDown: { + textAlign: 'right', + color: colors.grey600 + } +}); +const AccountBar = ({ toggleAccountsModal, selectedAddress, identities }) => ( + + <> + + {strings('account_bar.depositing_to')} + + + + + {identities[selectedAddress]?.name} () + + + + + +); + +AccountBar.propTypes = { + toggleAccountsModal: PropTypes.func.isRequired, + selectedAddress: PropTypes.string.isRequired, + identities: PropTypes.object.isRequired +}; + +const mapStateToProps = state => ({ + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + identities: state.engine.backgroundState.PreferencesController.identities +}); + +const mapDispatchToProps = dispatch => ({ + toggleAccountsModal: () => dispatch(toggleAccountsModal()) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(AccountBar); diff --git a/app/components/UI/FiatOrders/components/Heading.js b/app/components/UI/FiatOrders/components/Heading.js new file mode 100644 index 00000000000..393b3c765f0 --- /dev/null +++ b/app/components/UI/FiatOrders/components/Heading.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import Device from '../../../../util/Device'; + +const style = StyleSheet.create({ + view: { + margin: Device.isIphone5() ? 20 : 30 + } +}); + +const Heading = ({ ...props }) => ; + +export default Heading; diff --git a/app/components/UI/FiatOrders/components/InfoIcon.js b/app/components/UI/FiatOrders/components/InfoIcon.js new file mode 100644 index 00000000000..12f34980575 --- /dev/null +++ b/app/components/UI/FiatOrders/components/InfoIcon.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import IonicIcon from 'react-native-vector-icons/Ionicons'; +import Device from '../../../../util/Device'; +import { colors } from '../../../../styles/common'; + +const styles = StyleSheet.create({ + icon: { + color: colors.grey200 + } +}); + +const InfoIcon = props => ( + +); + +export default InfoIcon; diff --git a/app/components/UI/FiatOrders/components/PaymentMethod/Modal.js b/app/components/UI/FiatOrders/components/PaymentMethod/Modal.js new file mode 100644 index 00000000000..558bb06414e --- /dev/null +++ b/app/components/UI/FiatOrders/components/PaymentMethod/Modal.js @@ -0,0 +1,109 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, TouchableOpacity, ScrollView } from 'react-native'; +import { SafeAreaView } from 'react-navigation'; +import Modal from 'react-native-modal'; +import IonicIcon from 'react-native-vector-icons/Ionicons'; +import { strings } from '../../../../../../locales/i18n'; + +import Title from '../Title'; +import { colors } from '../../../../../styles/common'; +import StyledButton from '../../../StyledButton'; + +const styles = StyleSheet.create({ + modalView: { + backgroundColor: colors.white, + justifyContent: 'center', + alignItems: 'center', + marginVertical: 50, + borderRadius: 10, + shadowColor: colors.black, + shadowOffset: { + width: 0, + height: 5 + }, + shadowOpacity: 0.36, + shadowRadius: 6.68, + elevation: 11 + }, + modal: { + margin: 0, + width: '100%', + padding: 25 + }, + title: { + width: '100%', + paddingVertical: 15, + paddingHorizontal: 20, + paddingBottom: 5, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between' + }, + closeIcon: { + color: colors.black + }, + body: { + width: '100%', + paddingVertical: 5, + paddingHorizontal: 20 + }, + action: { + width: '100%', + alignItems: 'center', + marginTop: 15, + paddingVertical: 15, + paddingHorizontal: 20, + borderTopWidth: 1, + borderTopColor: colors.grey100 + }, + button: { + width: '50%' + } +}); + +const CloseIcon = props => ; + +const PaymentMethodModal = ({ isVisible, title, dismiss, children }) => ( + + + + {title} + + + + + + true}>{children} + + + + {strings('fiat_on_ramp.purchase_method_modal_close')} + + + + +); + +PaymentMethodModal.propTypes = { + isVisible: PropTypes.bool, + title: PropTypes.string.isRequired, + dismiss: PropTypes.func, + children: PropTypes.node +}; + +PaymentMethodModal.defaultProps = { + isVisible: false, + dismiss: undefined, + children: undefined +}; + +export default PaymentMethodModal; diff --git a/app/components/UI/FiatOrders/components/PaymentMethod/index.js b/app/components/UI/FiatOrders/components/PaymentMethod/index.js new file mode 100644 index 00000000000..46782c0e7b3 --- /dev/null +++ b/app/components/UI/FiatOrders/components/PaymentMethod/index.js @@ -0,0 +1,98 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { TouchableOpacity, View, StyleSheet } from 'react-native'; +import { colors, fontStyles } from '../../../../../styles/common'; + +import Text from '../../../../Base/Text'; +import InfoIcon from '../InfoIcon'; +import Modal from './Modal'; + +const style = StyleSheet.create({ + container: { + borderWidth: 1, + borderRadius: 8, + borderColor: colors.blue, + paddingVertical: 15, + paddingHorizontal: 20, + marginHorizontal: 25, + marginVertical: 12 + }, + content: { + flexDirection: 'row' + }, + badgeWrapper: { + position: 'absolute', + alignItems: 'center', + top: -14, + left: 0, + right: 0 + }, + badge: { + fontSize: 12, + paddingVertical: 4, + paddingHorizontal: 8, + backgroundColor: colors.blue, + color: colors.white, + margin: 0, + borderRadius: 12, + overflow: 'hidden', + ...fontStyles.bold + }, + details: { + flex: 2 + }, + terms: { + flex: 1, + alignItems: 'flex-end', + justifyContent: 'space-between', + marginLeft: 20 + }, + infoIconLine: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'flex-end' + }, + infoIcon: { + marginLeft: 2 + } +}); + +const PaymentMethod = ({ onPress, ...props }) => ( + +); + +PaymentMethod.propTypes = { + onPress: PropTypes.func, + children: PropTypes.node +}; + +PaymentMethod.defaultProps = { + onPress: undefined, + children: undefined +}; + +const Badge = props => ( + + + +); + +const Content = props => ; +const Details = props => ; +const Terms = props => ; +const InfoIconLine = props => ; + +const PaymentMethodInfoIcon = props => ( + + + +); + +PaymentMethod.Badge = Badge; +PaymentMethod.Content = Content; +PaymentMethod.Details = Details; +PaymentMethod.Terms = Terms; +PaymentMethod.InfoIconLine = InfoIconLine; +PaymentMethod.InfoIcon = PaymentMethodInfoIcon; +PaymentMethod.Modal = Modal; +export default PaymentMethod; diff --git a/app/components/UI/FiatOrders/components/ScreenView.js b/app/components/UI/FiatOrders/components/ScreenView.js new file mode 100644 index 00000000000..131b716ea7b --- /dev/null +++ b/app/components/UI/FiatOrders/components/ScreenView.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { SafeAreaView, StyleSheet, ScrollView } from 'react-native'; +import { colors } from '../../../../styles/common'; + +const styles = StyleSheet.create({ + wrapper: { + backgroundColor: colors.white, + flex: 1 + } +}); + +const ScreenView = props => ( + + + +); + +export default ScreenView; diff --git a/app/components/UI/FiatOrders/components/SubHeader.js b/app/components/UI/FiatOrders/components/SubHeader.js new file mode 100644 index 00000000000..532b19f32d7 --- /dev/null +++ b/app/components/UI/FiatOrders/components/SubHeader.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet } from 'react-native'; +import Text from '../../../Base/Text'; + +const style = StyleSheet.create({ + subHeader: { + margin: 5 + } +}); + +const SubHeader = ({ style: externalStyle, ...props }) => ; + +SubHeader.defaultProps = { + style: undefined +}; +SubHeader.propTypes = { + style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]) +}; + +export default SubHeader; diff --git a/app/components/UI/FiatOrders/components/Title.js b/app/components/UI/FiatOrders/components/Title.js new file mode 100644 index 00000000000..ce54b4a5f39 --- /dev/null +++ b/app/components/UI/FiatOrders/components/Title.js @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet } from 'react-native'; +import BaseTitle from '../../../Base/Title'; + +const style = StyleSheet.create({ + hero: { + margin: 5 + } +}); + +const Title = ({ style: externalStyle, ...props }) => ( + +); + +Title.defaultProps = { + hero: false, + style: undefined +}; + +Title.propTypes = { + hero: PropTypes.bool, + style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]) +}; + +export default Title; diff --git a/app/components/UI/FiatOrders/hooks/useInterval.js b/app/components/UI/FiatOrders/hooks/useInterval.js new file mode 100644 index 00000000000..1f7c2cb475b --- /dev/null +++ b/app/components/UI/FiatOrders/hooks/useInterval.js @@ -0,0 +1,24 @@ +import { useRef, useEffect } from 'react'; + +function useInterval(callback, delay) { + const savedCallback = useRef(); + + // Remember the latest function. + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + // Set up the interval. + useEffect(() => { + function tick() { + savedCallback.current(); + } + if (delay !== null) { + const id = setInterval(tick, delay); + + return () => clearInterval(id); + } + }, [delay]); +} + +export default useInterval; diff --git a/app/components/UI/FiatOrders/index.js b/app/components/UI/FiatOrders/index.js new file mode 100644 index 00000000000..171f066732b --- /dev/null +++ b/app/components/UI/FiatOrders/index.js @@ -0,0 +1,112 @@ +import PropTypes from 'prop-types'; +import { InteractionManager } from 'react-native'; +import { connect } from 'react-redux'; +import Device from '../../../util/Device'; +import AppConstants from '../../../core/AppConstants'; +import NotificationManager from '../../../core/NotificationManager'; +import { strings } from '../../../../locales/i18n'; +import { renderNumber } from '../../../util/number'; + +import { FIAT_ORDER_STATES, getPendingOrders } from '../../../reducers/fiatOrders'; +import useInterval from './hooks/useInterval'; +import processOrder from './orderProcessor'; + +/** + * @typedef {import('../../../reducers/fiatOrders').FiatOrder} FiatOrder + */ + +const POLLING_FREQUENCY = AppConstants.FIAT_ORDERS.POLLING_FREQUENCY; +const NOTIFICATION_DURATION = 5000; + +export const allowedToBuy = network => network === '1' || (network === '42' && Device.isIos()); + +const baseNotificationDetails = { + duration: NOTIFICATION_DURATION +}; + +/** + * @param {FiatOrder} fiatOrder + */ +export const getNotificationDetails = fiatOrder => { + switch (fiatOrder.state) { + case FIAT_ORDER_STATES.FAILED: { + return { + ...baseNotificationDetails, + title: strings('fiat_on_ramp.notifications.purchase_failed_title', { + currency: fiatOrder.cryptocurrency + }), + status: 'error' + }; + } + case FIAT_ORDER_STATES.CANCELLED: { + return { + ...baseNotificationDetails, + title: strings('fiat_on_ramp.notifications.purchase_cancelled_title'), + status: 'cancelled' + }; + } + case FIAT_ORDER_STATES.COMPLETED: { + return { + ...baseNotificationDetails, + title: strings('fiat_on_ramp.notifications.purchase_completed_title', { + amount: renderNumber(String(fiatOrder.cryptoAmount)), + currency: fiatOrder.cryptocurrency + }), + description: strings('fiat_on_ramp.notifications.purchase_completed_description', { + currency: fiatOrder.cryptocurrency + }), + status: 'success' + }; + } + case FIAT_ORDER_STATES.PENDING: + default: { + return { + ...baseNotificationDetails, + title: strings('fiat_on_ramp.notifications.purchase_pending_title', { + currency: fiatOrder.cryptocurrency + }), + description: strings('fiat_on_ramp.notifications.purchase_pending_description'), + status: 'pending' + }; + } + } +}; + +function FiatOrders({ pendingOrders, dispatch }) { + useInterval( + async () => { + await Promise.all( + pendingOrders.map(async order => { + const updatedOrder = await processOrder(order); + dispatch({ type: 'FIAT_UPDATE_ORDER', payload: updatedOrder }); + if (updatedOrder.state !== order.state) { + InteractionManager.runAfterInteractions(() => + NotificationManager.showSimpleNotification(getNotificationDetails(updatedOrder)) + ); + } + }) + ); + }, + pendingOrders.length ? POLLING_FREQUENCY : null + ); + + return null; +} + +FiatOrders.propTypes = { + orders: PropTypes.array, + selectedAddress: PropTypes.string, + network: PropTypes.string, + dispatch: PropTypes.func +}; + +const mapStateToProps = state => { + const orders = state.fiatOrders.orders; + const selectedAddress = state.engine.backgroundState.PreferencesController.selectedAddress; + const network = state.engine.backgroundState.NetworkController.network; + return { + pendingOrders: getPendingOrders(orders, selectedAddress, network) + }; +}; + +export default connect(mapStateToProps)(FiatOrders); diff --git a/app/components/UI/FiatOrders/orderProcessor/index.js b/app/components/UI/FiatOrders/orderProcessor/index.js new file mode 100644 index 00000000000..86c85b7435f --- /dev/null +++ b/app/components/UI/FiatOrders/orderProcessor/index.js @@ -0,0 +1,21 @@ +import { FIAT_ORDER_PROVIDERS } from '../../../../reducers/fiatOrders'; +import { processWyreApplePayOrder } from './wyreApplePay'; +import { processTransakOrder } from './transak'; +import Logger from '../../../../util/Logger'; + +function processOrder(order) { + switch (order.provider) { + case FIAT_ORDER_PROVIDERS.WYRE_APPLE_PAY: { + return processWyreApplePayOrder(order); + } + case FIAT_ORDER_PROVIDERS.TRANSAK: { + return processTransakOrder(order); + } + default: { + Logger.error('FiatOrders::ProcessOrder unrecognized provider', order); + return order; + } + } +} + +export default processOrder; diff --git a/app/components/UI/FiatOrders/orderProcessor/transak.js b/app/components/UI/FiatOrders/orderProcessor/transak.js new file mode 100644 index 00000000000..0f3d54aad7e --- /dev/null +++ b/app/components/UI/FiatOrders/orderProcessor/transak.js @@ -0,0 +1,243 @@ +import { useMemo } from 'react'; +import axios from 'axios'; +import qs from 'query-string'; +import AppConstants from '../../../../core/AppConstants'; +import { FIAT_ORDER_PROVIDERS, FIAT_ORDER_STATES } from '../../../../reducers/fiatOrders'; +import Logger from '../../../../util/Logger'; + +//* env vars + +const TRANSAK_API_KEY_STAGING = process.env.TRANSAK_API_KEY_STAGING; +const TRANSAK_API_KEY_SECRET_STAGING = process.env.TRANSAK_API_KEY_SECRET_STAGING; +const TRANSAK_API_KEY_PRODUCTION = process.env.TRANSAK_API_KEY_PRODUCTION; +const TRANSAK_API_KEY_SECRET_PRODUCTION = process.env.TRANSAK_API_KEY_SECRET_PRODUCTION; + +//* typedefs + +/** + * @typedef {import('../../../../reducers/fiatOrders').FiatOrder} FiatOrder + */ + +/** + * @typedef TransakOrder + * @type {object} + * @property {string} id + * @property {string} createdAt + * @property {string} updatedAt + * @property {string} completedAt + * @property {string} fiatCurrency + * @property {string} cryptocurrency + * @property {number} fiatAmount + * @property {string} walletLink + * @property {string} paymentOptionId Paymenth method ID, see: https://integrate.transak.com/Coverage-Payment-Methods-Fees-Limits-30c0954fbdf04beca68622d9734c59f9 + * @property {boolean} addressAdditionalData + * @property {string} network this is NOT ethernet networks id + * @property {string} amountPaid + * @property {number} referenceCode + * @property {string} redirectURL Our redirect URL + * @property {number} conversionPrice + * @property {number} cryptoAmount + * @property {number} totalFeeInCrypto + * @property {number} totalFeeInFiat + * @property {array} paymentOption + * @property {TRANSAK_ORDER_STATES} status + * @property {string} walletAddress + * @property {string} autoExpiresAt + * @property {string} fromWalletAddress + * @property {string} transactionHash + * @property {string} transactionLink + */ + +/** + * Query params added by Transak when redirecting after completing flow + * https://integrate.transak.com/Query-Parameters-9ec523df3b874ec58cef4fa3a906f238?p=d3edbf3a682d403daceee3249e8aea49 + * @typedef TransakRedirectOrder + * @type {object} + * @property {string} orderId + * @property {string} fiatCurrency + * @property {string} cryptocurrency + * @property {string} fiatAmount + * @property {string} cryptoAmount + * @property {string} isBuyOrSell + * @property {string} status + * @property {string} walletAddress + * @property {string} totalFee + * @property {string} partnerCustomerId + * @property {string} partnerOrderId + */ + +//* Constants + +const { + TRANSAK_URL, + TRANSAK_URL_STAGING, + TRANSAK_API_URL_STAGING, + TRANSAK_API_URL_PRODUCTION, + TRANSAK_REDIRECT_URL +} = AppConstants.FIAT_ORDERS; + +const isDevelopment = process.env.NODE_ENV !== 'production'; + +const TRANSAK_API_BASE_URL = `${isDevelopment ? TRANSAK_API_URL_STAGING : TRANSAK_API_URL_PRODUCTION}api/v1/`; +const TRANSAK_API_KEY = isDevelopment ? TRANSAK_API_KEY_STAGING : TRANSAK_API_KEY_PRODUCTION; +const TRANSAK_API_KEY_SECRET = isDevelopment ? TRANSAK_API_KEY_SECRET_STAGING : TRANSAK_API_KEY_SECRET_PRODUCTION; + +/** + * https://integrate.transak.com/69a2474c8d8d40daa04bd5bbe804fb6d?v=48a0c9fd98854078a4eaf5ec9a0a4f65 + * @enum {string} + */ +export const TRANSAK_ORDER_STATES = { + AWAITING_PAYMENT_FROM_USER: 'AWAITING_PAYMENT_FROM_USER', + PAYMENT_DONE_MARKED_BY_USER: 'PAYMENT_DONE_MARKED_BY_USER', + PROCESSING: 'PROCESSING', + PENDING_DELIVERY_FROM_TRANSAK: 'PENDING_DELIVERY_FROM_TRANSAK', + COMPLETED: 'COMPLETED', + EXPIRED: 'EXPIRED', + FAILED: 'FAILED', + CANCELLED: 'CANCELLED' +}; + +//* API + +const transakApi = axios.create({ + baseURL: TRANSAK_API_BASE_URL +}); + +// const getPartnerStatus = () => transakApi.get(`partners/${TRANSAK_API_KEY}`); +const getOrderStatus = orderId => + transakApi.get(`partners/order/${orderId}`, { params: { partnerAPISecret: TRANSAK_API_KEY_SECRET } }); + +//* Helpers + +/** + * Transforms a TransakOrder state into a FiatOrder state + * @param {TRANSAK_ORDER_STATES} transakOrderState + */ +const transakOrderStateToFiatOrderState = transakOrderState => { + switch (transakOrderState) { + case TRANSAK_ORDER_STATES.COMPLETED: { + return FIAT_ORDER_STATES.COMPLETED; + } + case TRANSAK_ORDER_STATES.EXPIRED: + case TRANSAK_ORDER_STATES.FAILED: { + return FIAT_ORDER_STATES.FAILED; + } + case TRANSAK_ORDER_STATES.CANCELLED: { + return FIAT_ORDER_STATES.CANCELLED; + } + case TRANSAK_ORDER_STATES.AWAITING_PAYMENT_FROM_USER: + case TRANSAK_ORDER_STATES.PAYMENT_DONE_MARKED_BY_USER: + case TRANSAK_ORDER_STATES.PROCESSING: + case TRANSAK_ORDER_STATES.PENDING_DELIVERY_FROM_TRANSAK: + default: { + return FIAT_ORDER_STATES.PENDING; + } + } +}; + +/** + * Transforms Transak order object into a Fiat order object used in the state. + * @param {TransakOrder} transakOrder Transak order object + * @returns {FiatOrder} Fiat order object to store in the state + */ +const transakOrderToFiatOrder = transakOrder => ({ + id: transakOrder.id, + provider: FIAT_ORDER_PROVIDERS.TRANSAK, + createdAt: new Date(transakOrder.createdAt).getTime(), + amount: transakOrder.fiatAmount, + fee: transakOrder.totalFeeInFiat, + cryptoAmount: transakOrder.cryptoAmount, + cryptoFee: transakOrder.totalFeeInCrypto, + currency: transakOrder.fiatCurrency, + cryptocurrency: transakOrder.cryptocurrency, + state: transakOrderStateToFiatOrderState(transakOrder.status), + account: transakOrder.walletAddress, + txHash: transakOrder.transactionHash || null, + data: transakOrder +}); + +/** + * Transforms Transak order object into a Fiat order object used in the state. + * @param {TransakRedirectOrder} transakRedirectOrder Transak order object + * @returns {FiatOrder} Fiat order object to store in the state + */ +const transakCallbackOrderToFiatOrder = transakRedirectOrder => ({ + id: transakRedirectOrder.orderId, + provider: FIAT_ORDER_PROVIDERS.TRANSAK, + createdAt: Date.now(), + amount: Number(transakRedirectOrder.fiatAmount), + fee: Number(transakRedirectOrder.totalFee), + currency: transakRedirectOrder.fiatCurrency, + cryptoAmount: transakRedirectOrder.cryptoAmount, + cryptocurrency: transakRedirectOrder.cryptocurrency, + state: transakOrderStateToFiatOrderState(transakRedirectOrder.status), + account: transakRedirectOrder.walletAddress, + data: transakRedirectOrder +}); + +//* Handlers + +/** + * Function to handle Transak flow redirect after order creation + * @param {String} url Custom URL with query params transak flow redirected to. + * Query parameters are: `orderId`, `fiatCurrency`, `cryptocurrency`, `fiatAmount`, + * `cryptoAmount`, `isBuyOrSell`, `status`, `walletAddress`, + * `totalFee`, `partnerCustomerId`, `partnerOrderId`. + * @param {String} network Current network selected in the app + * @returns {FiatOrder} + */ +export const handleTransakRedirect = (url, network) => { + /** @type {TransakRedirectOrder} */ + const data = qs.parse(url.split(TRANSAK_REDIRECT_URL)[1]); + const order = { ...transakCallbackOrderToFiatOrder(data), network }; + return order; +}; + +/** + * Function used to poll and update the order + * @param {FiatOrder} order Order coming from the state + * @param {TransakOrder} order.data Original Transak order + * @returns {FiatOrder} Fiat order to update in the state + */ +export async function processTransakOrder(order) { + try { + const { + data: { response } + } = await getOrderStatus(order.id); + if (!response) { + throw new Error('Payment Request Failed: empty transak response'); + } + return { + ...order, + ...transakOrderToFiatOrder(response) + }; + } catch (error) { + Logger.error(error, { message: 'FiatOrders::TransakProcessor error while processing order', order }); + return order; + } +} + +//* Hooks + +export const useTransakFlowURL = address => { + const params = useMemo( + () => + qs.stringify({ + apiKey: TRANSAK_API_KEY, + // cryptoCurrencyCode: 'ETH', + themeColor: '037dd6', + // fiatCurrency: 'USD', + walletAddressesData: JSON.stringify({ + networks: { + erc20: { address } + }, + coins: { + DAI: { address } + } + }), + redirectURL: TRANSAK_REDIRECT_URL + }), + [address] + ); + return `${isDevelopment ? TRANSAK_URL_STAGING : TRANSAK_URL}?${params}`; +}; diff --git a/app/components/UI/FiatOrders/orderProcessor/wyreApplePay.js b/app/components/UI/FiatOrders/orderProcessor/wyreApplePay.js new file mode 100644 index 00000000000..fc47aee5359 --- /dev/null +++ b/app/components/UI/FiatOrders/orderProcessor/wyreApplePay.js @@ -0,0 +1,444 @@ +import { useCallback, useMemo, useState, useEffect } from 'react'; +import { PaymentRequest } from '@exodus/react-native-payments'; +import axios from 'axios'; +import AppConstants from '../../../../core/AppConstants'; +import Logger from '../../../../util/Logger'; +import { strings } from '../../../../../locales/i18n'; +import { FIAT_ORDER_PROVIDERS, FIAT_ORDER_STATES } from '../../../../reducers/fiatOrders'; + +//* env vars + +const WYRE_ACCOUNT_ID = process.env.WYRE_ACCOUNT_ID; +const WYRE_ACCOUNT_ID_TEST = process.env.WYRE_ACCOUNT_ID_TEST; + +//* typedefs + +/** + * @typedef {import('../../../../reducers/fiatOrders').FiatOrder} FiatOrder + */ + +/** + * Wyre API errors. + * Source: https://docs.sendwyre.com/docs/errors + * @typedef WyreError + * @property {string} exceptionId A unique identifier for this exception. This is very helpful when contacting support + * @property {WYRE_ERROR_TYPE} type The category of the exception. See below + * @property {string} errorCode A more granular specification than type + * @property {string} message A human-friendly description of the problem + * @property {string} language Indicates the language of the exception message + * @property {boolean} transient In rare cases, an exception may signal true here to indicate a transient problem. This means the request can be safely re-attempted + * + */ + +/** + * @enum {string} + */ +export const WYRE_ERROR_TYPE = { + ValidationException: 'ValidationException', // The action failed due to problems with the request. 400 + InsufficientFundsException: 'InsufficientFundsException', // You requested the use of more funds in the specified currency than were available. 400 + AccessDeniedException: 'AccessDeniedException', // You lack sufficient privilege to perform the requested. action 401 + TransferException: 'TransferException', // There was a problem completing your transfer request. 400 + MFARequiredException: 'MFARequiredException', // An MFA action is required to complete the request. In general you should not get this exception while using API keys. 400 + CustomerSupportException: 'CustomerSupportException', // Please contact us at support@sendwyre.com to resolve this! 400 + NotFoundException: 'NotFoundException', // You referenced something that could not be located. 404 + RateLimitException: 'RateLimitException', // Your requests have exceeded your usage restrictions. Please contact us if you need this increased. 429 + AccountLockedException: 'AccountLockedException', // The account has had a locked placed on it for potential fraud reasons. The customer should contact Wyre support for follow-up. 400 + LockoutException: 'LockoutException', // The account or IP has been blocked due to detected malicious behavior. 403 + UnknownException: 'UnknownException' // A problem with our services internally. This should rarely happen. 500 +}; + +/** + * https://docs.sendwyre.com/docs/apple-pay-order-integration + * + * @typedef WyreOrder + * @property {string} id Wallet order id eg: "WO_ELTUVYCAFPG" + * @property {number} createdAt Timestamp in UTC eg: 1576263687643 + * @property {string} owner Owner eg: "account:AC_RNWQNRAZFPC" + * @property {WYRE_ORDER_STATES} status Order status eg: "PROCESSING", + * @property {string?} transferId Transfer id or null eg: "TF_MDA6MAY848D", + * @property {number} sourceAmount Fiat amount of order eg: 1.84, + * @property {string} sourceCurrency Fiat currency of order eg: "USD", + * @property {string} destCurrency Crypto currency eg: "ETH", + * @property {string} dest Destination of transfer eg: "ethereum:0x9E01E0E60dF079136a7a1d4ed97d709D5Fe3e341", + * @property {string} walletType Wallet type eg: "APPLE_PAY", + * @property {string} email Customer email eg: "user@company.com", + * @property {string?} errorMessage Error message null, + * @property {string} accountId Account ID: "AC_RNWQNRAZFPC", + * @property {string} paymentMethodName Display "Visa 2942" + */ + +/** + * https://docs.sendwyre.com/docs/wallet-order-processing + * @enum {string} + */ +export const WYRE_ORDER_STATES = { + RUNNING_CHECKS: 'RUNNING_CHECKS', + FAILED: 'FAILED', + PROCESSING: 'PROCESSING', + COMPLETE: 'COMPLETE' +}; + +/** + * @typedef WyreTransfer + * + * @property {string} transferId Transfer ID eg:"TF_MDA6MAY848D" + * @property {string} feeCurrency Fee currency "USD" + * @property {number} fee Fee + * @property {object} fees Fees object + * @property {number} fees.ETH Fees in ETH + * @property {number} fees.USD Fees in USD + * @property {string} sourceCurrency Source currency eg: "USD" + * @property {string} destCurrency eg: "ETH" + * @property {number} sourceAmount Source amount eg: 1.84 + * @property {number} destAmount Dest amount eg: 0.001985533306564713 + * @property {string} destSrn Destination address eg: "ethereum:0x9E01E0E60dF079136a7a1d4ed97d709D5Fe3e341" + * @property {string} from eg: "Walletorderholding WO_ELTUVYCAFPG" + * @property {string} to + * @property {number} rate rate eg: 0.0019760479041916164 + * @property {string?} customId customId eg:null + * @property {string} status status eg:COMPLETED + * @property {string?} blockchainNetworkTx Transfer transaction hash + * @property {string?} message + * @property {string} transferHistoryEntryType eg: "OUTGOING" + * @property {Array.<{statusDetail: string, state: string, createdAt: number}>} successTimeline + * @property {Array.<{statusDetail: string, state: string, createdAt: number}>} failedTimeline + * @property {string?} failureReason + * @property {string?} reversalReason + */ + +//* Constants */ + +const { WYRE_MERCHANT_ID, WYRE_MERCHANT_ID_TEST, WYRE_API_ENDPOINT, WYRE_API_ENDPOINT_TEST } = AppConstants.FIAT_ORDERS; +export const WYRE_IS_PROMOTION = false; +export const WYRE_REGULAR_FEE_PERCENT = 2.9; +export const WYRE_REGULAR_FEE_FLAT = 0.3; +export const WYRE_FEE_PERCENT = WYRE_IS_PROMOTION ? 0 : WYRE_REGULAR_FEE_PERCENT; +export const WYRE_FEE_FLAT = WYRE_IS_PROMOTION ? 0 : WYRE_REGULAR_FEE_FLAT; + +const getMerchantIdentifier = network => (network === '1' ? WYRE_MERCHANT_ID : WYRE_MERCHANT_ID_TEST); +const getPartnerId = network => (network === '1' ? WYRE_ACCOUNT_ID : WYRE_ACCOUNT_ID_TEST); + +//* API */ + +const wyreAPI = axios.create({ + baseURL: WYRE_API_ENDPOINT +}); + +const wyreTestAPI = axios.create({ + baseURL: WYRE_API_ENDPOINT_TEST +}); + +const getRates = network => (network === '1' ? wyreAPI : wyreTestAPI).get(`v3/rates`, { params: { as: 'PRICED' } }); +const createFiatOrder = (network, payload) => + (network === '1' ? wyreAPI : wyreTestAPI).post('v3/apple-pay/process/partner', payload, { + // * This promise will always be resolved, use response.status to handle errors + validateStatus: status => status >= 200, + // * Apple Pay timeouts at ~30s without throwing error, we want to catch that before and throw + timeout: 25000 + }); +const getOrderStatus = (network, orderId) => (network === '1' ? wyreAPI : wyreTestAPI).get(`v3/orders/${orderId}`); +const getTransferStatus = (network, transferId) => + (network === '1' ? wyreAPI : wyreTestAPI).get(`v2/transfer/${transferId}/track`); + +//* Helpers + +const destToAddress = dest => (dest.indexOf('ethereum:') === 0 ? dest.substring(9) : dest); + +export class WyreException extends Error { + /** + * Creates a WyreException based on a WyreError + * @param {string} message + * @param {WYRE_ERROR_TYPE} type + * @param {string} exceptionId + */ + constructor(message, type, exceptionId) { + super(message); + this.type = type; + this.id = exceptionId; + } +} + +/** + * Transforms a WyreOrder state into a FiatOrder state + * @param {WYRE_ORDER_STATES} wyreOrderState + */ +const wyreOrderStateToFiatState = wyreOrderState => { + switch (wyreOrderState) { + case WYRE_ORDER_STATES.COMPLETE: { + return FIAT_ORDER_STATES.COMPLETED; + } + case WYRE_ORDER_STATES.FAILED: { + return FIAT_ORDER_STATES.FAILED; + } + case WYRE_ORDER_STATES.RUNNING_CHECKS: + case WYRE_ORDER_STATES.PROCESSING: + default: { + return FIAT_ORDER_STATES.PENDING; + } + } +}; + +/** + * Transforms Wyre order object into a Fiat order object used in the state. + * @param {WyreOrder} wyreOrder Wyre order object + * @returns {FiatOrder} Fiat order object to store in the state + */ +const wyreOrderToFiatOrder = wyreOrder => ({ + id: wyreOrder.id, + provider: FIAT_ORDER_PROVIDERS.WYRE_APPLE_PAY, + createdAt: wyreOrder.createdAt, + amount: wyreOrder.sourceAmount, + fee: null, + cryptoAmount: null, + cryptoFee: null, + currency: wyreOrder.sourceCurrency, + cryptocurrency: wyreOrder.destCurrency, + state: wyreOrderStateToFiatState(wyreOrder.status), + account: destToAddress(wyreOrder.dest), + txHash: null, + data: { + order: wyreOrder + } +}); + +/** + * Returns fields present in a WyreTransfer which are not + * present in a WyreOrder to be assigned in a FiatOrder + * @param {WyreTransfer} wyreTransfer Wyre transfer object + * @returns {FiatOrder} Partial fiat order object to store in the state + */ +const wyreTransferToFiatOrder = wyreTransfer => ({ + fee: wyreTransfer.fee, + cryptoAmount: wyreTransfer.destAmount, + cryptoFee: wyreTransfer.fees ? wyreTransfer.fees[wyreTransfer.destCurrency] : null, + txHash: wyreTransfer.blockchainNetworkTx +}); + +//* Handlers + +export async function processWyreApplePayOrder(order) { + try { + const { data } = await getOrderStatus(order.network, order.id); + if (!data) { + Logger.error('FiatOrders::WyreApplePayProcessor empty data', order); + return order; + } + + const { transferId } = data; + + if (transferId) { + try { + const transferResponse = await getTransferStatus(order.network, transferId); + if (transferResponse.data) { + return { + ...order, + ...wyreOrderToFiatOrder(data), + ...wyreTransferToFiatOrder(transferResponse.data), + data: { + order: data, + transfer: transferResponse.data + } + }; + } + } catch (error) { + Logger.error(error, { + message: 'FiatOrders::WyreApplePayProcessor error while processing transfer', + order + }); + } + } + + return { + ...order, + ...wyreOrderToFiatOrder(data) + }; + } catch (error) { + Logger.error(error, { message: 'FiatOrders::WyreApplePayProcessor error while processing order', order }); + return order; + } +} + +//* Payment Request */ + +const USD_CURRENCY_CODE = 'USD'; +const ETH_CURRENCY_CODE = 'ETH'; + +const ABORTED = 'ABORTED'; + +const PAYMENT_REQUEST_COMPLETE = { + SUCCESS: 'success', + UNKNOWN: 'unknown', + FAIL: 'fail' +}; + +const getMethodData = network => [ + { + supportedMethods: ['apple-pay'], + data: { + countryCode: 'US', + currencyCode: USD_CURRENCY_CODE, + supportedNetworks: ['visa', 'mastercard', 'discover'], + merchantIdentifier: getMerchantIdentifier(network) + } + } +]; + +const getPaymentDetails = (cryptoCurrency, amount, fee, total) => ({ + displayItems: [ + { + amount: { currency: USD_CURRENCY_CODE, value: `${amount}` }, + label: strings('fiat_on_ramp.wyre_purchase', { currency: cryptoCurrency }) + }, + { + amount: { currency: USD_CURRENCY_CODE, value: `${fee}` }, + label: strings('fiat_on_ramp.Fee') + } + ], + total: { + amount: { currency: USD_CURRENCY_CODE, value: `${total}` }, + label: 'Wyre' + } +}); + +const paymentOptions = { + requestPayerPhone: true, + requestPayerEmail: true, + requestBilling: true, + merchantCapabilities: ['debit'] +}; + +const createPayload = (network, amount, address, paymentDetails) => { + const { + billingContact: { postalAddress, name }, + paymentData, + paymentMethod, + shippingContact, + transactionIdentifier + } = paymentDetails; + const dest = `ethereum:${address}`; + + const formattedBillingContact = { + addressLines: postalAddress.street.split('\n'), + administrativeArea: postalAddress.state, + country: postalAddress.country, + countryCode: postalAddress.ISOCountryCode, + familyName: name.familyName, + givenName: name.givenName, + locality: postalAddress.city, + postalCode: postalAddress.postalCode, + subAdministrativeArea: postalAddress.subAdministrativeArea, + subLocality: postalAddress.subLocality + }; + + const formattedShippingContact = { + ...formattedBillingContact, + emailAddress: shippingContact.emailAddress, + phoneNumber: shippingContact.phoneNumber + }; + + const partnerId = getPartnerId(network); + + return { + partnerId, + payload: { + orderRequest: { + amount, + dest, + destCurrency: ETH_CURRENCY_CODE, + referrerAccountId: partnerId, + sourceCurrency: USD_CURRENCY_CODE + }, + paymentObject: { + billingContact: formattedBillingContact, + shippingContact: formattedShippingContact, + token: { + paymentData, + paymentMethod: { + ...paymentMethod, + type: 'debit' + }, + transactionIdentifier + } + } + } + }; +}; + +// * Hooks */ + +export function useWyreRates(network, currencies) { + const [rates, setRates] = useState(null); + + useEffect(() => { + async function getWyreRates() { + try { + const { data } = await getRates(network); + const rates = data[currencies]; + setRates(rates); + } catch (error) { + Logger.error(error, 'FiatOrders::WyreAppleyPay error while fetching wyre rates'); + } + } + getWyreRates(); + }, [currencies, network]); + + return rates; +} + +export function useWyreApplePay(amount, address, network) { + const flatFee = useMemo(() => WYRE_FEE_FLAT.toFixed(2), []); + const percentFee = useMemo(() => WYRE_FEE_PERCENT.toFixed(2), []); + const percentFeeAmount = useMemo(() => ((Number(amount) * Number(percentFee)) / 100).toFixed(2), [ + amount, + percentFee + ]); + const fee = useMemo(() => (Number(percentFeeAmount) + Number(flatFee)).toFixed(2), [flatFee, percentFeeAmount]); + const total = useMemo(() => Number(amount) + Number(fee), [amount, fee]); + const methodData = useMemo(() => getMethodData(network), [network]); + const paymentDetails = useMemo(() => getPaymentDetails(ETH_CURRENCY_CODE, amount, fee, total), [ + amount, + fee, + total + ]); + + const showRequest = useCallback(async () => { + const paymentRequest = new PaymentRequest(methodData, paymentDetails, paymentOptions); + try { + const paymentResponse = await paymentRequest.show(); + if (!paymentResponse) { + throw new Error('Payment Request Failed: empty apple pay response'); + } + const payload = createPayload(network, total, address, paymentResponse.details); + const { data, status } = await createFiatOrder(network, payload); + if (status >= 200 && status < 300) { + paymentResponse.complete(PAYMENT_REQUEST_COMPLETE.SUCCESS); + return { ...wyreOrderToFiatOrder(data), network }; + } + paymentResponse.complete(PAYMENT_REQUEST_COMPLETE.FAIL); + throw new WyreException(data.message, data.type, data.exceptionId); + } catch (error) { + if (error.message.includes('AbortError')) { + return ABORTED; + } + if (paymentRequest && paymentRequest.abort) { + paymentRequest.abort(); + } + Logger.error(error, { message: 'FiatOrders::WyreApplePayPayment error while creating order' }); + throw error; + } + }, [address, methodData, network, paymentDetails, total]); + + return [showRequest, ABORTED, percentFee, flatFee, percentFeeAmount, fee, total]; +} + +export function useWyreTerms(navigation) { + const handleWyreTerms = useCallback( + () => + navigation.navigate('Webview', { + url: 'https://www.sendwyre.com/user-agreement/', + title: strings('fiat_on_ramp.wyre_user_agreement') + }), + [navigation] + ); + return handleWyreTerms; +} diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index 6958f9b42ee..ab48d9816f8 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -133,7 +133,7 @@ const styles = StyleSheet.create({ metamaskNameWrapper: { marginLeft: Device.isAndroid() ? 20 : 0 }, - webviewTitle: { + centeredTitle: { fontSize: 20, color: colors.fontPrimary, textAlign: 'center', @@ -797,7 +797,7 @@ export function getWebviewNavbar(navigation) { ''; }); return { - headerTitle: {title}, + headerTitle: {title}, headerLeft: Device.isAndroid() ? ( // eslint-disable-next-line react/jsx-no-bind navigation.pop()} style={styles.backButton}> @@ -822,3 +822,69 @@ export function getWebviewNavbar(navigation) { ) }; } + +export function getPaymentSelectorMethodNavbar(navigation) { + const rightAction = navigation.dismiss; + + return { + headerTitle: {strings('fiat_on_ramp.purchase_method')}, + headerLeft: , + headerRight: ( + // eslint-disable-next-line react/jsx-no-bind + + {strings('navigation.cancel')} + + ) + }; +} + +export function getPaymentMethodApplePayNavbar(navigation) { + return { + title: strings('fiat_on_ramp.amount_to_buy'), + headerTitleStyle: { + fontSize: 20, + color: colors.fontPrimary, + ...fontStyles.normal + }, + headerRight: ( + // eslint-disable-next-line react/jsx-no-bind + navigation.dismiss()} style={styles.closeButton}> + {strings('navigation.cancel')} + + ), + headerLeft: Device.isAndroid() ? ( + // eslint-disable-next-line react/jsx-no-bind + navigation.pop()} style={styles.backButton}> + + + ) : ( + // eslint-disable-next-line react/jsx-no-bind + navigation.pop()} style={styles.closeButton}> + {strings('navigation.back')} + + ) + }; +} + +export function getTransakWebviewNavbar(navigation) { + const title = navigation.getParam('title', ''); + return { + title, + headerTitleStyle: { + fontSize: 20, + color: colors.fontPrimary, + ...fontStyles.normal + }, + headerLeft: Device.isAndroid() ? ( + // eslint-disable-next-line react/jsx-no-bind + navigation.pop()} style={styles.backButton}> + + + ) : ( + // eslint-disable-next-line react/jsx-no-bind + navigation.pop()} style={styles.backButton}> + + + ) + }; +} diff --git a/app/components/UI/ReceiveRequest/ReceiveRequestAction/__snapshots__/index.test.js.snap b/app/components/UI/ReceiveRequest/ReceiveRequestAction/__snapshots__/index.test.js.snap deleted file mode 100644 index ca0c22c7eb7..00000000000 --- a/app/components/UI/ReceiveRequest/ReceiveRequestAction/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,77 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReceiveRequestAction should render correctly 1`] = ` - - - - - Title - - - - - Description - - - -`; diff --git a/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.js b/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.js deleted file mode 100644 index b8671e4ff0c..00000000000 --- a/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.js +++ /dev/null @@ -1,83 +0,0 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { TouchableOpacity, StyleSheet, View, Text } from 'react-native'; -import { fontStyles, colors } from '../../../../styles/common'; -import { connect } from 'react-redux'; - -const styles = StyleSheet.create({ - wrapper: { - margin: 8, - borderWidth: 1, - borderColor: colors.blue, - borderRadius: 8, - padding: 10, - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center' - }, - title: { - ...fontStyles.bold, - fontSize: 14, - padding: 5 - }, - description: { - ...fontStyles.normal, - fontSize: 12, - padding: 5, - textAlign: 'center', - color: colors.grey500 - }, - row: { - alignSelf: 'center' - }, - icon: { - marginBottom: 5 - } -}); - -/** - * PureComponent that renders a receive action - */ -class ReceiveRequestAction extends PureComponent { - static propTypes = { - /** - * The navigator object - */ - icon: PropTypes.object, - /** - * Action title - */ - actionTitle: PropTypes.string, - /** - * Action description - */ - actionDescription: PropTypes.string, - /** - * Custom style - */ - style: PropTypes.object, - /** - * Callback on press action - */ - onPress: PropTypes.func - }; - - render() { - const { icon, actionTitle, actionDescription, style, onPress } = this.props; - return ( - - {icon} - - {actionTitle} - - - - {actionDescription} - - - - ); - } -} - -export default connect()(ReceiveRequestAction); diff --git a/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.test.js b/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.test.js deleted file mode 100644 index c26149835b8..00000000000 --- a/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import ReceiveRequestAction from './'; -import configureMockStore from 'redux-mock-store'; - -const mockStore = configureMockStore(); - -describe('ReceiveRequestAction', () => { - it('should render correctly', () => { - const initialState = {}; - - const wrapper = shallow(, { - context: { store: mockStore(initialState) } - }); - expect(wrapper.dive()).toMatchSnapshot(); - }); -}); diff --git a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap index 445e3cc01a1..55478afeac0 100644 --- a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap +++ b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap @@ -37,14 +37,26 @@ exports[`ReceiveRequest should render correctly 1`] = ` Receive @@ -59,243 +72,155 @@ exports[`ReceiveRequest should render correctly 1`] = ` - + + + + Scan address to receive payment + + - - } - onPress={[Function]} + + + + - - } + upper={false} + > + Copy + + - + > + + + - - } - onPress={[Function]} - style={ + - + disabledContainerStyle={ + Object { + "opacity": 0.6, + } } onPress={[Function]} - style={ + styleDisabled={ Object { - "flex": 1, - "height": 343, - "width": 343, + "opacity": 0.6, } } - /> - - - - - - - + Buy ETH + + - - - - - Coming soon... - - - + Request Payment + + + + `; diff --git a/app/components/UI/ReceiveRequest/index.js b/app/components/UI/ReceiveRequest/index.js index 4e661aad874..70145817902 100644 --- a/app/components/UI/ReceiveRequest/index.js +++ b/app/components/UI/ReceiveRequest/index.js @@ -1,28 +1,31 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { InteractionManager, SafeAreaView, Dimensions, StyleSheet, View, Text } from 'react-native'; -import { colors, fontStyles } from '../../../styles/common'; -import ReceiveRequestAction from './ReceiveRequestAction'; -import Logger from '../../../util/Logger'; -import Share from 'react-native-share'; // eslint-disable-line import/default -import { connect } from 'react-redux'; -import { toggleReceiveModal } from '../../../actions/modals'; +import { InteractionManager, TouchableOpacity, SafeAreaView, Dimensions, StyleSheet, View, Alert } from 'react-native'; import Modal from 'react-native-modal'; -import { strings } from '../../../../locales/i18n'; -import ElevatedView from 'react-native-elevated-view'; -import AntIcon from 'react-native-vector-icons/AntDesign'; -import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons'; -import FontAwesome from 'react-native-vector-icons/FontAwesome'; -import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; -import Device from '../../../util/Device'; -import { generateUniversalLinkAddress } from '../../../util/payment-link-generator'; -import AddressQRCode from '../../Views/AddressQRCode'; +import Share from 'react-native-share'; +import QRCode from 'react-native-qrcode-svg'; +import Clipboard from '@react-native-community/clipboard'; +import EvilIcons from 'react-native-vector-icons/EvilIcons'; +import { connect } from 'react-redux'; + import Analytics from '../../../core/Analytics'; +import Logger from '../../../util/Logger'; +import Device from '../../../util/Device'; +import { strings } from '../../../../locales/i18n'; import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; +import { generateUniversalLinkAddress } from '../../../util/payment-link-generator'; +import { allowedToBuy } from '../FiatOrders'; +import { showAlert } from '../../../actions/alert'; +import { toggleReceiveModal } from '../../../actions/modals'; import { protectWalletModalVisible } from '../../../actions/user'; -const TOTAL_PADDING = 64; -const ACTION_WIDTH = (Dimensions.get('window').width - TOTAL_PADDING) / 2; +import { colors, fontStyles } from '../../../styles/common'; +import Text from '../../Base/Text'; +import ModalHandler from '../../Base/ModalHandler'; +import AddressQRCode from '../../Views/AddressQRCode'; +import EthereumAddress from '../EthereumAddress'; +import GlobalAlert from '../GlobalAlert'; +import StyledButton from '../StyledButton'; const styles = StyleSheet.create({ wrapper: { @@ -45,50 +48,48 @@ const styles = StyleSheet.create({ backgroundColor: colors.grey400, opacity: Device.isAndroid() ? 0.6 : 0.5 }, - actionsWrapper: { - marginHorizontal: 16, - paddingBottom: Device.isIphoneX() ? 16 : 8 + body: { + alignItems: 'center', + paddingHorizontal: 15 + }, + qrWrapper: { + margin: 15 + }, + addressWrapper: { + flexDirection: 'row', + alignItems: 'center', + margin: 15, + padding: 9, + paddingHorizontal: 15, + backgroundColor: colors.grey000, + borderRadius: 30 + }, + copyButton: { + backgroundColor: colors.grey050, + color: colors.fontPrimary, + borderRadius: 12, + overflow: 'hidden', + paddingVertical: 3, + paddingHorizontal: 6, + marginHorizontal: 6 }, - row: { + actionRow: { flexDirection: 'row', - alignItems: 'center' + marginBottom: 15 + }, + actionButton: { + flex: 1, + marginHorizontal: 8 }, title: { ...fontStyles.normal, + color: colors.fontPrimary, fontSize: 18, flexDirection: 'row', alignSelf: 'center' }, titleWrapper: { - marginVertical: 8 - }, - modal: { - margin: 0, - width: '100%' - }, - copyAlert: { - width: 180, - backgroundColor: colors.darkAlert, - padding: 20, - paddingTop: 30, - alignSelf: 'center', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 8 - }, - copyAlertIcon: { - marginBottom: 20 - }, - copyAlertText: { - textAlign: 'center', - color: colors.white, - fontSize: 16, - ...fontStyles.normal - }, - receiveAction: { - flex: 1, - width: ACTION_WIDTH, - height: ACTION_WIDTH + marginTop: 10 } }); @@ -113,6 +114,14 @@ class ReceiveRequest extends PureComponent { * Action that toggles the receive modal */ toggleReceiveModal: PropTypes.func, + /** + /* Triggers global alert + */ + showAlert: PropTypes.func, + /** + * Network id + */ + network: PropTypes.string, /** * Prompts protect wallet modal */ @@ -151,14 +160,29 @@ class ReceiveRequest extends PureComponent { /** * Shows an alert message with a coming soon message */ - onBuy = () => { - InteractionManager.runAfterInteractions(() => { - this.setState({ buyModalVisible: true }); - setTimeout(() => { - this.setState({ buyModalVisible: false }); - }, 1500); - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.RECEIVE_OPTIONS_BUY); + onBuy = async () => { + const { navigation, toggleReceiveModal, network } = this.props; + if (!allowedToBuy(network)) { + Alert.alert(strings('fiat_on_ramp.network_not_supported'), strings('fiat_on_ramp.switch_network')); + } else { + toggleReceiveModal(); + navigation.navigate('PaymentMethodSelector'); + InteractionManager.runAfterInteractions(() => { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.WALLET_BUY_ETH); + }); + } + }; + + copyAccountToClipboard = async () => { + const { selectedAddress } = this.props; + Clipboard.setString(selectedAddress); + this.props.showAlert({ + isVisible: true, + autodismiss: 1500, + content: 'clipboard-alert', + data: { msg: strings('account_details.account_copied_to_clipboard') } }); + setTimeout(() => this.props.protectWalletModalVisible(), 1500); }; /** @@ -189,36 +213,7 @@ class ReceiveRequest extends PureComponent { }); }; - actions = [ - { - icon: , - title: strings('receive_request.share_title'), - description: strings('receive_request.share_description'), - onPress: this.onShare - }, - { - icon: , - title: strings('receive_request.qr_code_title'), - description: strings('receive_request.qr_code_description'), - onPress: this.openQrModal - }, - { - icon: , - title: strings('receive_request.request_title'), - description: strings('receive_request.request_description'), - onPress: this.onReceive - }, - { - icon: , - title: strings('receive_request.buy_title'), - description: strings('receive_request.buy_description'), - onPress: this.onBuy - } - ]; - render() { - const { qrModalVisible, buyModalVisible } = this.state; - return ( @@ -229,81 +224,84 @@ class ReceiveRequest extends PureComponent { {strings('receive_request.title')} + + + {({ isVisible, toggleModal }) => ( + <> + { + toggleModal(); + InteractionManager.runAfterInteractions(() => { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.RECEIVE_OPTIONS_QR_CODE); + }); + }} + > + + + + + + + )} + - - - - - - - - + {strings('receive_request.scan_address')} + + + + + + + {strings('receive_request.copy')} + + + + + + + {allowedToBuy(this.props.network) && ( + + {strings('fiat_on_ramp.buy_eth')} + + )} + + {strings('receive_request.request_payment')} + - - - - - - - - - {strings('receive_request.coming_soon')} - - + + ); } } const mapStateToProps = state => ({ + network: state.engine.backgroundState.NetworkController.network, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, receiveAsset: state.modals.receiveAsset }); const mapDispatchToProps = dispatch => ({ toggleReceiveModal: () => dispatch(toggleReceiveModal()), + showAlert: config => dispatch(showAlert(config)), protectWalletModalVisible: () => dispatch(protectWalletModalVisible()) }); diff --git a/app/components/UI/ReceiveRequest/index.test.js b/app/components/UI/ReceiveRequest/index.test.js index 0d70c59336c..7ce372372d7 100644 --- a/app/components/UI/ReceiveRequest/index.test.js +++ b/app/components/UI/ReceiveRequest/index.test.js @@ -10,7 +10,8 @@ describe('ReceiveRequest', () => { const initialState = { engine: { backgroundState: { - PreferencesController: { selectedAddress: '0x' } + PreferencesController: { selectedAddress: '0x' }, + NetworkController: { network: '1' } } }, modals: { diff --git a/app/components/UI/Tokens/index.js b/app/components/UI/Tokens/index.js index a1731664e00..19056318748 100644 --- a/app/components/UI/Tokens/index.js +++ b/app/components/UI/Tokens/index.js @@ -15,6 +15,8 @@ import { connect } from 'react-redux'; import { safeToChecksumAddress } from '../../../util/address'; import Analytics from '../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics'; +import StyledButton from '../StyledButton'; +import { allowedToBuy } from '../FiatOrders'; const styles = StyleSheet.create({ wrapper: { @@ -28,6 +30,23 @@ const styles = StyleSheet.create({ alignItems: 'center', marginTop: 50 }, + tokensHome: { + justifyContent: 'center', + alignItems: 'center', + marginTop: 35, + marginHorizontal: 50 + }, + tokensHomeText: { + ...fontStyles.normal, + marginBottom: 15, + marginHorizontal: 15, + fontSize: 18, + color: colors.fontPrimary, + textAlign: 'center' + }, + tokensHomeButton: { + width: '100%' + }, text: { fontSize: 20, color: colors.fontTertiary, @@ -111,7 +130,11 @@ class Tokens extends PureComponent { /** * Primary currency, either ETH or Fiat */ - primaryCurrency: PropTypes.string + primaryCurrency: PropTypes.string, + /** + * Network id + */ + network: PropTypes.string }; actionSheet = null; @@ -183,12 +206,46 @@ class Tokens extends PureComponent { ); }; + goToBuy = () => { + this.props.navigation.navigate('PaymentMethodSelector'); + InteractionManager.runAfterInteractions(() => { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.WALLET_BUY_ETH); + }); + }; + + renderBuyEth() { + const { tokens, network, tokenBalances } = this.props; + if (!allowedToBuy(network)) { + return null; + } + const eth = tokens.find(token => token.isETH); + const ethBalance = eth && eth.balance !== '0'; + const hasTokens = eth ? tokens.length > 1 : tokens.length > 0; + const hasTokensBalance = + hasTokens && + tokens.some( + token => !token.isETH && tokenBalances[token.address] && !tokenBalances[token.address].isZero() + ); + + return ( + + {!ethBalance && !hasTokensBalance && ( + {strings('wallet.ready_to_explore')} + )} + + {strings('fiat_on_ramp.buy_eth')} + + + ); + } + renderList() { const { tokens } = this.props; return ( {tokens.map(item => this.renderItem(item))} + {this.renderBuyEth()} {this.renderFooter()} ); @@ -237,6 +294,7 @@ class Tokens extends PureComponent { } const mapStateToProps = state => ({ + network: state.engine.backgroundState.NetworkController.network, currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, primaryCurrency: state.settings.primaryCurrency, diff --git a/app/components/UI/Tokens/index.test.js b/app/components/UI/Tokens/index.test.js index f3cf057e636..ec7ebe31773 100644 --- a/app/components/UI/Tokens/index.test.js +++ b/app/components/UI/Tokens/index.test.js @@ -22,6 +22,9 @@ describe('Tokens', () => { }, TokenBalancesController: { contractBalance: {} + }, + NetworkController: { + network: '1' } } }, 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 602c0e8d602..fc22c4e25c3 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap @@ -1,195 +1,97 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TransactionDetails should render correctly 1`] = ` - - + - + + Status + + + + - - - Status - - - Confirmed - - - + Date + + - - Date - - - [missing "en.date.months.NaN" translation] NaN at NaN:NaNam - - - - - + + + - - - - From - + + + From + + - - + + + + To + + - - To - - - - + + + - + `; diff --git a/app/components/UI/TransactionElement/TransactionDetails/index.js b/app/components/UI/TransactionElement/TransactionDetails/index.js index b42eb4f2595..080c039f87e 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/index.js +++ b/app/components/UI/TransactionElement/TransactionDetails/index.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { TouchableOpacity, StyleSheet, Text, View } from 'react-native'; -import { colors, fontStyles, baseStyles } from '../../../../styles/common'; +import { TouchableOpacity, StyleSheet, View } from 'react-native'; +import { colors, fontStyles } from '../../../../styles/common'; import { strings } from '../../../../../locales/i18n'; import NetworkList, { getNetworkTypeById, @@ -18,39 +18,11 @@ import { toDateFormat } from '../../../../util/date'; import StyledButton from '../../StyledButton'; import { safeToChecksumAddress } from '../../../../util/address'; import AppConstants from '../../../../core/AppConstants'; +import StatusText from '../../../Base/StatusText'; +import Text from '../../../Base/Text'; +import DetailsModal from '../../../Base/DetailsModal'; const styles = StyleSheet.create({ - detailRowWrapper: { - paddingHorizontal: 15 - }, - detailRowTitle: { - fontSize: 10, - color: colors.grey500, - marginBottom: 8, - ...fontStyles.normal - }, - flexRow: { - flexDirection: 'row' - }, - section: { - paddingVertical: 16 - }, - sectionBorderBottom: { - borderBottomColor: colors.grey100, - borderBottomWidth: 1 - }, - flexEnd: { - flex: 1, - alignItems: 'flex-end' - }, - textUppercase: { - textTransform: 'uppercase' - }, - detailRowText: { - fontSize: 12, - color: colors.fontPrimary, - ...fontStyles.normal - }, viewOnEtherscan: { fontSize: 16, color: colors.blue, @@ -64,10 +36,6 @@ const styles = StyleSheet.create({ summaryWrapper: { marginVertical: 8 }, - statusText: { - fontSize: 12, - ...fontStyles.normal - }, actionContainerStyle: { height: 25, width: 70, @@ -147,6 +115,7 @@ class TransactionDetails extends PureComponent { viewOnEtherscan = () => { const { + navigation, transactionObject: { networkID }, transactionDetails: { transactionHash }, network: { @@ -159,7 +128,7 @@ class TransactionDetails extends PureComponent { if (type === 'rpc') { const url = `${rpcBlockExplorer}/tx/${transactionHash}`; const title = new URL(rpcBlockExplorer).hostname; - this.props.navigation.push('Webview', { + navigation.push('Webview', { url, title }); @@ -167,7 +136,7 @@ class TransactionDetails extends PureComponent { const network = getNetworkTypeById(networkID); const url = getEtherscanTransactionUrl(network, transactionHash); const etherscan_url = getEtherscanBaseUrl(network).replace('https://', ''); - this.props.navigation.push('Webview', { + navigation.push('Webview', { url, title: etherscan_url }); @@ -179,20 +148,6 @@ class TransactionDetails extends PureComponent { } }; - 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}; - } - }; - renderSpeedUpButton = () => ( { const { + transactionDetails, transactionObject, transactionObject: { status, @@ -230,76 +186,70 @@ class TransactionDetails extends PureComponent { const renderSpeedUpAction = safeToChecksumAddress(to) !== AppConstants.CONNEXT.CONTRACTS[networkId]; const { rpcBlockExplorer } = this.state; return ( - - - - - {strings('transactions.status')} - {this.renderStatusText(status)} - {!!renderTxActions && ( - - {renderSpeedUpAction && this.renderSpeedUpButton()} - {this.renderCancelButton()} - - )} - - - {strings('transactions.date')} - {toDateFormat(time)} - - - - - - - {strings('transactions.from')} - - - - {strings('transactions.to')} - - - - - {!!nonce && ( - - - {strings('transactions.nonce')} + + + + {strings('transactions.status')} + + {!!renderTxActions && ( + + {renderSpeedUpAction && this.renderSpeedUpButton()} + {this.renderCancelButton()} + + )} + + + {strings('transactions.date')} + + {toDateFormat(time)} + + + + + + {strings('transactions.from')} + + - {`#${parseInt(nonce.replace(/^#/, ''), 16)}`} - + + + {strings('transactions.to')} + + + + + + {!!nonce && ( + + + {strings('transactions.nonce')} + {`#${parseInt(nonce.replace(/^#/, ''), 16)}`} + + )} - {this.props.transactionDetails.transactionHash && + {transactionDetails.transactionHash && transactionObject.status !== 'cancelled' && rpcBlockExplorer !== NO_RPC_BLOCK_EXPLORER && ( - + {(rpcBlockExplorer && `${strings('transactions.view_on')} ${getBlockExplorerName(rpcBlockExplorer)}`) || strings('transactions.view_on_etherscan')} )} - + ); }; } diff --git a/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap b/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap index 27335c29c4c..81355a1a054 100644 --- a/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TransactionElement should render correctly 1`] = ``; +exports[`TransactionElement should render correctly 1`] = `""`; diff --git a/app/components/UI/TransactionElement/index.js b/app/components/UI/TransactionElement/index.js index 4c30ee9ccf9..74f20521f2c 100644 --- a/app/components/UI/TransactionElement/index.js +++ b/app/components/UI/TransactionElement/index.js @@ -1,20 +1,21 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { TouchableHighlight, StyleSheet, Text, View, Image } from 'react-native'; -import { colors, fontStyles } from '../../../styles/common'; +import { TouchableHighlight, StyleSheet, Image } from 'react-native'; +import { colors } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import { toDateFormat } from '../../../util/date'; import TransactionDetails from './TransactionDetails'; import { safeToChecksumAddress } from '../../../util/address'; 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'; import { TRANSACTION_TYPES } from '../../../util/transactions'; +import ListItem from '../../Base/ListItem'; +import StatusText from '../../Base/StatusText'; +import DetailsModal from '../../Base/DetailsModal'; const styles = StyleSheet.create({ row: { @@ -23,53 +24,6 @@ const styles = StyleSheet.create({ borderBottomWidth: StyleSheet.hairlineWidth, borderColor: colors.grey100 }, - rowContent: { - padding: 0 - }, - rowOnly: { - padding: 15, - minHeight: Device.isIos() ? 95 : 100 - }, - date: { - color: colors.fontSecondary, - fontSize: 12, - marginBottom: 10, - ...fontStyles.normal - }, - info: { - flex: 1, - marginLeft: 15 - }, - address: { - fontSize: 15, - color: colors.fontPrimary, - ...fontStyles.normal - }, - status: { - marginTop: 4, - fontSize: 12, - letterSpacing: 0.5, - ...fontStyles.bold - }, - amount: { - fontSize: 15, - color: colors.fontPrimary, - ...fontStyles.normal - }, - amountFiat: { - fontSize: 12, - color: colors.fontSecondary, - textTransform: 'uppercase', - ...fontStyles.normal - }, - amounts: { - flex: 0.6, - alignItems: 'flex-end' - }, - subRow: { - flexDirection: 'row' - }, - actionContainerStyle: { height: 25, width: 70, @@ -83,63 +37,23 @@ const styles = StyleSheet.create({ padding: 0, paddingHorizontal: 10 }, - transactionActionsContainer: { - 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 }, - iconWrapper: { - flexDirection: 'row', - alignItems: 'center' - }, icon: { width: 28, height: 28 - }, - statusText: { - fontSize: 12, - ...fontStyles.normal } }); -const transactionIconApprove = require('../../../images/transaction-icons/approve.png'); // eslint-disable-line -const transactionIconInteraction = require('../../../images/transaction-icons/interaction.png'); // eslint-disable-line -const transactionIconSent = require('../../../images/transaction-icons/send.png'); // eslint-disable-line -const transactionIconReceived = require('../../../images/transaction-icons/receive.png'); // eslint-disable-line +/* eslint-disable import/no-commonjs */ +const transactionIconApprove = require('../../../images/transaction-icons/approve.png'); +const transactionIconInteraction = require('../../../images/transaction-icons/interaction.png'); +const transactionIconSent = require('../../../images/transaction-icons/send.png'); +const transactionIconReceived = require('../../../images/transaction-icons/receive.png'); -const transactionIconApproveFailed = require('../../../images/transaction-icons/approve-failed.png'); // eslint-disable-line -const transactionIconInteractionFailed = require('../../../images/transaction-icons/interaction-failed.png'); // eslint-disable-line -const transactionIconSentFailed = require('../../../images/transaction-icons/send-failed.png'); // eslint-disable-line -const transactionIconReceivedFailed = require('../../../images/transaction-icons/receive-failed.png'); // eslint-disable-line +const transactionIconApproveFailed = require('../../../images/transaction-icons/approve-failed.png'); +const transactionIconInteractionFailed = require('../../../images/transaction-icons/interaction-failed.png'); +const transactionIconSentFailed = require('../../../images/transaction-icons/send-failed.png'); +const transactionIconReceivedFailed = require('../../../images/transaction-icons/receive-failed.png'); +/* eslint-enable import/no-commonjs */ /** * View that renders a transaction item part of transactions list @@ -242,20 +156,6 @@ class TransactionElement extends PureComponent { this.mounted = false; } - getStatusStyle(status) { - switch (status) { - case 'confirmed': - return [styles.statusText, { color: colors.green400 }]; - case 'pending': - case 'submitted': - return [styles.statusText, { color: colors.orange }]; - case 'failed': - case 'cancelled': - return [styles.statusText, { color: colors.red }]; - } - return null; - } - onPressItem = () => { const { tx, i, onPressItem } = this.props; onPressItem(tx.id, i); @@ -270,12 +170,9 @@ class TransactionElement extends PureComponent { const { tx, selectedAddress } = this.props; const incoming = safeToChecksumAddress(tx.transaction.to) === selectedAddress; const selfSent = incoming && safeToChecksumAddress(tx.transaction.from) === selectedAddress; - return ( - - {(!incoming || selfSent) && tx.transaction.nonce && `#${parseInt(tx.transaction.nonce, 16)} - `} - {`${toDateFormat(tx.time)}`} - - ); + return `${ + (!incoming || selfSent) && tx.transaction.nonce ? `#${parseInt(tx.transaction.nonce, 16)} - ` : '' + }${toDateFormat(tx.time)}`; }; renderTxElementIcon = (transactionElement, status) => { @@ -304,25 +201,7 @@ class TransactionElement extends PureComponent { icon = isFailedTransaction ? transactionIconApproveFailed : transactionIconApprove; break; } - return ( - - - - ); - }; - - 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}; - } + return ; }; /** @@ -343,28 +222,26 @@ class TransactionElement extends PureComponent { const renderTxActions = status === 'submitted' || status === 'approved'; const renderSpeedUpAction = safeToChecksumAddress(to) !== AppConstants.CONNEXT.CONTRACTS[networkId]; return ( - - {this.renderTxTime()} - - {this.renderTxElementIcon(transactionElement, status)} - - - {actionKey} - - {this.renderStatusText(status)} - - - {value} - {fiatValue} - - + + {this.renderTxTime()} + + {this.renderTxElementIcon(transactionElement, status)} + + {actionKey} + + + + {value} + {fiatValue} + + {!!renderTxActions && ( - + {renderSpeedUpAction && this.renderSpeedUpButton()} {this.renderCancelButton()} - + )} - + ); }; @@ -412,48 +289,40 @@ class TransactionElement extends PureComponent { const { tx } = this.props; const { detailsModalVisible, transactionElement, transactionDetails } = this.state; - if (!transactionElement || !transactionDetails) return ; + if (!transactionElement || !transactionDetails) return null; return ( - + <> - {this.renderTxElement(transactionElement)} + {this.renderTxElement(transactionElement)} - - - - - {transactionElement.actionKey} - - - - - - + + + + {transactionElement.actionKey} + + + + + - + ); } } diff --git a/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap b/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap index c1e77c3228c..5b0b28f90af 100644 --- a/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionReview/TransactionReviewFeeCard/__snapshots__/index.test.js.snap @@ -3,224 +3,147 @@ exports[`TransactionReviewFeeCard should render correctly 1`] = ` - - + + Amount + + + + + - Amount + Network fee - - - - - Network fee + + Edit - - - Edit - - - - - - - - - + + - - Total - - Amount - + + + + + + Total + + Amount + + + - - - + - + `; diff --git a/app/components/UI/TransactionReview/TransactionReviewFeeCard/index.js b/app/components/UI/TransactionReview/TransactionReviewFeeCard/index.js index c8945b6a5e3..e37249045ed 100644 --- a/app/components/UI/TransactionReview/TransactionReviewFeeCard/index.js +++ b/app/components/UI/TransactionReview/TransactionReviewFeeCard/index.js @@ -1,65 +1,15 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { StyleSheet, Text, View, TouchableOpacity, ActivityIndicator } from 'react-native'; -import { colors, fontStyles } from '../../../../styles/common'; +import { StyleSheet, View, TouchableOpacity, ActivityIndicator } from 'react-native'; +import { colors } from '../../../../styles/common'; import { strings } from '../../../../../locales/i18n'; +import Summary from '../../../Base/Summary'; +import Text from '../../../Base/Text'; const styles = StyleSheet.create({ overview: { - borderWidth: 1, - borderColor: colors.grey200, - borderRadius: 10, - padding: 16, marginHorizontal: 24 }, - overviewAccent: { - color: colors.blue - }, - overviewCol: { - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'column' - }, - topOverviewCol: { - borderBottomWidth: 1, - borderColor: colors.grey200, - paddingBottom: 12 - }, - bottomOverviewCol: { - paddingTop: 12 - }, - amountRow: { - width: '100%', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center' - }, - amountRowBottomSpace: { - paddingBottom: 12 - }, - totalValueRow: { - justifyContent: 'flex-end' - }, - networkTextWrapper: { - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center' - }, - overviewText: { - ...fontStyles.bold, - color: colors.fontPrimary, - fontSize: 14 - }, - amountText: { - textTransform: 'uppercase' - }, - networkFeeText: { - paddingRight: 5 - }, - totalValueText: { - color: colors.fontSecondary, - textTransform: 'uppercase' - }, loader: { backgroundColor: colors.white, height: 10 @@ -148,40 +98,45 @@ class TransactionReviewFeeCard extends PureComponent { equivalentTotalAmount = totalValue; } return ( - - - - {strings('transaction.amount')} - {amount} - - - - - {strings('transaction.gas_fee')} + + + + {strings('transaction.amount')} + + + {amount} + + + + + + {strings('transaction.gas_fee')} + + + + {' '} + {strings('transaction.edit')} - - - {strings('transaction.edit')} - - - - {this.renderIfGasEstimationReady({networkFee})} - - - - - - {strings('transaction.total')} {strings('transaction.amount')} + + + + {this.renderIfGasEstimationReady( + + {networkFee} - {!!totalFiat && this.renderIfGasEstimationReady(totalAmount)} - - - {this.renderIfGasEstimationReady( - {equivalentTotalAmount} - )} - - - + )} + + + + + {strings('transaction.total')} {strings('transaction.amount')} + + {!!totalFiat && this.renderIfGasEstimationReady(totalAmount)} + + + {this.renderIfGasEstimationReady({equivalentTotalAmount})} + + ); } } diff --git a/app/components/UI/TransactionReview/TransactionReviewInformation/__snapshots__/index.test.js.snap b/app/components/UI/TransactionReview/TransactionReviewInformation/__snapshots__/index.test.js.snap index d22a205becd..81b40efda3e 100644 --- a/app/components/UI/TransactionReview/TransactionReviewInformation/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionReview/TransactionReviewInformation/__snapshots__/index.test.js.snap @@ -11,8 +11,8 @@ exports[`TransactionReviewInformation should render correctly 1`] = ` diff --git a/app/components/UI/TransactionReview/TransactionReviewInformation/index.js b/app/components/UI/TransactionReview/TransactionReviewInformation/index.js index adfc831dce7..7c3ddcb06e3 100644 --- a/app/components/UI/TransactionReview/TransactionReviewInformation/index.js +++ b/app/components/UI/TransactionReview/TransactionReviewInformation/index.js @@ -70,8 +70,8 @@ const styles = StyleSheet.create({ maxWidth: '30%' }, viewDataWrapper: { - marginTop: 32, - marginBottom: 16 + flex: 1, + marginTop: 16 }, viewDataButton: { alignSelf: 'center' diff --git a/app/components/Views/ActivityView/index.js b/app/components/Views/ActivityView/index.js new file mode 100644 index 00000000000..40472acbda2 --- /dev/null +++ b/app/components/Views/ActivityView/index.js @@ -0,0 +1,64 @@ +import React, { useEffect, useContext } from 'react'; +import PropTypes from 'prop-types'; +import { View, StyleSheet } from 'react-native'; +import ScrollableTabView from 'react-native-scrollable-tab-view'; +import { connect } from 'react-redux'; +import { NavigationContext } from 'react-navigation'; +import { getHasOrders } from '../../../reducers/fiatOrders'; + +import getNavbarOptions from '../../UI/Navbar'; +import TransactionsView from '../TransactionsView'; +import TabBar from '../../Base/TabBar'; +import { strings } from '../../../../locales/i18n'; +import FiatOrdersView from '../FiatOrdersView'; + +const styles = StyleSheet.create({ + wrapper: { + flex: 1 + } +}); + +function ActivityView({ hasOrders, ...props }) { + const navigation = useContext(NavigationContext); + + useEffect( + () => { + navigation.setParams({ hasOrders }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [hasOrders] + ); + + return ( + + + + {hasOrders && } + + + ); +} + +ActivityView.defaultProps = { + hasOrders: false +}; + +ActivityView.propTypes = { + hasOrders: PropTypes.bool +}; + +ActivityView.navigationOptions = ({ navigation }) => { + const title = navigation.getParam('hasOrders', false) ? 'activity_view.title' : 'transactions_view.title'; + return getNavbarOptions(title, navigation); +}; + +const mapStateToProps = state => { + const orders = state.fiatOrders.orders; + const selectedAddress = state.engine.backgroundState.PreferencesController.selectedAddress; + const network = state.engine.backgroundState.NetworkController.network; + return { + hasOrders: getHasOrders(orders, selectedAddress, network) + }; +}; + +export default connect(mapStateToProps)(ActivityView); diff --git a/app/components/Views/AddressQRCode/index.js b/app/components/Views/AddressQRCode/index.js index 2bbab541bde..c571a513a51 100644 --- a/app/components/Views/AddressQRCode/index.js +++ b/app/components/Views/AddressQRCode/index.js @@ -10,6 +10,7 @@ import IonicIcon from 'react-native-vector-icons/Ionicons'; import Device from '../../../util/Device'; import { showAlert } from '../../../actions/alert'; import GlobalAlert from '../../UI/GlobalAlert'; +import { protectWalletModalVisible } from '../../../actions/user'; const WIDTH = Dimensions.get('window').width - 88; @@ -76,7 +77,11 @@ class AddressQRCode extends PureComponent { /** /* Callback to close the modal */ - closeQrModal: PropTypes.func + closeQrModal: PropTypes.func, + /** + * Prompts protect wallet modal + */ + protectWalletModalVisible: PropTypes.func }; /** @@ -84,6 +89,7 @@ class AddressQRCode extends PureComponent { */ closeQrModal = () => { this.props.closeQrModal(); + setTimeout(() => this.props.protectWalletModalVisible(), 1000); }; copyAccountToClipboard = async () => { @@ -139,7 +145,8 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ - showAlert: config => dispatch(showAlert(config)) + showAlert: config => dispatch(showAlert(config)), + protectWalletModalVisible: () => dispatch(protectWalletModalVisible()) }); export default connect( diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js index dba023bb1a3..15037ac9b13 100644 --- a/app/components/Views/BrowserTab/index.js +++ b/app/components/Views/BrowserTab/index.js @@ -15,7 +15,7 @@ import { } from 'react-native'; // eslint-disable-next-line import/named import { withNavigation } from 'react-navigation'; -import { WebView } from 'react-native-webview'; +import { WebView } from 'react-native-webview-forked'; import Icon from 'react-native-vector-icons/FontAwesome'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'; diff --git a/app/components/Views/FiatOrdersView/OrderDetails.js b/app/components/Views/FiatOrdersView/OrderDetails.js new file mode 100644 index 00000000000..567054f4fff --- /dev/null +++ b/app/components/Views/FiatOrdersView/OrderDetails.js @@ -0,0 +1,107 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet } from 'react-native'; +import { toDateFormat } from '../../../util/date'; +import { strings } from '../../../../locales/i18n'; + +import Text from '../../Base/Text'; +import StatusText from '../../Base/StatusText'; +import DetailsModal from '../../Base/DetailsModal'; +import EthereumAddress from '../../UI/EthereumAddress'; +import { getProviderName } from '../../../reducers/fiatOrders'; +import Summary from '../../Base/Summary'; +import { addCurrencySymbol, renderNumber } from '../../../util/number'; + +const styles = StyleSheet.create({ + summary: { + marginTop: 8, + marginBottom: 16 + } +}); + +function OrderDetails({ order: { ...order }, closeModal }) { + return ( + + + + {strings('fiat_on_ramp.purchased_currency', { currency: order.cryptocurrency })} + + + + + + + {strings('fiat_on_ramp.status')} + + + + {strings('fiat_on_ramp.date')} + + {toDateFormat(order.createdAt)} + + + + + + {strings('fiat_on_ramp.from')} + + {getProviderName(order.provider)} + + + + {strings('fiat_on_ramp.to')} + + + + + + {!!order.cryptoAmount && ( + + + {strings('fiat_on_ramp.amount')} + + {renderNumber(String(order.cryptoAmount))} {order.cryptocurrency} + + + + )} + {Number.isFinite(order.amount) && Number.isFinite(order.fee) && ( + + + + {strings('fiat_on_ramp.amount')} + + + {addCurrencySymbol((order.amount - order.fee).toLocaleString(), order.currency)} + + + + + {strings('fiat_on_ramp.Fee')} + + + {addCurrencySymbol(order.fee.toLocaleString(), order.currency)} + + + + + + {strings('fiat_on_ramp.total_amount')} + + + {addCurrencySymbol(order.amount.toLocaleString(), order.currency)} + + + + )} + + + ); +} + +OrderDetails.propTypes = { + order: PropTypes.object, + closeModal: PropTypes.func +}; + +export default OrderDetails; diff --git a/app/components/Views/FiatOrdersView/OrderListItem.js b/app/components/Views/FiatOrdersView/OrderListItem.js new file mode 100644 index 00000000000..b7c809ba835 --- /dev/null +++ b/app/components/Views/FiatOrdersView/OrderListItem.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Image, StyleSheet } from 'react-native'; +import ListItem from '../../Base/ListItem'; +import { strings } from '../../../../locales/i18n'; +import { toDateFormat } from '../../../util/date'; +import { renderNumber, addCurrencySymbol } from '../../../util/number'; +import { getProviderName } from '../../../reducers/fiatOrders'; +import StatusText from '../../Base/StatusText'; +/** + * @typedef {import('../../../reducers/fiatOrders').FiatOrder} FiatOrder + */ + +// eslint-disable-next-line import/no-commonjs +const transactionIconReceived = require('../../../images/transaction-icons/receive.png'); + +const styles = StyleSheet.create({ + icon: { + width: 28, + height: 28 + } +}); + +/** + * + * @param {object} props + * @param {FiatOrder} props.order + */ +function OrderListItem({ order }) { + return ( + + {order.createdAt && {toDateFormat(order.createdAt)}} + + + + + + + + {getProviderName(order.provider)}:{' '} + {strings('fiat_on_ramp.purchased_currency', { currency: order.cryptocurrency })} + + + + + + {order.cryptoAmount ? renderNumber(String(order.cryptoAmount)) : '...'} {order.cryptocurrency} + + {addCurrencySymbol(order.amount, order.currency)} + + + + ); +} + +OrderListItem.propTypes = { + order: PropTypes.object +}; + +export default OrderListItem; diff --git a/app/components/Views/FiatOrdersView/index.js b/app/components/Views/FiatOrdersView/index.js new file mode 100644 index 00000000000..fc19f47732d --- /dev/null +++ b/app/components/Views/FiatOrdersView/index.js @@ -0,0 +1,85 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { View, StyleSheet, FlatList, TouchableHighlight } from 'react-native'; +import Modal from 'react-native-modal'; +import { connect } from 'react-redux'; +import { getOrders } from '../../../reducers/fiatOrders'; + +import { colors } from '../../../styles/common'; +import ModalHandler from '../../Base/ModalHandler'; +import OrderListItem from './OrderListItem'; +import OrderDetails from './OrderDetails'; + +/** + * @typedef {import('../../../reducers/fiatOrders').FiatOrder} FiatOrder + */ +const styles = StyleSheet.create({ + wrapper: { + flex: 1 + }, + row: { + borderBottomWidth: StyleSheet.hairlineWidth, + borderColor: colors.grey100 + } +}); + +const keyExtractor = item => item.id; + +/** + * + * @param {object} data + * @param {FiatOrder} data.item + */ +const renderItem = ({ item }) => ( + + {({ isVisible, toggleModal }) => ( + <> + + + + + + + + + )} + +); + +renderItem.propTypes = { + item: PropTypes.object +}; + +function FiatOrdersView({ orders, ...props }) { + return ( + + + + ); +} + +FiatOrdersView.propTypes = { + orders: PropTypes.array +}; + +const mapStateToProps = state => { + const orders = state.fiatOrders.orders; + const selectedAddress = state.engine.backgroundState.PreferencesController.selectedAddress; + const network = state.engine.backgroundState.NetworkController.network; + return { + orders: getOrders(orders, selectedAddress, network) + }; +}; + +export default connect(mapStateToProps)(FiatOrdersView); diff --git a/app/components/Views/SendFlow/Confirm/index.js b/app/components/Views/SendFlow/Confirm/index.js index e9e62b0b69e..77c38c0a6e0 100644 --- a/app/components/Views/SendFlow/Confirm/index.js +++ b/app/components/Views/SendFlow/Confirm/index.js @@ -981,7 +981,11 @@ class Confirm extends PureComponent { onPress={isPaymentChannelTransaction ? this.onPaymentChannelSend : this.onNext} testID={'txn-confirm-send-button'} > - {transactionConfirmed ? : 'Send'} + {transactionConfirmed ? ( + + ) : ( + strings('transaction.send') + )} {this.renderFromAccountModal()} diff --git a/app/components/Views/SendFlow/SendTo/index.js b/app/components/Views/SendFlow/SendTo/index.js index d03085962a4..7d7cb774387 100644 --- a/app/components/Views/SendFlow/SendTo/index.js +++ b/app/components/Views/SendFlow/SendTo/index.js @@ -23,6 +23,7 @@ import WarningMessage from '../WarningMessage'; import { util } from '@metamask/controllers'; import Analytics from '../../../../core/Analytics'; import { ANALYTICS_EVENT_OPTS } from '../../../../util/analytics'; +import { allowedToBuy } from '../../../UI/FiatOrders'; const { hexToBN } = util; const styles = StyleSheet.create({ @@ -111,6 +112,11 @@ const styles = StyleSheet.create({ warningContainer: { marginHorizontal: 24, marginBottom: 32 + }, + buyEth: { + ...fontStyles.bold, + color: colors.black, + textDecorationLine: 'underline' } }); @@ -448,6 +454,28 @@ class SendFlow extends PureComponent { this.setState({ toInputHighlighted: !toInputHighlighted }); }; + goToBuy = () => { + this.props.navigation.navigate('PaymentMethodSelector'); + InteractionManager.runAfterInteractions(() => { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.WALLET_BUY_ETH); + }); + }; + + renderBuyEth = () => { + if (!allowedToBuy(this.props.network)) { + return null; + } + + return ( + <> + {'\n'} + + {strings('fiat_on_ramp.buy_eth')} + + + ); + }; + render = () => { const { isPaymentChannelTransaction } = this.props; const { @@ -517,7 +545,15 @@ class SendFlow extends PureComponent { {!isPaymentChannelTransaction && balanceIsZero && ( - + + {strings('transaction.not_enough_for_gas')} + + {this.renderBuyEth()} + + } + /> )} diff --git a/app/components/Views/TransactionSummary/index.js b/app/components/Views/TransactionSummary/index.js index c3d69e56ced..8b315bd31de 100644 --- a/app/components/Views/TransactionSummary/index.js +++ b/app/components/Views/TransactionSummary/index.js @@ -1,66 +1,16 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { StyleSheet, View, ActivityIndicator, Text, TouchableOpacity } from 'react-native'; -import { colors, fontStyles } from '../../../styles/common'; +import { StyleSheet, View, ActivityIndicator, TouchableOpacity } from 'react-native'; +import { colors } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import { TRANSACTION_TYPES } from '../../../util/transactions'; +import Summary from '../../Base/Summary'; +import Text from '../../Base/Text'; const styles = StyleSheet.create({ - summaryWrapper: { - flexDirection: 'column', - borderWidth: 1, - borderColor: colors.grey050, - borderRadius: 8, - padding: 16 - }, - summaryRow: { - flexDirection: 'row', - justifyContent: 'space-between', - marginVertical: 6 - }, - totalCryptoRow: { - alignItems: 'flex-end', - marginTop: 8 - }, - textSummary: { - ...fontStyles.normal, - color: colors.black, - fontSize: 12 - }, - textSummaryAmount: { - textTransform: 'uppercase' - }, - textFee: { - fontStyle: 'italic' - }, - textCrypto: { - ...fontStyles.normal, - textAlign: 'right', - fontSize: 12, - textTransform: 'uppercase', - color: colors.grey500 - }, - textBold: { - ...fontStyles.bold, - alignSelf: 'flex-end' - }, - separator: { - borderBottomWidth: 1, - borderBottomColor: colors.grey050, - marginVertical: 6 - }, loader: { backgroundColor: colors.white, height: 10 - }, - transactionFeeLeft: { - display: 'flex', - flexDirection: 'row' - }, - transactionEditText: { - fontSize: 12, - marginLeft: 8, - color: colors.blue } }); @@ -93,28 +43,38 @@ export default class TransactionSummary extends PureComponent { this.props.transactionType === TRANSACTION_TYPES.RECEIVED ) { return ( - - - {strings('transaction.amount')} - {amount} - + + + + {strings('transaction.amount')} + + + {amount} + + {secondaryTotalAmount && ( - - {secondaryTotalAmount} - + + + {secondaryTotalAmount} + + )} - + ); } return ( - - - {strings('transaction.amount')} - {amount} - - - - + + + + {strings('transaction.amount')} + + + {amount} + + + + + {!fee ? strings('transaction.transaction_fee_less') : strings('transaction.transaction_fee')} @@ -125,32 +85,39 @@ export default class TransactionSummary extends PureComponent { onPress={onEditPress} key="transactionFeeEdit" > - + + {' '} {strings('transaction.edit')} )} - + {!!fee && this.renderIfGastEstimationReady( - {fee} + + {fee} + )} - - - - {strings('transaction.total_amount')} + + + + + {strings('transaction.total_amount')} + {this.renderIfGastEstimationReady( - + {totalAmount} )} - - + + {this.renderIfGastEstimationReady( - {secondaryTotalAmount} + + {secondaryTotalAmount} + )} - - + + ); }; } diff --git a/app/components/Views/TransactionsView/__snapshots__/index.test.js.snap b/app/components/Views/TransactionsView/__snapshots__/index.test.js.snap index fb108b6e544..e3064207040 100644 --- a/app/components/Views/TransactionsView/__snapshots__/index.test.js.snap +++ b/app/components/Views/TransactionsView/__snapshots__/index.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TransactionsView should render correctly 1`] = ` - getNavbarOptions('transactions_view.title', navigation); - static propTypes = { /** * ETH to current currency conversion rate @@ -214,7 +211,6 @@ class TransactionsView extends PureComponent { render = () => { const { conversionRate, currentCurrency, selectedAddress, navigation, networkType } = this.props; - return ( {this.state.loading ? ( @@ -254,4 +250,4 @@ const mapDispatchToProps = dispatch => ({ export default connect( mapStateToProps, mapDispatchToProps -)(TransactionsView); +)(withNavigation(TransactionsView)); diff --git a/app/core/AppConstants.js b/app/core/AppConstants.js index 3da33cac0a9..6efbe79bc9b 100644 --- a/app/core/AppConstants.js +++ b/app/core/AppConstants.js @@ -34,5 +34,17 @@ export default { : 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/76.0.3809.123 Mobile/15E148 Safari/605.1', NOTIFICATION_NAMES: { accountsChanged: 'wallet_accountsChanged' + }, + FIAT_ORDERS: { + TRANSAK_URL: 'https://global.transak.com/', + TRANSAK_URL_STAGING: 'https://staging-global.transak.com/', + TRANSAK_API_URL_PRODUCTION: 'https://api.transak.com/', + TRANSAK_API_URL_STAGING: 'https://staging-api.transak.com/', + TRANSAK_REDIRECT_URL: 'https://metamask.io/', + WYRE_API_ENDPOINT: 'https://api.sendwyre.com/', + WYRE_API_ENDPOINT_TEST: 'https://api.testwyre.com/', + WYRE_MERCHANT_ID: 'merchant.io.metamask.wyre', + WYRE_MERCHANT_ID_TEST: 'merchant.io.metamask.wyre.test', + POLLING_FREQUENCY: 10000 } }; diff --git a/app/images/ApplePayLogo.png b/app/images/ApplePayLogo.png new file mode 100644 index 00000000000..8bb4621b639 Binary files /dev/null and b/app/images/ApplePayLogo.png differ diff --git a/app/images/ApplePayLogo@2x.png b/app/images/ApplePayLogo@2x.png new file mode 100644 index 00000000000..2d93bbea3b2 Binary files /dev/null and b/app/images/ApplePayLogo@2x.png differ diff --git a/app/images/ApplePayLogo@3x.png b/app/images/ApplePayLogo@3x.png new file mode 100644 index 00000000000..1d77cb5b677 Binary files /dev/null and b/app/images/ApplePayLogo@3x.png differ diff --git a/app/images/ApplePayMark.png b/app/images/ApplePayMark.png new file mode 100644 index 00000000000..4a0f6c58311 Binary files /dev/null and b/app/images/ApplePayMark.png differ diff --git a/app/images/ApplePayMark@2x.png b/app/images/ApplePayMark@2x.png new file mode 100644 index 00000000000..3c0aa16c13d Binary files /dev/null and b/app/images/ApplePayMark@2x.png differ diff --git a/app/images/ApplePayMark@3x.png b/app/images/ApplePayMark@3x.png new file mode 100644 index 00000000000..b3618ba58d8 Binary files /dev/null and b/app/images/ApplePayMark@3x.png differ diff --git a/app/images/TransakLogo.png b/app/images/TransakLogo.png new file mode 100644 index 00000000000..84242afac8b Binary files /dev/null and b/app/images/TransakLogo.png differ diff --git a/app/images/TransakLogo@2x.png b/app/images/TransakLogo@2x.png new file mode 100644 index 00000000000..1f3519b4847 Binary files /dev/null and b/app/images/TransakLogo@2x.png differ diff --git a/app/images/TransakLogo@3x.png b/app/images/TransakLogo@3x.png new file mode 100644 index 00000000000..117606fd4a6 Binary files /dev/null and b/app/images/TransakLogo@3x.png differ diff --git a/app/images/WyreLogo.png b/app/images/WyreLogo.png new file mode 100644 index 00000000000..959537a4b7f Binary files /dev/null and b/app/images/WyreLogo.png differ diff --git a/app/images/WyreLogo@2x.png b/app/images/WyreLogo@2x.png new file mode 100644 index 00000000000..fad71536ddd Binary files /dev/null and b/app/images/WyreLogo@2x.png differ diff --git a/app/images/WyreLogo@3x.png b/app/images/WyreLogo@3x.png new file mode 100644 index 00000000000..40a125e2a1c Binary files /dev/null and b/app/images/WyreLogo@3x.png differ diff --git a/app/reducers/fiatOrders/index.js b/app/reducers/fiatOrders/index.js new file mode 100644 index 00000000000..3e29333144d --- /dev/null +++ b/app/reducers/fiatOrders/index.js @@ -0,0 +1,131 @@ +/** + * @typedef FiatOrder + * @type {object} + * @property {string} id - Original id given by Provider. Orders are identified by (provider, id) + * @property {FIAT_ORDER_PROVIDERS} provider Fiat Provider + * @property {number} createdAt Fiat amount + * @property {string} amount Fiat amount + * @property {string?} fee Fiat fee + * @property {string?} cryptoAmount Crypto currency amount + * @property {string?} cryptoFee Crypto currency fee + * @property {string} currency "USD" + * @property {string} cryptocurrency "ETH" + * @property {FIAT_ORDER_STATES} state Order state + * @property {string} account + * @property {string} network + * @property {?string} txHash + * @property {object} data original provider data + * @property {object} data.order : Wyre order response + * @property {object} data.transfer : Wyre transfer response + */ + +/** + * @enum {string} + */ +export const FIAT_ORDER_PROVIDERS = { + WYRE: 'WYRE', + WYRE_APPLE_PAY: 'WYRE_APPLE_PAY', + TRANSAK: 'TRANSAK' +}; + +/** + * @enum {string} + */ +export const FIAT_ORDER_STATES = { + PENDING: 'PENDING', + FAILED: 'FAILED', + COMPLETED: 'COMPLETED', + CANCELLED: 'CANCELLED' +}; + +/** + * Selectors + */ + +/** + * Get the provider display name + * @param {FIAT_ORDER_PROVIDERS} provider + */ +export const getProviderName = provider => { + switch (provider) { + case FIAT_ORDER_PROVIDERS.WYRE: + case FIAT_ORDER_PROVIDERS.WYRE_APPLE_PAY: { + return 'Wyre'; + } + case FIAT_ORDER_PROVIDERS.TRANSAK: { + return 'Transak'; + } + default: { + return provider; + } + } +}; + +export const getOrders = (orders, selectedAddress, network) => + orders.filter(order => order.account === selectedAddress && order.network === network); + +export const getPendingOrders = (orders, selectedAddress, network) => + orders.filter( + order => + order.account === selectedAddress && order.network === network && order.state === FIAT_ORDER_STATES.PENDING + ); + +export const getHasOrders = (orders, selectedAddress, network) => + orders.some(order => order.account === selectedAddress && order.network === network); + +const initialState = { + orders: [] +}; + +const findOrderIndex = (provider, id, orders) => + orders.findIndex(order => order.id === id && order.provider === provider); + +const fiatOrderReducer = (state = initialState, action) => { + switch (action.type) { + case 'FIAT_ADD_ORDER': { + const orders = state.orders; + const order = action.payload; + const index = findOrderIndex(order.provider, order.id, orders); + if (index !== -1) { + return state; + } + return { + ...state, + orders: [action.payload, ...state.orders] + }; + } + case 'FIAT_UPDATE_ORDER': { + const orders = state.orders; + const order = action.payload; + const index = findOrderIndex(order.provider, order.id, orders); + return { + ...state, + orders: [ + ...orders.slice(0, index), + { + ...orders[index], + ...order + }, + ...orders.slice(index + 1) + ] + }; + } + case 'FIAT_REMOVE_ORDER': { + const orders = state.orders; + const order = action.payload; + const index = findOrderIndex(order.provider, order.id, state.orders); + return { + ...state, + orders: [...orders.slice(0, index), ...orders.slice(index + 1)] + }; + } + case 'FIAT_RESET': { + return initialState; + } + default: { + return state; + } + } +}; + +export default fiatOrderReducer; diff --git a/app/reducers/index.js b/app/reducers/index.js index 6c6d64adde8..969d3ac20ea 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -10,6 +10,7 @@ import userReducer from './user'; import wizardReducer from './wizard'; import analyticsReducer from './analytics'; import onboardingReducer from './onboarding'; +import fiatOrders from './fiatOrders'; import notificationReducer from './notification'; import { combineReducers } from 'redux'; @@ -26,7 +27,8 @@ const rootReducer = combineReducers({ user: userReducer, wizard: wizardReducer, onboarding: onboardingReducer, - notification: notificationReducer + notification: notificationReducer, + fiatOrders }); export default rootReducer; diff --git a/app/util/Logger.js b/app/util/Logger.js index 3ad0bb39522..b7e0be72aea 100644 --- a/app/util/Logger.js +++ b/app/util/Logger.js @@ -1,6 +1,6 @@ 'use strict'; -import { addBreadcrumb, captureException, withScope } from '@sentry/react-native'; +import { addBreadcrumb, captureException, captureMessage, withScope } from '@sentry/react-native'; import AsyncStorage from '@react-native-community/async-storage'; /** @@ -55,4 +55,21 @@ export default class Logger { } } } + + /** + * captureMessage wrapper + * + * @param {object} args - data to be logged + * @returns - void + */ + static async message(...args) { + // Check if user passed accepted opt-in to metrics + const metricsOptIn = await AsyncStorage.getItem('@MetaMask:metricsOptIn'); + if (__DEV__) { + args.unshift('[MetaMask DEBUG]:'); + console.log.apply(null, args); // eslint-disable-line no-console + } else if (metricsOptIn === 'agreed') { + captureMessage(JSON.stringify(args)); + } + } } diff --git a/app/util/analytics.js b/app/util/analytics.js index d35f4b1b98f..68daa1b2a84 100644 --- a/app/util/analytics.js +++ b/app/util/analytics.js @@ -105,7 +105,11 @@ const NAMES = { DAPP_APPROVE_SCREEN_CANCEL: 'Cancel', DAPP_APPROVE_SCREEN_EDIT_PERMISSION: 'Edit permission', DAPP_APPROVE_SCREEN_EDIT_FEE: 'Edit tx fee', - DAPP_APPROVE_SCREEN_VIEW_DETAILS: 'View tx details' + DAPP_APPROVE_SCREEN_VIEW_DETAILS: 'View tx details', + // Fiat Orders + WALLET_BUY_ETH: 'Buy ETH', + PAYMENTS_SELECTS_DEBIT_OR_ACH: 'Selects debit card or bank account as payment method', + PAYMENTS_SELECTS_APPLE_PAY: 'Selects Apple Pay as payment method' }; const ACTIONS = { @@ -144,7 +148,10 @@ const ACTIONS = { // Send Flow SEND_FLOW: 'Send Flow', // Dapp Interactions - APPROVE_REQUEST: 'Approve Request' + APPROVE_REQUEST: 'Approve Request', + BUY_ETH: 'Buy ETH', + SELECTS_DEBIT_OR_ACH: 'Selects Debit or ACH', + SELECTS_APPLE_PAY: 'Selects Apple Pay' }; const CATEGORIES = { @@ -161,7 +168,9 @@ const CATEGORIES = { RECEIVE_OPTIONS: 'Receive Options', INSTAPAY_VIEW: 'InstaPay View', SEND_FLOW: 'Send Flow', - DAPP_INTERACTIONS: 'Dapp Interactions' + DAPP_INTERACTIONS: 'Dapp Interactions', + WALLET: 'Wallet', + PAYMENTS: 'Payments' }; export const ANALYTICS_EVENT_OPTS = { @@ -471,5 +480,17 @@ export const ANALYTICS_EVENT_OPTS = { CATEGORIES.DAPP_INTERACTIONS, ACTIONS.APPROVE_REQUEST, NAMES.DAPP_APPROVE_SCREEN_VIEW_DETAILS + ), + // Fiat Orders + WALLET_BUY_ETH: generateOpt(CATEGORIES.WALLET, ACTIONS.BUY_ETH, NAMES.WALLET_BUY_ETH), + PAYMENTS_SELECTS_DEBIT_OR_ACH: generateOpt( + CATEGORIES.PAYMENTS, + ACTIONS.SELECTS_DEBIT_OR_ACH, + NAMES.PAYMENTS_SELECTS_DEBIT_OR_ACH + ), + PAYMENTS_SELECTS_APPLE_PAY: generateOpt( + CATEGORIES.PAYMENTS, + ACTIONS.SELECTS_APPLE_PAY, + NAMES.PAYMENTS_SELECTS_APPLE_PAY ) }; diff --git a/app/util/number.js b/app/util/number.js index e7fd484c0f9..8232d470b8d 100644 --- a/app/util/number.js +++ b/app/util/number.js @@ -303,17 +303,24 @@ export function weiToFiat(wei, conversionRate, currencyCode, decimalsToShow = 5) } /** - * Adds currency symbol to a value + * Renders fiat amount with currency symbol if exists * - * @param {number} wei - BN corresponding to an amount of wei - * @param {string} currencyCode - Current currency code to display + * @param {number|string} amount Number corresponding to a currency amount + * @param {string} currencyCode Current currency code to display * @returns {string} - Currency-formatted string */ -export function addCurrencySymbol(value, currencyCode) { +export function addCurrencySymbol(amount, currencyCode) { if (currencySymbols[currencyCode]) { - return `${currencySymbols[currencyCode]}${value}`; + return `${currencySymbols[currencyCode]}${amount}`; } - return `${value} ${currencyCode}`; + + const lowercaseCurrencyCode = currencyCode.toLowerCase(); + + if (currencySymbols[lowercaseCurrencyCode]) { + return `${currencySymbols[lowercaseCurrencyCode]}${amount}`; + } + + return `${amount} ${currencyCode}`; } /** @@ -377,10 +384,7 @@ export function balanceToFiat(balance, conversionRate, exchangeRate, currencyCod return undefined; } const fiatFixed = balanceToFiatNumber(balance, conversionRate, exchangeRate); - if (currencySymbols[currencyCode]) { - return `${currencySymbols[currencyCode]}${fiatFixed}`; - } - return `${fiatFixed} ${currencyCode}`; + return addCurrencySymbol(fiatFixed, currencyCode); } /** @@ -437,7 +441,7 @@ export function renderWei(value) { return renderWei.toString(); } /** - * Formatc a string number in an string number with at most 5 decimal places + * Format a string number in an string number with at most 5 decimal places * * @param {string} number - String containing a number * @returns {string} - String number with none or at most 5 decimal places diff --git a/babel.config.js b/babel.config.js index 599c51ce991..26781fed9ca 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-commonjs module.exports = { presets: ['module:metro-react-native-babel-preset'], plugins: ['transform-inline-environment-variables'], diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index f08ff26f157..28d352e7f08 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 15F7795E22A1B7B500B1DF8C /* Mixpanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F7795722A1B79400B1DF8C /* Mixpanel.framework */; }; 15F7795F22A1B7B500B1DF8C /* Mixpanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 15F7795722A1B79400B1DF8C /* Mixpanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 15F7796522A1BC8C00B1DF8C /* RCTAnalytics.m in Sources */ = {isa = PBXBuildFile; fileRef = 15F7796422A1BC8C00B1DF8C /* RCTAnalytics.m */; }; + 18967E5A4221746AF1C1516F /* libPods-MetaMask.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 909175ED86295C18A9811286 /* libPods-MetaMask.a */; }; 2370F9A340CF4ADFBCFB0543 /* EuclidCircularB-RegularItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 58572D81B5D54ED79A16A16D /* EuclidCircularB-RegularItalic.otf */; }; 298242C958524BB38FB44CAE /* Roboto-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C9FD3FB1258A41A5A0546C83 /* Roboto-BoldItalic.ttf */; }; 2A27FC9EEF1F4FD18E658544 /* config.json in Resources */ = {isa = PBXBuildFile; fileRef = EF1C01B7F08047F9B8ADCFBA /* config.json */; }; @@ -47,7 +48,6 @@ CD13D926E1E84D9ABFE672C0 /* Roboto-BlackItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3E2492C67CF345CABD7B8601 /* Roboto-BlackItalic.ttf */; }; D171C39A8BD44DBEB6B68480 /* EuclidCircularB-MediumItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 42CBA652072F4BE2A8B815C1 /* EuclidCircularB-MediumItalic.otf */; }; DC6A024F56DD43E1A83B47B1 /* Roboto-MediumItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D5FF0FF1DFB74B3C8BB99E09 /* Roboto-MediumItalic.ttf */; }; - E2015246FC33F4C4B8E4155C /* libPods-MetaMask.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B069442EBFA83EF178D30B2E /* libPods-MetaMask.a */; }; E34DE917F6FC4438A6E88402 /* EuclidCircularB-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 13EE4910D3BD408A8FCCA5D7 /* EuclidCircularB-BoldItalic.otf */; }; EF65C42EA15B4774B1947A12 /* Roboto-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C752564A28B44392AEE16BD5 /* Roboto-Medium.ttf */; }; FF0F3B13A5354C41913F766D /* EuclidCircularB-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 67FBD519E04742E0AF191782 /* EuclidCircularB-Bold.otf */; }; @@ -191,7 +191,6 @@ 1F06D56A2D2F41FB9345D16F /* Lottie.framework */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = wrapper.framework; name = Lottie.framework; path = System/Library/Frameworks/Lottie.framework; sourceTree = SDKROOT; }; 278065D027394AD9B2906E38 /* libBVLinearGradient.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libBVLinearGradient.a; sourceTree = ""; }; 2D16E6891FA4F8E400B85C8A /* libReact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReact.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 2E000D1F2B3B0387309934A2 /* Pods-MetaMask.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MetaMask.debug.xcconfig"; path = "Target Support Files/Pods-MetaMask/Pods-MetaMask.debug.xcconfig"; sourceTree = ""; }; 3E2492C67CF345CABD7B8601 /* Roboto-BlackItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-BlackItalic.ttf"; path = "../app/fonts/Roboto-BlackItalic.ttf"; sourceTree = ""; }; 42C239E9FAA64BD9A34B8D8A /* MaterialCommunityIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialCommunityIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf"; sourceTree = ""; }; 42C6DDE3B80F47AFA9C9D4F5 /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = ""; }; @@ -209,8 +208,10 @@ 654378AF243E2ADC00571B9C /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; 67FBD519E04742E0AF191782 /* EuclidCircularB-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "EuclidCircularB-Bold.otf"; path = "../app/fonts/EuclidCircularB-Bold.otf"; sourceTree = ""; }; 684F2C84313849199863B5FE /* Roboto-Black.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-Black.ttf"; path = "../app/fonts/Roboto-Black.ttf"; sourceTree = ""; }; + 740D82312DA04CE122022365 /* Pods-MetaMask.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MetaMask.debug.xcconfig"; path = "Target Support Files/Pods-MetaMask/Pods-MetaMask.debug.xcconfig"; sourceTree = ""; }; 7FF1597C0ACA4902B86140B2 /* Zocial.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Zocial.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Zocial.ttf"; sourceTree = ""; }; 8E369AC13A2049B6B21E5120 /* libRCTSearchApi.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTSearchApi.a; sourceTree = ""; }; + 909175ED86295C18A9811286 /* libPods-MetaMask.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MetaMask.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 9499B01ECAC44DA29AC44E80 /* EuclidCircularB-SemiboldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "EuclidCircularB-SemiboldItalic.otf"; path = "../app/fonts/EuclidCircularB-SemiboldItalic.otf"; sourceTree = ""; }; A498EA4CD2F8488DB666B94C /* Entypo.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Entypo.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"; sourceTree = ""; }; A783D1CD7D27456796FE2E1B /* Roboto-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-Bold.ttf"; path = "../app/fonts/Roboto-Bold.ttf"; sourceTree = ""; }; @@ -218,8 +219,7 @@ A98029A3662F4C1391489A6B /* EuclidCircularB-Light.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "EuclidCircularB-Light.otf"; path = "../app/fonts/EuclidCircularB-Light.otf"; sourceTree = ""; }; A98DB430A7DA47EFB97EDF8B /* FontAwesome5_Solid.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Solid.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf"; sourceTree = ""; }; AA9EDF17249955C7005D89EE /* MetaMaskDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = MetaMaskDebug.entitlements; path = MetaMask/MetaMaskDebug.entitlements; sourceTree = ""; }; - B069442EBFA83EF178D30B2E /* libPods-MetaMask.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MetaMask.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - B6684830E1789B9B249D040C /* Pods-MetaMask.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MetaMask.release.xcconfig"; path = "Target Support Files/Pods-MetaMask/Pods-MetaMask.release.xcconfig"; sourceTree = ""; }; + B80AE1A0D2D7B86D04BCE696 /* Pods-MetaMask.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MetaMask.release.xcconfig"; path = "Target Support Files/Pods-MetaMask/Pods-MetaMask.release.xcconfig"; sourceTree = ""; }; BB8BA2D3C0354D6090B56A8A /* Roboto-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-Light.ttf"; path = "../app/fonts/Roboto-Light.ttf"; sourceTree = ""; }; BF485CDA047B4D52852B87F5 /* EvilIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = EvilIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = ""; }; C752564A28B44392AEE16BD5 /* Roboto-Medium.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-Medium.ttf"; path = "../app/fonts/Roboto-Medium.ttf"; sourceTree = ""; }; @@ -250,7 +250,7 @@ 15ACC9FC22655C3A0063978B /* Lottie.framework in Frameworks */, 15F7795E22A1B7B500B1DF8C /* Mixpanel.framework in Frameworks */, 153F84CA2319B8FD00C19B63 /* Branch.framework in Frameworks */, - E2015246FC33F4C4B8E4155C /* libPods-MetaMask.a in Frameworks */, + 18967E5A4221746AF1C1516F /* libPods-MetaMask.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -358,7 +358,7 @@ children = ( 153C1A742217BCDC0088EFE0 /* JavaScriptCore.framework */, 2D16E6891FA4F8E400B85C8A /* libReact.a */, - B069442EBFA83EF178D30B2E /* libPods-MetaMask.a */, + 909175ED86295C18A9811286 /* libPods-MetaMask.a */, ); name = Frameworks; sourceTree = ""; @@ -460,8 +460,8 @@ AA342D524556DBBE26F5997C /* Pods */ = { isa = PBXGroup; children = ( - 2E000D1F2B3B0387309934A2 /* Pods-MetaMask.debug.xcconfig */, - B6684830E1789B9B249D040C /* Pods-MetaMask.release.xcconfig */, + 740D82312DA04CE122022365 /* Pods-MetaMask.debug.xcconfig */, + B80AE1A0D2D7B86D04BCE696 /* Pods-MetaMask.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -473,7 +473,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "MetaMask" */; buildPhases = ( - 6DA7FCA07245EB28ED1BEB65 /* [CP] Check Pods Manifest.lock */, + 1EFD70C7D753BD5DBF716A3E /* [CP] Check Pods Manifest.lock */, 65E00B0A247EA25400E5AC88 /* Start Packager */, 15FDD86321B76696006B7C35 /* Override xcconfig files */, 13B07F871A680F5B00A75B9A /* Sources */, @@ -481,7 +481,7 @@ 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 15ACCA0022655C3A0063978B /* Embed Frameworks */, - AB6DBF608FE351FFD3A85A0F /* [CP] Copy Pods Resources */, + 6EDA5DD4542EEC12BF192BB6 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -703,7 +703,7 @@ shellPath = /bin/sh; shellScript = "if [ -e ../.ios.env ]\nthen\n cp -rf ../.ios.env debug.xcconfig\n cp -rf ../.ios.env release.xcconfig\nelse\n cp -rf ../.ios.env.example debug.xcconfig\n cp -rf ../.ios.env.example release.xcconfig\nfi\n\n"; }; - 65E00B0A247EA25400E5AC88 /* Start Packager */ = { + 1EFD70C7D753BD5DBF716A3E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -711,18 +711,21 @@ inputFileListPaths = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "Start Packager"; + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MetaMask-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 6DA7FCA07245EB28ED1BEB65 /* [CP] Check Pods Manifest.lock */ = { + 65E00B0A247EA25400E5AC88 /* Start Packager */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -730,21 +733,18 @@ inputFileListPaths = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Check Pods Manifest.lock"; + name = "Start Packager"; outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-MetaMask-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n"; showEnvVarsInLog = 0; }; - AB6DBF608FE351FFD3A85A0F /* [CP] Copy Pods Resources */ = { + 6EDA5DD4542EEC12BF192BB6 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -839,7 +839,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2E000D1F2B3B0387309934A2 /* Pods-MetaMask.debug.xcconfig */; + baseConfigurationReference = 740D82312DA04CE122022365 /* Pods-MetaMask.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_OPTIMIZATION = time; @@ -847,7 +847,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 511; + CURRENT_PROJECT_VERSION = 518; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -902,7 +902,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B6684830E1789B9B249D040C /* Pods-MetaMask.release.xcconfig */; + baseConfigurationReference = B80AE1A0D2D7B86D04BCE696 /* Pods-MetaMask.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_OPTIMIZATION = time; @@ -910,7 +910,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 511; + CURRENT_PROJECT_VERSION = 518; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/MetaMask/MetaMask.entitlements b/ios/MetaMask/MetaMask.entitlements index 0ee0d28c177..70d4d8f04a3 100644 --- a/ios/MetaMask/MetaMask.entitlements +++ b/ios/MetaMask/MetaMask.entitlements @@ -12,8 +12,8 @@ com.apple.developer.in-app-payments - merchant.io.metamask - merchant.io.metamask-test + merchant.io.metamask.wyre + merchant.io.metamask.wyre.test diff --git a/ios/Podfile b/ios/Podfile index 728a441cda1..dd039c0d771 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -104,6 +104,7 @@ target 'MetaMask' do add_flipper_pods! + post_install do |installer| flipper_post_install(installer) end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1d95a3f2a63..1c32727cacf 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -278,7 +278,9 @@ PODS: - React - react-native-viewpager (3.3.0): - React - - react-native-webview (7.0.5): + - react-native-webview (10.7.0): + - React + - react-native-webview-forked (7.0.5): - React - React-RCTActionSheet (0.62.2): - React-Core/RCTActionSheetHeaders (= 0.62.2) @@ -344,6 +346,8 @@ PODS: - React-cxxreact (= 0.62.2) - React-jsi (= 0.62.2) - ReactCommon/callinvoker (= 0.62.2) + - ReactNativePayments (1.5.0): + - React - RNCAsyncStorage (1.9.0): - React - RNCCheckbox (0.4.2): @@ -436,6 +440,7 @@ DEPENDENCIES: - react-native-view-shot (from `../node_modules/react-native-view-shot`) - "react-native-viewpager (from `../node_modules/@react-native-community/viewpager`)" - react-native-webview (from `../node_modules/react-native-webview`) + - react-native-webview-forked (from `../node_modules/react-native-webview-forked`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) @@ -448,6 +453,7 @@ DEPENDENCIES: - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "ReactNativePayments (from `../node_modules/@exodus/react-native-payments/lib/ios`)" - "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)" - "RNCCheckbox (from `../node_modules/@react-native-community/checkbox`)" - "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)" @@ -540,6 +546,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/viewpager" react-native-webview: :path: "../node_modules/react-native-webview" + react-native-webview-forked: + :path: "../node_modules/react-native-webview-forked" React-RCTActionSheet: :path: "../node_modules/react-native/Libraries/ActionSheetIOS" React-RCTAnimation: @@ -562,6 +570,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/Libraries/Vibration" ReactCommon: :path: "../node_modules/react-native/ReactCommon" + ReactNativePayments: + :path: "../node_modules/@exodus/react-native-payments/lib/ios" RNCAsyncStorage: :path: "../node_modules/@react-native-community/async-storage" RNCCheckbox: @@ -639,7 +649,8 @@ SPEC CHECKSUMS: react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865 react-native-view-shot: 4475fde003fe8a210053d1f98fb9e06c1d834e1c react-native-viewpager: a7b438ca32c57b2614ece2a123e7fe116f743131 - react-native-webview: 174e0f8f1bf547224a134215607c75c4bb0312b7 + react-native-webview: 6edf4d6f71b9161fc3e96083726a538ee395304d + react-native-webview-forked: 30ecda2456675d54273c274a5d659faad44b17ad React-RCTActionSheet: f41ea8a811aac770e0cc6e0ad6b270c644ea8b7c React-RCTAnimation: 49ab98b1c1ff4445148b72a3d61554138565bad0 React-RCTBlob: a332773f0ebc413a0ce85942a55b064471587a71 @@ -651,6 +662,7 @@ SPEC CHECKSUMS: React-RCTText: fae545b10cfdb3d247c36c56f61a94cfd6dba41d React-RCTVibration: 4356114dbcba4ce66991096e51a66e61eda51256 ReactCommon: ed4e11d27609d571e7eee8b65548efc191116eb3 + ReactNativePayments: a4e3ac915256a4e759c8a04338b558494a63a0f5 RNCAsyncStorage: 453cd7c335ec9ba3b877e27d02238956b76f3268 RNCCheckbox: 357578d3b42652c78ee9a1bb9bcfc3195af6e161 RNCClipboard: 8148e21ac347c51fd6cd4b683389094c216bb543 @@ -672,6 +684,6 @@ SPEC CHECKSUMS: Yoga: 3ebccbdd559724312790e7742142d062476b698e YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 17c3a229d2052b61aedf2b90c073f6a3342b048f +PODFILE CHECKSUM: 038ce06ee40546e81f34f1057e015a7a12547d35 COCOAPODS: 1.9.3 diff --git a/locales/en.json b/locales/en.json index 90c6579c6ef..2cc02583751 100644 --- a/locales/en.json +++ b/locales/en.json @@ -245,7 +245,11 @@ "private_key_detected": "Private key detected", "do_you_want_to_import_this_account": "Do you want to import this account?", "error": "Error", - "logout_to_import_seed": "You need to log out first in order to import a seed phrase." + "logout_to_import_seed": "You need to log out first in order to import a seed phrase.", + "ready_to_explore": "Ready to start exploring blockchain applications?" + }, + "activity_view": { + "title": "Activity" }, "transactions_view": { "title": "Transactions" @@ -560,7 +564,13 @@ "could_not_resolve_ens": "Couldn't resolve ENS", "asset": "Asset", "balance": "Balance", - "not_enough_for_gas": "You have 0 ETH in your account to pay for transaction fees. Buy some ETH or deposit from another account." + "not_enough_for_gas": "You have 0 ETH in your account to pay for transaction fees. Buy some ETH or deposit from another account.", + "send": "Send", + "confirmed": "Confirmed", + "pending": "Pending", + "submitted": "Submitted", + "failed": "Failed", + "cancelled": "Cancelled" }, "custom_gas": { "total": "Total", @@ -1104,10 +1114,13 @@ "request_title": "Request", "request_description": "Request assets from friends", "buy_title": "Buy", - "buy_description": "Buy Crypto with Credit Card", + "buy_description": "Buy crypto with debit card or bank transfer", "public_address": "Public Address", "public_address_qr_code": "Public Address", - "coming_soon": "Coming soon..." + "coming_soon": "Coming soon...", + "request_payment": "Request Payment", + "copy": "Copy", + "scan_address": "Scan address to receive payment" }, "experimental_settings": { "payment_channels": "Payment Channels", @@ -1225,6 +1238,76 @@ "title": "You're all set!", "text": "You can now return to your browser" }, + "account_bar": { + "depositing_to": "Depositing to:" + }, + "fiat_on_ramp": { + "buy_eth": "Buy ETH", + "purchased_currency": "Purchased {{currency}}", + "network_not_supported": "Current network not supported", + "switch_network": "Please switch to Mainnet", + "switch": "Switch", + "purchases": "Purchases", + "purchase_method": "Purchase Method", + "amount_to_buy": "Amount to buy", + "transak_webview_title": "Transak", + "wyre_user_agreement": "Wyre User Agreement", + "wyre_terms_of_service": "Wyre Terms of Service", + "best_deal": "Best deal", + "purchase_method_title": { + "wyre_first_line": "0% fee when you use", + "wyre_second_line": "Apple Pay.", + "wyre_sub_header": "Valid until July 1st, 2020", + "first_line": "How do you want to make", + "second_line": "your purchase?" + }, + "bank_transfer_debit": "Bank transfer or debit card", + "requires_registration": "Requires registration", + "options_fees_vary": "Options and fees vary based on location", + "countries": "countries", + "some_states_excluded": "Some states excluded", + "purchase_method_modal_close": "Close", + "modal_transak_support": "Transak Support", + "modal_wyre_support": "Wyre Support", + "transak_cta": "Buy ETH with Transak", + "transak_modal_text": "Exact payment methods and fees vary depending on location. Supported countries are:", + "apple_pay": "Apple Pay", + "via": "via", + "fee": "fee", + "Fee": "Fee", + "limited_time": "limited time", + "wyre_loading_rates": " ", + "wyre_estimated": "Estimated {{amount}} {{currency}}", + "wyre_minutes": "1 - 2 minutes", + "wyre_max": "Max $450 weekly", + "wyre_requires_debit_card": "Requires debit card", + "wyre_us_only": "🇺🇸 U.S. only", + "wyre_modal_text": "Paying with Apple Pay, powered by Wyre is supported in the United Sates 🇺🇸 except for CT, HI, NC, NH, NY, VA and VT.", + "wyre_modal_terms_of_service_apply": "Wyre terms of service apply.", + "wyre_minimum_deposit": "Minimum deposit is {{amount}}", + "wyre_maximum_deposit": "Maximum deposit is {{amount}}", + "wyre_purchase": "{{currency}} Purchase", + "buy_with": "Buy with", + "plus_fee": "Plus a {{fee}} fee", + "notifications": { + "purchase_failed_title": "Purchase of {{currency}} has failed! Please try again, sorry for the inconvenience!", + "purchase_cancelled_title": "Your purchase was cancelled", + "purchase_completed_title": "Your purchase of {{amount}} {{currency}} was successful!", + "purchase_completed_description": "Your {{currency}} is now available", + "purchase_pending_title": "Processing your purchase of {{currency}}", + "purchase_pending_description": "Your deposit is in progress" + }, + "date": "Date", + "from": "From", + "to": "To", + "status": "Status", + "completed": "Completed", + "pending": "Pending", + "failed": "Failed", + "cancelled": "Canceled", + "amount": "Amount", + "total_amount": "Total amount" + }, "protect_wallet_modal": { "title": "Protect your wallet", "top_button": "Protect wallet", diff --git a/locales/es.json b/locales/es.json index 38f4dfcbd6b..bff134c17a8 100644 --- a/locales/es.json +++ b/locales/es.json @@ -172,7 +172,10 @@ "send": { "title": "Enviar", "deeplink_failure": "Ooops! Algo ha salido mal, por favor intėntalo de nuevo", - "warn_network_change": "La red ha sido cambiada a " + "warn_network_change": "La red ha sido cambiada a ", + "send_to": "Enviar a", + "confirm": "Confirmar", + "amount": "Monto" }, "receive": { "title": "Recibir" @@ -203,7 +206,11 @@ "private_key_detected": "Clave privada detectada", "do_you_want_to_import_this_account": "Deseas importar esta cuenta?", "error": "Error", - "logout_to_import_seed": "Primero debes cerrar la sesión para poder importar una nueva frase semilla." + "logout_to_import_seed": "Primero debes cerrar la sesión para poder importar una nueva frase semilla.", + "ready_to_explore": "¿Listo para explorar aplicaciones blockchain?" + }, + "activity_view": { + "title": "Actividad" }, "transactions_view": { "title": "Transacciones" @@ -508,7 +515,13 @@ "could_not_resolve_ens": "No se pudo resolver nombre ENS", "asset": "Activo", "balance": "Balance", - "not_enough_for_gas": "Tienes 0 ETH en tu cuenta para pagar por la tarifa de transacción. Compra ETH o depositalo desde otra cuenta." + "not_enough_for_gas": "Tienes 0 ETH en tu cuenta para pagar por la tarifa de transacción. Compra ETH o deposítalo desde otra cuenta.", + "send": "Enviar", + "confirmed": "Confirmada", + "pending": "Pendiente", + "submitted": "Enviada", + "failed": "Fallida", + "cancelled": "Cancelada" }, "custom_gas": { "advanced_options": "Mostrar opciones avanzadas", @@ -639,7 +652,10 @@ "address_copied_to_clipboard": "La dirección fue copiada al portapapeles", "transaction_error": "Error de transacción", "address_to_placeholder": "Buscar, dirección pública (0x), o ENS", - "address_from_balance": "Balance:" + "address_from_balance": "Balance:", + "status": "Estado", + "date": "Fecha", + "nonce": "Nonce" }, "address_book": { "recents": "Recientes", @@ -971,10 +987,13 @@ "request_title": "Solicitar", "request_description": "Solicitar activos de amigos", "buy_title": "Comprar", - "buy_description": "Compre con tarjeta de crédito", + "buy_description": "Compre con tarjeta de dédito o transferencia bancaria", "public_address": "Dirección Pública", "public_address_qr_code": "Dirección Pública", - "coming_soon": "Pronto..." + "coming_soon": "Pronto...", + "request_payment": "Solicitar Pago", + "copy": "Copiar", + "scan_address": "Escanee dirección para recibir pago" }, "experimental_settings": { "payment_channels": "Canales de Pago", @@ -1088,6 +1107,76 @@ "title": "Listo!", "text": "Ahora puedes volver a tu navegador" }, + "account_bar": { + "depositing_to": "Depositando a:" + }, + "fiat_on_ramp": { + "buy_eth": "Comprar ETH", + "purchased_currency": "Compra de {{currency}}", + "network_not_supported": "La red actual no esta soportada", + "switch_network": "Por favor cambiar a Mainnet", + "switch": "Cambiar", + "purchases": "Compras", + "purchase_method": "Método de pago", + "amount_to_buy": "Monto a comprar", + "transak_webview_title": "Transak", + "wyre_user_agreement": "Acuerdo de Usuario de Wyre", + "wyre_terms_of_service": "Términos de Uso de Wyre", + "best_deal": "Mejor oferta", + "purchase_method_title": { + "wyre_first_line": "0% de comisión al", + "wyre_second_line": "pagar con Apple Pay.", + "wyre_sub_header": "Válido hasta 1 Jul, 2020", + "first_line": "¿Cómo quiere pagar", + "second_line": "su compra?" + }, + "bank_transfer_debit": "Transferencia bancaria o tarjeta de débito", + "requires_registration": "Requiere registro", + "options_fees_vary": "Opciones y comisiones varían según ubicación", + "countries": "países", + "some_states_excluded": "Algunos estados excluídos", + "purchase_method_modal_close": "Cerrar", + "modal_transak_support": "Disponibilidad de Transak", + "modal_wyre_support": "Disponibilidad de Wyre", + "transak_cta": "Comprar ETH con Transak", + "transak_modal_text": "Métodos de pago y comisiones varían dependiendo de la ubicación. Países disponibles:", + "apple_pay": "Apple Pay", + "via": "via", + "fee": "comisión", + "Fee": "Comisión", + "limited_time": "tiempo limitado", + "wyre_loading_rates": " ", + "wyre_estimated": "Estimado {{amount}} {{currency}}", + "wyre_minutes": "1 - 2 minutos", + "wyre_max": "Máx. $450 semanal", + "wyre_requires_debit_card": "Requiere tarjeta de débito", + "wyre_us_only": "🇺🇸 solo EUA", + "wyre_modal_text": "El pago con Apple Pay, provisto por Wyre es aceptado en los Estados Unidos 🇺🇸 a excepción de CT, HI, NC, NH, NY, VA y VT.", + "wyre_modal_terms_of_service_apply": "Rige Acuerdo de Usuario de Wyre.", + "wyre_minimum_deposit": "El monto mínimo es {{amount}}", + "wyre_maximum_deposit": "El monto máximo es {{amount}}", + "wyre_purchase": "Comprar {{currency}}", + "buy_with": "Comprar con", + "plus_fee": "Más {{fee}} de comisión", + "notifications": { + "purchase_failed_title": "¡Su compra de {{currency}} ha fallado! Lo sentimos, por favor intente de nuevo", + "purchase_cancelled_title": "Su compra fue cancelada", + "purchase_completed_title": "¡Su compra de {{amount}} {{currency}} fue exitosa!", + "purchase_completed_description": "Su {{currency}} está disponible", + "purchase_pending_title": "Procesando su compra de {{currency}}", + "purchase_pending_description": "Su depósito está en progreso" + }, + "date": "Fecha", + "from": "Desde", + "to": "Para", + "status": "Estado", + "completed": "Realizada", + "pending": "Pendiente", + "failed": "Fallida", + "cancelled": "Cancelada", + "amount": "Cantidad", + "total_amount": "Cantidad total" + }, "protect_wallet_modal": { "title": "Protege tu billetera", "top_button": "Proteger billetera", diff --git a/package.json b/package.json index ec192fbba47..237282a3b18 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "react-native-level-fs/**/bl": "^0.9.5" }, "dependencies": { + "@exodus/react-native-payments": "https://github.com/wachunei/react-native-payments.git#package-json-hack", "@metamask/controllers": "2.0.3", "@react-native-community/async-storage": "1.9.0", "@react-native-community/blur": "^3.6.0", @@ -83,6 +84,7 @@ "@walletconnect/client": "1.0.0-rc.3", "@walletconnect/utils": "1.0.0-rc.3", "asyncstorage-down": "4.2.0", + "axios": "^0.19.2", "babel-plugin-transform-inline-environment-variables": "0.4.3", "babel-plugin-transform-remove-console": "6.9.4", "base-64": "0.1.0", @@ -121,6 +123,7 @@ "pubnub": "4.27.3", "pump": "3.0.0", "qs": "6.7.0", + "query-string": "^6.12.1", "react": "16.11.0", "react-native": "0.62.2", "react-native-actionsheet": "beefe/react-native-actionsheet#107/head", @@ -167,7 +170,8 @@ "react-native-v8": "^0.62.2-patch.1", "react-native-vector-icons": "6.4.2", "react-native-view-shot": "^3.1.2", - "react-native-webview": "git+https://github.com/MetaMask/react-native-webview.git#931ae99a0f80a650c958e9d2e39e4ed0e68d95a7", + "react-native-webview": "10.7.0", + "react-native-webview-forked": "git+https://github.com/MetaMask/react-native-webview#8c1942b4d3887a80d5d88128a5e55f3944f7fe21", "react-navigation": "4.0.10", "react-navigation-drawer": "1.4.0", "react-navigation-stack": "1.7.3", @@ -207,6 +211,7 @@ "husky": "1.3.1", "jest": "^25.2.7", "jest-serializer": "24.4.0", + "jetifier": "^1.6.6", "lint-staged": "8.1.5", "metro": "^0.59.0", "metro-react-native-babel-preset": "^0.59.0", diff --git a/scripts/postinstall.sh b/scripts/postinstall.sh index 85d4739beda..7c1b96f0164 100755 --- a/scripts/postinstall.sh +++ b/scripts/postinstall.sh @@ -4,13 +4,16 @@ echo "PostInstall script:" echo "1. React Native nodeify..." node_modules/.bin/rn-nodeify --install 'crypto,buffer,react-native-randombytes,vm,stream,http,https,os,url,net,fs' --hack -echo "2. Patch npm packages" +echo "2. jetify" +npx jetify + +echo "3. Patch npm packages" npx patch-package -echo "2. Create xcconfig files..." +echo "4. Create xcconfig files..." echo "" > ios/debug.xcconfig echo "" > ios/release.xcconfig -echo "3. Init git submodules" +echo "5. Init git submodules" echo "This may take a while..." git submodule update --init diff --git a/yarn.lock b/yarn.lock index f13172361d2..c35ad4af556 100644 --- a/yarn.lock +++ b/yarn.lock @@ -735,6 +735,14 @@ dependencies: "@types/hammerjs" "^2.0.36" +"@exodus/react-native-payments@https://github.com/wachunei/react-native-payments.git#package-json-hack": + version "1.5.0" + resolved "https://github.com/wachunei/react-native-payments.git#dbc8cbbed570892d2fea5e3d183bf243e062c1e5" + dependencies: + es6-error "^4.0.2" + uuid "3.3.2" + validator "^7.0.0" + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -2096,6 +2104,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + babel-core@7.0.0-bridge.0: version "7.0.0-bridge.0" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" @@ -3367,7 +3382,7 @@ debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@3.1.0: +debug@3.1.0, debug@=3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -3979,6 +3994,11 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es6-error@^4.0.2: + version "4.1.1" + resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" + integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== + es6-promise@^4.0.3: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -4001,6 +4021,11 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escodegen@1.x.x, escodegen@^1.11.1: version "1.14.1" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" @@ -5440,6 +5465,13 @@ fn-name@~2.0.1: resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7" integrity sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc= +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + for-each@~0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -7150,6 +7182,11 @@ jetifier@^1.6.2: resolved "https://registry.yarnpkg.com/jetifier/-/jetifier-1.6.5.tgz#ea87324a4230bef20a9651178ecab978ee54a8cb" integrity sha512-T7yzBSu9PR+DqjYt+I0KVO1XTb1QhAfHnXV5Nd3xpbXM6Xg4e3vP60Q4qkNU8Fh6PHC2PivPUNN3rY7G2MxcDQ== +jetifier@^1.6.6: + version "1.6.6" + resolved "https://registry.yarnpkg.com/jetifier/-/jetifier-1.6.6.tgz#fec8bff76121444c12dc38d2dad6767c421dab68" + integrity sha512-JNAkmPeB/GS2tCRqUzRPsTOHpGDah7xP18vGJfIjZC+W2sxEHbxgJxetIjIqhjQ3yYbYNEELkM/spKLtwoOSUQ== + js-sha3@0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.5.tgz#baf0c0e8c54ad5903447df96ade7a4a1bca79a4a" @@ -9684,6 +9721,15 @@ qs@~6.5.2: integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== query-string@^6.11.1: + version "6.13.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.1.tgz#d913ccfce3b4b3a713989fe6d39466d92e71ccad" + integrity sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA== + dependencies: + decode-uri-component "^0.2.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + +query-string@^6.12.1: version "6.12.1" resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.12.1.tgz#2ae4d272db4fba267141665374e49a1de09e8a7c" integrity sha512-OHj+zzfRMyj3rmo/6G8a5Ifvw3AleL/EbcHMD27YA31Q+cO5lfmQxECkImuNVjcskLcvBRVHNAB3w6udMs1eAA== @@ -10139,13 +10185,21 @@ react-native-view-shot@^3.1.2: resolved "https://registry.yarnpkg.com/react-native-view-shot/-/react-native-view-shot-3.1.2.tgz#8c8e84c67a4bc8b603e697dbbd59dbc9b4f84825" integrity sha512-9u9fPtp6a52UMoZ/UCPrCjKZk8tnkI9To0Eh6yYnLKFEGkRZ7Chm6DqwDJbYJHeZrheCCopaD5oEOnRqhF4L2Q== -"react-native-webview@git+https://github.com/MetaMask/react-native-webview.git#931ae99a0f80a650c958e9d2e39e4ed0e68d95a7": +"react-native-webview-forked@git+https://github.com/MetaMask/react-native-webview#8c1942b4d3887a80d5d88128a5e55f3944f7fe21": version "7.0.5" - resolved "git+https://github.com/MetaMask/react-native-webview.git#931ae99a0f80a650c958e9d2e39e4ed0e68d95a7" + resolved "git+https://github.com/MetaMask/react-native-webview#8c1942b4d3887a80d5d88128a5e55f3944f7fe21" dependencies: escape-string-regexp "1.0.5" invariant "2.2.4" +react-native-webview@10.7.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-10.7.0.tgz#b96e152ffdae4eeffaa74f43671a35733885b52d" + integrity sha512-4TSYwJqMBUTKB9+xqGbPwx+eLXbp6RRD7lQ2BumT8eSTfuuqr2rXNqcrlKU1VRla7QGGYowmYmxl2aXIx5k9wA== + dependencies: + escape-string-regexp "2.0.0" + invariant "2.2.4" + react-native@0.62.2: version "0.62.2" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.62.2.tgz#d831e11a3178705449142df19a70ac2ca16bad10" @@ -12193,6 +12247,11 @@ uuid@2.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.1.tgz#c2a30dedb3e535d72ccf82e343941a50ba8533ac" integrity sha1-wqMN7bPlNdcsz4LjQ5QaULqFM6w= +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + uuid@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" @@ -12235,6 +12294,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-7.2.0.tgz#a63dcbaba51d4350bf8df20988e0d5a54d711791" + integrity sha512-c8NGTUYeBEcUIGeMppmNVKHE7wwfm3mYbNZxV+c5mlv9fDHI7Ad3p07qfNrn/CvpdkK2k61fOLRO2sTEhgQXmg== + varint@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.0.tgz#d826b89f7490732fabc0c0ed693ed475dcb29ebf"