diff --git a/jestSetup.js b/jestSetup.js index ff0869d2d54..9aa10d2717d 100644 --- a/jestSetup.js +++ b/jestSetup.js @@ -1,3 +1,5 @@ +import './node_modules/react-native-gesture-handler/jestSetup.js' + import { jest } from '@jest/globals' import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock' @@ -46,12 +48,6 @@ jest.mock('@react-navigation/elements', () => ({ getDefaultHeaderHeight: () => 44 })) -jest.mock('react-native-gesture-handler', () => ({ - PanGestureHandler({ children }) { - return children - } -})) - jest.mock('rn-qr-generator', () => ({ detect() { return Promise.resolve() diff --git a/src/__tests__/modals/__snapshots__/AccelerateTxModal.test.tsx.snap b/src/__tests__/modals/__snapshots__/AccelerateTxModal.test.tsx.snap index b4399674cd3..b54f8101607 100644 --- a/src/__tests__/modals/__snapshots__/AccelerateTxModal.test.tsx.snap +++ b/src/__tests__/modals/__snapshots__/AccelerateTxModal.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AccelerateTxModalComponent should render with loading props 1`] = ` - - + `; diff --git a/src/__tests__/modals/__snapshots__/AddressModal.test.tsx.snap b/src/__tests__/modals/__snapshots__/AddressModal.test.tsx.snap index 7b234322731..42b9fa44851 100644 --- a/src/__tests__/modals/__snapshots__/AddressModal.test.tsx.snap +++ b/src/__tests__/modals/__snapshots__/AddressModal.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AddressModalComponent should render with loaded props 1`] = ` - - + `; diff --git a/src/__tests__/modals/__snapshots__/AutoLogoutModal.test.tsx.snap b/src/__tests__/modals/__snapshots__/AutoLogoutModal.test.tsx.snap index 5403c445ee4..56cd5560983 100644 --- a/src/__tests__/modals/__snapshots__/AutoLogoutModal.test.tsx.snap +++ b/src/__tests__/modals/__snapshots__/AutoLogoutModal.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AutoLogoutModal should render with loading props 1`] = ` - - + `; diff --git a/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap b/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap index b026c839963..d737c15bbb0 100644 --- a/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap +++ b/src/__tests__/modals/__snapshots__/CategoryModal.test.tsx.snap @@ -13,7 +13,6 @@ exports[`CategoryModal should render with a subcategory 1`] = ` } } accessible={true} - collapsable={false} focusable={true} onClick={[Function]} onResponderGrant={[Function]} @@ -23,320 +22,424 @@ exports[`CategoryModal should render with a subcategory 1`] = ` onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} style={ - { - "backgroundColor": "transparent", - "bottom": 0, - "left": 0, - "opacity": 0, - "position": "absolute", - "right": 0, - "top": 0, - } - } - > - - , + }, + { + "opacity": 0, + }, + ] + } + />, - - Choose a Category - + /> + + + - Income + Choose a Category - - Expense - - - + Income + + + - + + Expense + + + - Transfer - + + Transfer + + + + + Exchange + + - - Exchange - - - - - - + /> - + + Sub-category + + + - Sub-category - + testID="undefined.textInput" + textAlignVertical="top" + value="Paycheck" + /> - - - - - -  - + +  + + - - + + + + + Income:Paycheck + + + + + + + + + + + + + - + + + + Exchange:Paycheck + + + + + + + + + + + + + + + + + + Expense:Paycheck + + + + + + + + + + + + + + + > + + + + Transfer:Paycheck + + + + + + + + + + + - - - - - , + }, + { + "opacity": 0, + }, + ] + } + />, - - Choose a Category - + /> + + + - Income + Choose a Category - - Expense - - - + Income + + + - + + Expense + + + - Transfer - + + Transfer + + + + + Exchange + + - - Exchange - - - - - - + + + - + Sub-category + + + - Sub-category - + testID="undefined.textInput" + textAlignVertical="top" + value="" + /> - - - - - -  - + +  + + - - + + + + + Exchange:Buy Bitcoin + + + + + + + + + + + + Exchange:Sell Bitcoin + + + + + + + + + + + + Expense:Air Travel + + + + + + + - + > + + + + Expense:Alcohol & Bars + + + + + - - - - - , + }, + { + "opacity": 0, + }, + ] + } + />, - - + /> + + + + + + + + - + Thanks for using Edge! + + + + + + + } + > - Thanks for using Edge! +  - - - - - -  - - - + - - Knowledge Base - - - Commonly asked questions and FAQ - - + Commonly asked questions and FAQ + - + + + + > + +  + + - - -  - - - + - - Submit a Support Ticket - - - Troubleshooting and technical support - - + Troubleshooting and technical support + - + + + + > + +  + + - - -  - - - + - - Call for Assistance - - - Get in touch by phone - - + Get in touch by phone + - + + + + > + +  + + - - -  - - - + - - Visit the Edge site - - - More info on Edge - - + More info on Edge + - + + + + > + +  + + - - -  - - - + - - Terms of Service - - - Understand your obligations and risks associated with our platform - - + Understand your obligations and risks associated with our platform + - - - Version 1.2.3 - - - Build 2019010101 - - - - - + + Version 1.2.3 + + + > + Build 2019010101 + + - + - + `; diff --git a/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap index c2e9a18dd45..6c9b48fdd66 100644 --- a/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap @@ -1704,6 +1704,11 @@ exports[`CryptoExchangeQuoteScreenComponent should render with loading props 1`] } /> { const styles = getStyles(theme) return ( - bridge.resolve(undefined)} paddingRem={[1, 0]}> + bridge.resolve(undefined)} paddingRem={[1, 0]}> {lstrings.fio_address_choose_domain_label} @@ -169,16 +169,16 @@ class DomainListModalComponent extends React.Component { value={input} iconComponent={SearchIconAnimated} /> - - - + {/* */} + ) } } diff --git a/src/components/modals/AccelerateTxModal.tsx b/src/components/modals/AccelerateTxModal.tsx index 3c1b8d05046..46d0bca2686 100644 --- a/src/components/modals/AccelerateTxModal.tsx +++ b/src/components/modals/AccelerateTxModal.tsx @@ -13,7 +13,7 @@ import { WarningCard } from '../cards/WarningCard' import { cacheStyles, Theme, ThemeProps, withTheme } from '../services/ThemeContext' import { ModalMessage, ModalTitle } from '../themed/ModalParts' import { Slider } from '../themed/Slider' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' import { RowUi4 } from '../ui4/RowUi4' interface OwnProps { @@ -111,7 +111,7 @@ export class AccelerateTxModalComponent extends PureComponent { const isSending = status === 'sending' return ( - + {lstrings.transaction_details_accelerate_transaction_header} {lstrings.transaction_details_accelerate_transaction_instructional} @@ -140,7 +140,7 @@ export class AccelerateTxModalComponent extends PureComponent { disabledText={lstrings.transaction_details_accelerate_transaction_slider_disabled} /> - + ) } } diff --git a/src/components/modals/AddressModal.tsx b/src/components/modals/AddressModal.tsx index 15ec36ff6c0..7c1d0b4caa6 100644 --- a/src/components/modals/AddressModal.tsx +++ b/src/components/modals/AddressModal.tsx @@ -20,7 +20,7 @@ import { cacheStyles, Theme, ThemeProps, withTheme } from '../services/ThemeCont import { FilledTextInput } from '../themed/FilledTextInput' import { MainButton } from '../themed/MainButton' import { ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' interface OwnProps { bridge: AirshipBridge @@ -304,7 +304,7 @@ export class AddressModalComponent extends React.Component { const styles = getStyles(theme) return ( - + {title || lstrings.address_modal_default_header} @@ -338,7 +338,7 @@ export class AddressModalComponent extends React.Component { {/* TODO: Style ButtonsViewUi4 for Modals */} - + ) } } diff --git a/src/components/modals/AutoLogoutModal.tsx b/src/components/modals/AutoLogoutModal.tsx index f40644c998e..796bbefd2aa 100644 --- a/src/components/modals/AutoLogoutModal.tsx +++ b/src/components/modals/AutoLogoutModal.tsx @@ -10,8 +10,8 @@ import { DisplayTime, displayToSeconds, secondsToDisplay } from '../../util/disp import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' import { ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' import { ButtonsViewUi4 } from '../ui4/ButtonsViewUi4' +import { ModalUi4 } from '../ui4/ModalUi4' interface Props { bridge: AirshipBridge @@ -76,7 +76,7 @@ export const AutoLogoutModal = (props: Props) => { }, []) return ( - + {lstrings.dialog_title} {isAndroid ? ( @@ -116,7 +116,7 @@ export const AutoLogoutModal = (props: Props) => { )} - + ) } diff --git a/src/components/modals/ButtonsModal.tsx b/src/components/modals/ButtonsModal.tsx index ff122d4def9..09078fe496a 100644 --- a/src/components/modals/ButtonsModal.tsx +++ b/src/components/modals/ButtonsModal.tsx @@ -2,11 +2,12 @@ import * as React from 'react' import { View, ViewStyle } from 'react-native' import { AirshipBridge } from 'react-native-airship' +import { useHandler } from '../../hooks/useHandler' import { showError } from '../services/AirshipInstance' import { useTheme } from '../services/ThemeContext' import { MainButton } from '../themed/MainButton' import { ModalMessage, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' export interface ButtonInfo { label: string @@ -26,12 +27,15 @@ export interface ButtonModalProps { message?: string children?: React.ReactNode buttons: Buttons - closeArrow?: boolean disableCancel?: boolean fullScreen?: boolean // Adds a border: warning?: boolean + + /** @deprecated. Does nothing. */ + // eslint-disable-next-line react/no-unused-prop-types + closeArrow?: boolean } /** @@ -46,10 +50,10 @@ export interface ButtonModalProps { * or other interactive elements. */ export function ButtonsModal(props: ButtonModalProps) { - const { bridge, title, message, children, buttons, closeArrow = false, disableCancel = false, fullScreen = false, warning } = props + const { bridge, title, message, children, buttons, disableCancel = false, fullScreen = false, warning } = props const theme = useTheme() - const handleCancel = disableCancel ? () => {} : () => bridge.resolve(undefined) + const handleCancel = useHandler(() => bridge.resolve(undefined)) const containerStyle: ViewStyle = { flex: fullScreen ? 1 : 0 @@ -62,7 +66,7 @@ export function ButtonsModal(prop } return ( - + {title != null ? {title} : null} @@ -96,6 +100,6 @@ export function ButtonsModal(prop return })} - + ) } diff --git a/src/components/modals/CategoryModal.tsx b/src/components/modals/CategoryModal.tsx index db5258a68e6..1037d2ae643 100644 --- a/src/components/modals/CategoryModal.tsx +++ b/src/components/modals/CategoryModal.tsx @@ -1,7 +1,7 @@ -import { FlashList, ListRenderItem } from '@shopify/flash-list' import * as React from 'react' -import { TouchableHighlight, View } from 'react-native' +import { ListRenderItem, TouchableHighlight, View } from 'react-native' import { AirshipBridge } from 'react-native-airship' +import { FlatList } from 'react-native-gesture-handler' import { Category, displayCategories, formatCategory, getSubcategories, joinCategory, setNewSubcategory, splitCategory } from '../../actions/CategoriesActions' import { useAsyncEffect } from '../../hooks/useAsyncEffect' @@ -14,8 +14,8 @@ import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { DividerLine } from '../themed/DividerLine' import { EdgeText } from '../themed/EdgeText' import { FilledTextInput } from '../themed/FilledTextInput' -import { ModalFooter, ModalFooterFade, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalFooter, ModalTitle } from '../themed/ModalParts' +import { ModalUi4 } from '../ui4/ModalUi4' interface Props { bridge: AirshipBridge @@ -133,7 +133,7 @@ export function CategoryModal(props: Props) { )) return ( - + {lstrings.category_modal_title} {categoryOrder.map(item => ( @@ -150,17 +150,17 @@ export function CategoryModal(props: Props) { value={subcategory} /> - - + {/* */} - + ) } diff --git a/src/components/modals/ConfirmContinueModal.tsx b/src/components/modals/ConfirmContinueModal.tsx index a52b775c5d8..b1fbfa94c7b 100644 --- a/src/components/modals/ConfirmContinueModal.tsx +++ b/src/components/modals/ConfirmContinueModal.tsx @@ -10,7 +10,7 @@ import { EdgeText } from '../themed/EdgeText' import { Fade } from '../themed/Fade' import { MainButton } from '../themed/MainButton' import { ModalMessage, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' interface Props { bridge: AirshipBridge @@ -48,7 +48,7 @@ export function ConfirmContinueModal(props: Props) { } return ( - + {title != null && ( @@ -71,7 +71,7 @@ export function ConfirmContinueModal(props: Props) { - + ) } diff --git a/src/components/modals/FioCreateHandleModal.tsx b/src/components/modals/FioCreateHandleModal.tsx index 89abee0f385..932c9f92609 100644 --- a/src/components/modals/FioCreateHandleModal.tsx +++ b/src/components/modals/FioCreateHandleModal.tsx @@ -10,8 +10,8 @@ import { parseMarkedText } from '../../util/parseMarkedText' import { styled } from '../hoc/styled' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' -import { ThemedModal } from '../themed/ThemedModal' import { ButtonsViewUi4 } from '../ui4/ButtonsViewUi4' +import { ModalUi4 } from '../ui4/ModalUi4' interface Props { bridge: AirshipBridge @@ -38,7 +38,7 @@ export const FioCreateHandleModal = (props: Props) => { }) return ( - + @@ -58,7 +58,7 @@ export const FioCreateHandleModal = (props: Props) => { secondary={{ label: lstrings.not_now_button, onPress: handleCancel }} layout="column" /> - + ) } diff --git a/src/components/modals/FioExpiredModal.tsx b/src/components/modals/FioExpiredModal.tsx index 665fc5aa09f..c16abbf1ef2 100644 --- a/src/components/modals/FioExpiredModal.tsx +++ b/src/components/modals/FioExpiredModal.tsx @@ -4,18 +4,18 @@ import { AirshipBridge } from 'react-native-airship' import { lstrings } from '../../locales/strings' import { MainButton } from '../themed/MainButton' import { ModalMessage, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' export function FioExpiredModal(props: { bridge: AirshipBridge; fioName: string }) { const { bridge, fioName } = props const title = `${lstrings.fio_address_confirm_screen_fio_label} ${lstrings.string_expiration}` return ( - bridge.resolve(false)}> + bridge.resolve(false)}> {title} {lstrings.fio_domain_details_expired_soon} {fioName} bridge.resolve(true)} /> - + ) } diff --git a/src/components/modals/FlipInputModal2.tsx b/src/components/modals/FlipInputModal2.tsx index a291b591368..7106a76e18e 100644 --- a/src/components/modals/FlipInputModal2.tsx +++ b/src/components/modals/FlipInputModal2.tsx @@ -22,8 +22,8 @@ import { FiatText } from '../text/FiatText' import { EdgeText } from '../themed/EdgeText' import { ExchangedFlipInput2, ExchangedFlipInputAmounts, ExchangedFlipInputRef, ExchangeFlipInputFields } from '../themed/ExchangedFlipInput2' import { MiniButton } from '../themed/MiniButton' -import { ThemedModal } from '../themed/ThemedModal' import { CardUi4 } from '../ui4/CardUi4' +import { ModalUi4 } from '../ui4/ModalUi4' export interface FlipInputModalResult { nativeAmount: string @@ -219,7 +219,7 @@ const FlipInputModal2Component = React.forwardRef((pro })) return ( - + {/* Extra view needed here to fullscreen the modal on small devices */} {renderFlipInput()} @@ -232,7 +232,7 @@ const FlipInputModal2Component = React.forwardRef((pro - + ) }) diff --git a/src/components/modals/HelpModal.tsx b/src/components/modals/HelpModal.tsx index 105b769c07a..c5ece07b33f 100644 --- a/src/components/modals/HelpModal.tsx +++ b/src/components/modals/HelpModal.tsx @@ -17,7 +17,7 @@ import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' import { ModalTitle } from '../themed/ModalParts' import { SelectableRow } from '../themed/SelectableRow' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' const buildNumber = getBuildNumber() const versionNumber = getVersion() @@ -61,7 +61,7 @@ export const HelpModal = (props: Props) => { const helpSiteMoreInfoText = sprintf(lstrings.help_site_more_info_text, config.appName) return ( - + @@ -106,7 +106,7 @@ export const HelpModal = (props: Props) => { {versionText} {buildText} - + ) } diff --git a/src/components/modals/InsufficientFeesModal.tsx b/src/components/modals/InsufficientFeesModal.tsx index 2de550c6cb4..a19040ca3c3 100644 --- a/src/components/modals/InsufficientFeesModal.tsx +++ b/src/components/modals/InsufficientFeesModal.tsx @@ -13,7 +13,7 @@ import { getCurrencyCode } from '../../util/CurrencyInfoHelpers' import { roundedFee } from '../../util/utils' import { MainButton } from '../themed/MainButton' import { ModalMessage, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' interface Props { bridge: AirshipBridge @@ -54,12 +54,12 @@ export function InsufficientFeesModal(props: Props) { }) return ( - + {lstrings.buy_crypto_modal_title} {sprintf(lstrings.buy_parent_crypto_modal_message_2s, amountString, name)} - + ) } diff --git a/src/components/modals/ListModal.tsx b/src/components/modals/ListModal.tsx index fa3f108ed89..cc377f461cc 100644 --- a/src/components/modals/ListModal.tsx +++ b/src/components/modals/ListModal.tsx @@ -1,13 +1,13 @@ -import { FlashList, ListRenderItem } from '@shopify/flash-list' import * as React from 'react' -import { Keyboard, ViewStyle, ViewToken } from 'react-native' +import { Keyboard, ListRenderItem, ViewStyle, ViewToken } from 'react-native' import { AirshipBridge } from 'react-native-airship' +import { FlatList } from 'react-native-gesture-handler' import { useFilter } from '../../hooks/useFilter' import { useTheme } from '../services/ThemeContext' import { FilledTextInput } from '../themed/FilledTextInput' import { ModalFooter, ModalMessage, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' interface Props { bridge: AirshipBridge @@ -36,8 +36,6 @@ interface Props { rowComponent?: (props: T) => React.ReactElement rowDataFilter?: (filterText: string, data: T, index: number) => boolean onViewableItemsChanged?: (info: { viewableItems: ViewToken[]; changed: ViewToken[] }) => void - // Footer Props - closeArrow?: boolean // Defaults to 'true' } export function ListModal({ @@ -50,7 +48,6 @@ export function ListModal({ fullScreen = true, rowComponent, rowDataFilter, - closeArrow = true, onSubmitEditing, onViewableItemsChanged, label: placeholder, @@ -73,7 +70,7 @@ export function ListModal({ }, [theme]) return ( - + {title == null ? null : {title}} {message == null ? null : {message}} {textInput == null ? null : ( @@ -95,16 +92,16 @@ export function ListModal({ {...textProps} /> )} - `${i}`} renderItem={renderItem} onScroll={() => Keyboard.dismiss()} onViewableItemsChanged={onViewableItemsChanged} /> - + ) } diff --git a/src/components/modals/LogsModal.tsx b/src/components/modals/LogsModal.tsx index 3f1c30d386e..11ae5ee5af0 100644 --- a/src/components/modals/LogsModal.tsx +++ b/src/components/modals/LogsModal.tsx @@ -11,7 +11,7 @@ import { showToast } from '../services/AirshipInstance' import { FilledTextInput } from '../themed/FilledTextInput' import { MainButton } from '../themed/MainButton' import { ModalMessage, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' interface Props { bridge: AirshipBridge logs: MultiLogOutput @@ -71,7 +71,7 @@ export const LogsModal = (props: Props) => { } return ( - + {lstrings.settings_button_export_logs} {!isDangerous ? null : } {isDangerous ? null : {lstrings.settings_modal_export_logs_message}} @@ -89,6 +89,6 @@ export const LogsModal = (props: Props) => { )} - + ) } diff --git a/src/components/modals/PasswordReminderModal.tsx b/src/components/modals/PasswordReminderModal.tsx index 280e0d3246a..5b75d0b492d 100644 --- a/src/components/modals/PasswordReminderModal.tsx +++ b/src/components/modals/PasswordReminderModal.tsx @@ -10,8 +10,8 @@ import { showError, showToast } from '../services/AirshipInstance' import { ThemeProps, withTheme } from '../services/ThemeContext' import { FilledTextInput } from '../themed/FilledTextInput' import { ModalMessage, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' import { ButtonsViewUi4 } from '../ui4/ButtonsViewUi4' +import { ModalUi4 } from '../ui4/ModalUi4' interface OwnProps { bridge: AirshipBridge @@ -79,7 +79,7 @@ export class PasswordReminderModalComponent extends React.PureComponent + {lstrings.password_reminder_remember_your_password} {lstrings.password_reminder_you_will_need_your_password} @@ -107,7 +107,7 @@ export class PasswordReminderModalComponent extends React.PureComponent - + ) } } diff --git a/src/components/modals/PermissionsSettingModal.tsx b/src/components/modals/PermissionsSettingModal.tsx index 8d6fe4bf704..96f669df730 100644 --- a/src/components/modals/PermissionsSettingModal.tsx +++ b/src/components/modals/PermissionsSettingModal.tsx @@ -11,7 +11,7 @@ import { showError } from '../services/AirshipInstance' import { checkIfDenied } from '../services/PermissionsManager' import { MainButton } from '../themed/MainButton' import { ModalMessage } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' export function PermissionsSettingModal(props: { bridge: AirshipBridge // returns true if mandatory and denied @@ -45,9 +45,9 @@ export function PermissionsSettingModal(props: { const handleClose = () => bridge.resolve(mandatory) return ( - + {message} - + ) } diff --git a/src/components/modals/RawTextModal.tsx b/src/components/modals/RawTextModal.tsx index 67c92f09840..afd88aa8704 100644 --- a/src/components/modals/RawTextModal.tsx +++ b/src/components/modals/RawTextModal.tsx @@ -7,7 +7,7 @@ import { lstrings } from '../../locales/strings' import { showToast } from '../services/AirshipInstance' import { MainButton } from '../themed/MainButton' import { ModalMessage, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' interface Props { bridge: AirshipBridge @@ -27,7 +27,7 @@ export function RawTextModal(props: Props) { } return ( - + {title != null ? {title} : null} {body} @@ -35,6 +35,6 @@ export function RawTextModal(props: Props) { {disableCopy ? null : ( )} - + ) } diff --git a/src/components/modals/SwapVerifyTermsModal.tsx b/src/components/modals/SwapVerifyTermsModal.tsx index dfc20ac99ea..bbcde81449d 100644 --- a/src/components/modals/SwapVerifyTermsModal.tsx +++ b/src/components/modals/SwapVerifyTermsModal.tsx @@ -10,7 +10,7 @@ import { Airship } from '../services/AirshipInstance' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { MainButton } from '../themed/MainButton' import { ModalMessage, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' interface TermsUri { termsUri?: string @@ -59,7 +59,7 @@ function SwapVerifyTermsModal(props: Props) { const styles = getStyles(theme) return ( - bridge.resolve(false)}> + bridge.resolve(false)}> {displayName} @@ -84,7 +84,7 @@ function SwapVerifyTermsModal(props: Props) { )} - + ) } diff --git a/src/components/modals/TextInputModal.tsx b/src/components/modals/TextInputModal.tsx index b433b4b3dc4..8e4ec275a4d 100644 --- a/src/components/modals/TextInputModal.tsx +++ b/src/components/modals/TextInputModal.tsx @@ -8,7 +8,7 @@ import { Alert } from '../themed/Alert' import { FilledTextInput } from '../themed/FilledTextInput' import { MainButton } from '../themed/MainButton' import { ModalMessage, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' interface Props { // Resolves to the entered string, or void if cancelled. @@ -91,7 +91,7 @@ export function TextInputModal(props: Props) { } return ( - bridge.resolve(undefined)}> + bridge.resolve(undefined)}> {title != null ? {title} : null} {typeof message === 'string' ? {message} : <>{message}} {warningMessage != null ? : null} @@ -126,6 +126,6 @@ export function TextInputModal(props: Props) { ) : ( )} - + ) } diff --git a/src/components/modals/TransferModal.tsx b/src/components/modals/TransferModal.tsx index 7e3b9464e65..74d24ec1968 100644 --- a/src/components/modals/TransferModal.tsx +++ b/src/components/modals/TransferModal.tsx @@ -17,7 +17,7 @@ import { Airship } from '../services/AirshipInstance' import { Theme, useTheme } from '../services/ThemeContext' import { ModalTitle } from '../themed/ModalParts' import { SelectableRow } from '../themed/SelectableRow' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' import { WalletListModal, WalletListResult } from './WalletListModal' interface Props { @@ -152,24 +152,13 @@ export const TransferModal = ({ account, bridge, depositOrSend, navigation }: Pr const options = depositOrSend === 'deposit' ? depositOptions : sendOptions return ( - + {depositOrSend === 'deposit' ? lstrings.loan_fragment_deposit : lstrings.fragment_send_subtitle} {options.map((option, index) => { const { title, icon, onPress } = option - return ( - {icon}} - // HACK: ThemedModal has 1 rem padding all around, making it - // impossible to use components expecting split 0.5rem - // margin/padding. - marginRem={[0.5, -0.5]} - /> - ) + return {icon}} /> })} - + ) } diff --git a/src/components/modals/UpdateModal.tsx b/src/components/modals/UpdateModal.tsx index 7235bac22f6..de22198517d 100644 --- a/src/components/modals/UpdateModal.tsx +++ b/src/components/modals/UpdateModal.tsx @@ -9,7 +9,7 @@ import { config } from '../../theme/appConfig' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { MainButton } from '../themed/MainButton' import { ModalMessage, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' interface Props { bridge: AirshipBridge @@ -31,7 +31,7 @@ export function UpdateModal(props: Props) { const message = sprintf(lstrings.update_fresh_new_version, config.appName) return ( - + {lstrings.update_header} @@ -39,7 +39,7 @@ export function UpdateModal(props: Props) { {message} - + ) } diff --git a/src/components/modals/WalletListMenuModal.tsx b/src/components/modals/WalletListMenuModal.tsx index 85717531dd6..d0c7033eba4 100644 --- a/src/components/modals/WalletListMenuModal.tsx +++ b/src/components/modals/WalletListMenuModal.tsx @@ -18,8 +18,8 @@ import { getWalletName } from '../../util/CurrencyWalletHelpers' import { showError } from '../services/AirshipInstance' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { ModalScrollArea, ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' import { CryptoIconUi4 } from '../ui4/CryptoIconUi4' +import { ModalUi4 } from '../ui4/ModalUi4' interface Option { value: WalletListMenuKey @@ -212,7 +212,7 @@ export function WalletListMenuModal(props: Props) { ) return ( - + {wallet != null && ( {getWalletName(wallet)} @@ -238,7 +238,7 @@ export function WalletListMenuModal(props: Props) { ))} - + ) } diff --git a/src/components/modals/WcSmartContractModal.tsx b/src/components/modals/WcSmartContractModal.tsx index 51a08fccc98..ebef8e51ecf 100644 --- a/src/components/modals/WcSmartContractModal.tsx +++ b/src/components/modals/WcSmartContractModal.tsx @@ -19,12 +19,12 @@ import { zeroString } from '../../util/utils' import { Airship, showError } from '../services/AirshipInstance' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { Alert } from '../themed/Alert' -import { ModalFooter, ModalFooterFade, ModalTitle } from '../themed/ModalParts' +import { ModalFooter, ModalTitle } from '../themed/ModalParts' import { SafeSlider } from '../themed/SafeSlider' -import { ThemedModal } from '../themed/ThemedModal' import { CryptoFiatAmountTile } from '../tiles/CryptoFiatAmountTile' import { FiatAmountTile } from '../tiles/FiatAmountTile' import { CardUi4 } from '../ui4/CardUi4' +import { ModalUi4 } from '../ui4/ModalUi4' import { RowUi4 } from '../ui4/RowUi4' interface Props extends WcSmartContractModalProps { @@ -141,7 +141,7 @@ export const WcSmartContractModal = (props: Props) => { ) return ( - + {lstrings.wc_smartcontract_title} @@ -177,8 +177,8 @@ export const WcSmartContractModal = (props: Props) => { )} {slider} - - + {/* */} + ) } diff --git a/src/components/modals/WebViewModal.tsx b/src/components/modals/WebViewModal.tsx index 16971d48b2a..2516dd9000b 100644 --- a/src/components/modals/WebViewModal.tsx +++ b/src/components/modals/WebViewModal.tsx @@ -4,7 +4,7 @@ import { WebView } from 'react-native-webview' import { Airship } from '../services/AirshipInstance' import { ModalTitle } from '../themed/ModalParts' -import { ThemedModal } from '../themed/ThemedModal' +import { ModalUi4 } from '../ui4/ModalUi4' export async function showWebViewModal(title: string, uri: string): Promise { await Airship.show(bridge => ) @@ -25,11 +25,11 @@ export const WebViewModal = (props: Props) => { } return ( - + {title} - + ) } diff --git a/src/components/scenes/CryptoExchangeQuoteScene.tsx b/src/components/scenes/CryptoExchangeQuoteScene.tsx index fc69f383b1f..d16a314adea 100644 --- a/src/components/scenes/CryptoExchangeQuoteScene.tsx +++ b/src/components/scenes/CryptoExchangeQuoteScene.tsx @@ -25,9 +25,9 @@ import { LineTextDivider } from '../themed/LineTextDivider' import { ModalFooter, ModalTitle } from '../themed/ModalParts' import { SceneHeader } from '../themed/SceneHeader' import { Slider } from '../themed/Slider' -import { ThemedModal } from '../themed/ThemedModal' import { WalletListSectionHeader } from '../themed/WalletListSectionHeader' import { AlertCardUi4 } from '../ui4/AlertCardUi4' +import { ModalUi4 } from '../ui4/ModalUi4' export interface CryptoExchangeQuoteParams { selectedQuote: EdgeSwapQuote @@ -140,7 +140,7 @@ export const CryptoExchangeQuoteScene = (props: Props) => { const handlePoweredByTap = useHandler(async () => { await Airship.show(bridge => ( - bridge.resolve()}> + bridge.resolve()}> {lstrings.quote_swap_provider} { renderSectionHeader={renderSectionHeader} sections={sectionList} /> - + )) }) diff --git a/src/components/scenes/Fio/FioStakingChangeScene.tsx b/src/components/scenes/Fio/FioStakingChangeScene.tsx index 1123596a788..d55a289ca87 100644 --- a/src/components/scenes/Fio/FioStakingChangeScene.tsx +++ b/src/components/scenes/Fio/FioStakingChangeScene.tsx @@ -28,7 +28,7 @@ import { ExchangedFlipInputAmounts } from '../../themed/ExchangedFlipInput2' import { ModalMessage, ModalTitle } from '../../themed/ModalParts' import { SceneHeader } from '../../themed/SceneHeader' import { Slider } from '../../themed/Slider' -import { ThemedModal } from '../../themed/ThemedModal' +import { ModalUi4 } from '../../ui4/ModalUi4' import { RowUi4 } from '../../ui4/RowUi4' interface Props extends EdgeSceneProps<'fioStakingChange'> { @@ -185,13 +185,13 @@ export const FioStakingChangeScene = withWallet((props: Props) => { const handleUnlockDate = async () => { await Airship.show(bridge => { return ( - + }> {lstrings.staking_change_unlock_explainer_title} {lstrings.staking_change_unlock_explainer1} {lstrings.staking_change_unlock_explainer2} - + ) }) } diff --git a/src/components/ui4/ModalUi4.tsx b/src/components/ui4/ModalUi4.tsx new file mode 100644 index 00000000000..0eefe82fba1 --- /dev/null +++ b/src/components/ui4/ModalUi4.tsx @@ -0,0 +1,203 @@ +import * as React from 'react' +import { BackHandler, Dimensions, StyleSheet, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native' +import { AirshipBridge } from 'react-native-airship' +import { Gesture, GestureDetector, ScrollView } from 'react-native-gesture-handler' +import { cacheStyles } from 'react-native-patina' +import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated' +import AntDesignIcon from 'react-native-vector-icons/AntDesign' +import { BlurView } from 'rn-id-blurview' + +import { useHandler } from '../../hooks/useHandler' +import { fixSides, mapSides, sidesToMargin } from '../../util/sides' +import { maybeComponent } from '../hoc/maybeComponent' +import { Theme, useTheme } from '../services/ThemeContext' + +export interface ModalPropsUi4 { + bridge: AirshipBridge + children?: React.ReactNode + + // Internal padding to place inside the component. + paddingRem?: number[] | number + + // Include a scroll area: + scroll?: boolean + + // Gives the box a border: + warning?: boolean + + // Called when the user taps outside the modal or clicks the back button. + // If this is missing, the modal will not be closable. + onCancel?: () => void +} + +const safeAreaGap = 64 // Overkill to avoid bottom of screen +const duration = 300 + +/** + * A modal that slides a modal up from the bottom of the screen + * and dims the rest of the app. + */ +export function ModalUi4(props: ModalPropsUi4): JSX.Element { + const { bridge, children, paddingRem, scroll = false, warning = false, onCancel } = props + const theme = useTheme() + const styles = getStyles(theme) + + // Use margin instead of padding to give children the ability to bypass the + // default "padding," if necessary + const childrenMargin = sidesToMargin(mapSides(fixSides(paddingRem, 0.5), theme.rem)) + const closeThreshold = theme.rem(6) + const dragSlop = theme.rem(1) + + // + // Shared state + // + + const opacity = useSharedValue(0) + const offset = useSharedValue(Dimensions.get('window').height) + + const handleCancel = useHandler(() => { + if (onCancel != null) onCancel() + }) + + // + // Effects + // + + React.useEffect(() => bridge.on('clear', handleCancel), [bridge, handleCancel]) + + React.useEffect(() => { + // Animate in: + opacity.value = withTiming(1, { duration }) + offset.value = withTiming(0, { duration }) + + // Animate out: + bridge.on('result', () => { + opacity.value = withTiming(0, { duration }) + offset.value = withTiming(Dimensions.get('window').height, { duration }, () => runOnJS(bridge.remove)()) + }) + }, [bridge, opacity, offset]) + + React.useEffect(() => { + const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { + handleCancel() + return true + }) + return () => backHandler.remove() + }, [handleCancel]) + + const gesture = Gesture.Pan() + .onUpdate(e => { + offset.value = e.translationY + }) + .onEnd(() => { + if (offset.value > closeThreshold) { + runOnJS(handleCancel)() + } + offset.value = withTiming(0, { duration }) + }) + + // + // Dynamic styles + // + + const underlayStyle = useAnimatedStyle(() => ({ + opacity: opacity.value + })) + + const bodyStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: Math.max(-dragSlop, offset.value) }] + })) + + const bodyLayout = { + borderColor: warning ? theme.warningText : theme.modalBorderColor, + borderWidth: warning ? 4 : theme.modalBorderWidth, + marginBottom: -safeAreaGap - dragSlop, + paddingBottom: safeAreaGap + dragSlop + } + + return ( + <> + + + + + + {/* Need another Biew here because BlurView doesn't accept rounded corners in its styling */} + + + + + + + + {scroll ? ( + + {children} + + ) : ( + {children} + )} + + {onCancel == null ? null : ( + + + + )} + + + + ) +} + +const getStyles = cacheStyles((theme: Theme) => ({ + underlay: { + bottom: 0, + left: 0, + position: 'absolute', + right: 0, + top: 0 + }, + body: { + alignSelf: 'flex-end', + backgroundColor: theme.modalBackgroundUi4, + borderTopLeftRadius: theme.rem(1), + borderTopRightRadius: theme.rem(1), + flexShrink: 1, + width: theme.rem(30) // This works as a maxWidth because flexShrink is set + }, + + blurContainer: { + position: 'absolute', + width: '100%', + height: '100%', + borderTopLeftRadius: theme.rem(1), + borderTopRightRadius: theme.rem(1), + overflow: 'hidden' + }, + dragBarContainer: { + alignItems: 'center', + left: 0, + position: 'absolute', + right: 0, + top: 0 + }, + dragBar: { + // TODO: Replace this color we have a proper design: + backgroundColor: theme.deactivatedText, + borderRadius: theme.rem(0.125), + height: theme.rem(0.25), + marginTop: theme.rem(0.5), + width: theme.rem(3) + }, + closeIcon: { + alignItems: 'center', + height: theme.rem(2), + justifyContent: 'center', + position: 'absolute', + right: 0, + top: 0, + width: theme.rem(2) + } +})) + +const MaybeScrollView = maybeComponent(ScrollView) diff --git a/src/theme/variables/edgeDark.ts b/src/theme/variables/edgeDark.ts index 2e086074b45..a0f7d693717 100644 --- a/src/theme/variables/edgeDark.ts +++ b/src/theme/variables/edgeDark.ts @@ -550,6 +550,8 @@ export const edgeDark: Theme = { start: { x: 1, y: 0 } }, + modalBackgroundUi4: 'rgba(255, 255, 255, 0.376)', + txDirBgReceiveUi4: palette.greenOp60, txDirBgSendUi4: palette.redOp60, txDirBgSwapUi4: palette.grayOp70, diff --git a/src/theme/variables/edgeLight.ts b/src/theme/variables/edgeLight.ts index 591304d5f8f..44a03901cc7 100644 --- a/src/theme/variables/edgeLight.ts +++ b/src/theme/variables/edgeLight.ts @@ -519,6 +519,8 @@ export const edgeLight: Theme = { start: { x: 1, y: 0 } }, + modalBackgroundUi4: '#ffffff80', + txDirBgReceiveUi4: palette.greenOp60, txDirBgSendUi4: palette.redOp60, txDirBgSwapUi4: palette.grayOp70, diff --git a/src/theme/variables/testDark.ts b/src/theme/variables/testDark.ts index 07e4c3a896a..fc5c69cedcd 100644 --- a/src/theme/variables/testDark.ts +++ b/src/theme/variables/testDark.ts @@ -550,6 +550,8 @@ export const testDark: Theme = { start: { x: 1, y: 0 } }, + modalBackgroundUi4: '#00000080', + txDirBgReceiveUi4: palette.greenOp60, txDirBgSendUi4: palette.redOp60, txDirBgSwapUi4: palette.grayOp70, diff --git a/src/theme/variables/testLight.ts b/src/theme/variables/testLight.ts index 32b666db638..79c4159916f 100644 --- a/src/theme/variables/testLight.ts +++ b/src/theme/variables/testLight.ts @@ -519,6 +519,8 @@ export const testLight: Theme = { start: { x: 1, y: 0 } }, + modalBackgroundUi4: '#ffffff80', + txDirBgReceiveUi4: palette.greenOp60, txDirBgSendUi4: palette.redOp60, txDirBgSwapUi4: palette.grayOp70, diff --git a/src/types/Theme.ts b/src/types/Theme.ts index 5afc844cfb7..96dc5320e2a 100644 --- a/src/types/Theme.ts +++ b/src/types/Theme.ts @@ -454,6 +454,8 @@ export interface Theme { fioCardGradientUi4: ThemeGradientParams swapCardGradientUi4: ThemeGradientParams + modalBackgroundUi4: string + txDirBgReceiveUi4: string txDirBgSendUi4: string txDirBgSwapUi4: string