From b9acd352504c91b1a36ac398b3686121b210c12d Mon Sep 17 00:00:00 2001 From: Ovidio Rodriguez Date: Tue, 4 Apr 2023 16:01:43 -0400 Subject: [PATCH 1/7] Type safety: implement enums --- components/Amount.tsx | 17 +-- components/BlueWalletWarning.tsx | 3 +- components/Conversion.tsx | 27 ++-- components/HopPicker.tsx | 4 +- .../LayerBalances/OnchainSwipeableRow.tsx | 3 +- components/LayerBalances/index.tsx | 13 +- components/NodeIdenticon.tsx | 5 +- components/SetFeesForm.tsx | 5 +- components/UnitToggle.tsx | 3 +- enums/index.ts | 59 +++++++++ stores/ChannelsStore.ts | 10 +- stores/InvoicesStore.ts | 6 +- stores/SettingsStore.ts | 27 ++-- stores/TransactionsStore.ts | 9 +- stores/UnitsStore.ts | 39 +++--- utils/BackendUtils.ts | 13 +- utils/ConnectionFormatUtils.ts | 3 +- utils/handleAnything.ts | 11 +- views/Activity/Activity.tsx | 5 +- views/Channels/Channel.tsx | 3 +- views/Channels/ChannelsPane.tsx | 11 +- views/EditFee.tsx | 35 +++--- views/LnurlAuth.tsx | 3 +- views/LnurlPay/LnurlPay.tsx | 39 ++++-- views/OpenChannel.tsx | 44 +++---- views/Order.tsx | 90 +++++++------- views/PaymentRequest.tsx | 59 +++++---- views/Receive.tsx | 44 +++---- views/Send.tsx | 73 +++++------ views/Settings/InvoicesSettings.tsx | 3 +- views/Settings/NodeConfiguration.tsx | 115 ++++++++++-------- views/Settings/PaymentsSettings.tsx | 32 +++-- views/Settings/Settings.tsx | 5 +- views/SparkQRScanner.tsx | 3 +- views/Wallet/BalancePane.tsx | 3 +- views/Wallet/KeypadPane.tsx | 7 +- views/Wallet/Wallet.tsx | 10 +- 37 files changed, 493 insertions(+), 348 deletions(-) create mode 100644 enums/index.ts diff --git a/components/Amount.tsx b/components/Amount.tsx index 540da225a..22eb28df9 100644 --- a/components/Amount.tsx +++ b/components/Amount.tsx @@ -10,12 +10,13 @@ import { Spacer } from './layout/Spacer'; import { Row } from './layout/Row'; import { Body } from './text/Body'; import LoadingIndicator from './LoadingIndicator'; +import { Units } from '../enums'; -type Units = 'sats' | 'BTC' | 'fiat'; +type UnitType = 'sats' | 'BTC' | 'fiat'; interface AmountDisplayProps { amount: string; - unit: Units; + unit: UnitType; symbol?: string; negative?: boolean; plural?: boolean; @@ -38,11 +39,11 @@ function AmountDisplay({ color = undefined, pending = false }: AmountDisplayProps) { - if (unit === 'fiat' && !symbol) { + if (unit === Units.fiat && !symbol) { console.error('Must include a symbol when rendering fiat'); } - const actualSymbol = unit === 'BTC' ? '₿' : symbol; + const actualSymbol = unit === Units.BTC ? '₿' : symbol; const Pending = () => ( {pending ? : null} @@ -88,8 +89,8 @@ function AmountDisplay({ ); - case 'BTC': - case 'fiat': + case Units.BTC: + case Units.fiat: if (rtl) { return ( @@ -173,7 +174,7 @@ export default class Amount extends React.Component { // display fiat amounts when rate fetch fails as $N/A if (unformattedAmount.error) { const amount = 'N/A'; - const unit = 'fiat'; + const unit = Units.fiat; const symbol = '$'; if (toggleable) { diff --git a/components/BlueWalletWarning.tsx b/components/BlueWalletWarning.tsx index 548459716..8a8ad9f0d 100644 --- a/components/BlueWalletWarning.tsx +++ b/components/BlueWalletWarning.tsx @@ -2,6 +2,7 @@ import { View } from 'react-native'; import { ErrorMessage } from './SuccessErrorMessage'; import stores from '../stores/Stores'; import { localeString } from '../utils/LocaleUtils'; +import { Implementation } from '../enums'; export default function BlueWalletWarning() { const SettingsStore = stores.settingsStore; @@ -9,7 +10,7 @@ export default function BlueWalletWarning() { SettingsStore.settings.nodes && SettingsStore.settings.nodes[SettingsStore.settings.selectedNode || 0]; const isLndHubIo = - node.implementation === 'lndhub' && + node.implementation === Implementation.lndhub && (node.lndhubUrl.includes('https://lndhub.io') || node.lndhubUrl.includes('https://lndhub.herokuapp.com')); diff --git a/components/Conversion.tsx b/components/Conversion.tsx index c85e55bdb..26d2fcc2e 100644 --- a/components/Conversion.tsx +++ b/components/Conversion.tsx @@ -13,6 +13,7 @@ import SettingsStore, { DEFAULT_FIAT } from '../stores/SettingsStore'; import { themeColor } from '../utils/ThemeUtils'; import ClockIcon from '../assets/images/SVG/Clock.svg'; +import { Units } from '../enums'; interface ConversionProps { amount: string | number; @@ -76,13 +77,13 @@ export default class Conversion extends React.Component< if (amount) { const amountStr = amount.toString(); switch (units) { - case 'sats': + case Units.sats: satAmount = amountStr; break; - case 'BTC': + case Units.BTC: satAmount = Number(amountStr) * SATS_PER_BTC; break; - case 'fiat': + case Units.fiat: satAmount = Number( (Number(amountStr.replace(/,/g, '.')) / Number(rate)) * Number(SATS_PER_BTC) @@ -96,7 +97,7 @@ export default class Conversion extends React.Component< if (!fiat || fiat === DEFAULT_FIAT || (!amount && !sats)) return; const ConversionDisplay = ({ - units = 'sats', + units = Units.sats, showRate }: { units: string; @@ -113,7 +114,7 @@ export default class Conversion extends React.Component< <> {` | ${getRate( - this.props.UnitsStore.units === 'sats' + this.props.UnitsStore.units === Units.sats )}`} @@ -122,7 +123,7 @@ export default class Conversion extends React.Component< ); const ConversionPendingDisplay = ({ - units = 'sats', + units = Units.sats, showRate }: { units: string; @@ -153,7 +154,7 @@ export default class Conversion extends React.Component< <> {` | ${getRate( - this.props.UnitsStore.units === 'sats' + this.props.UnitsStore.units === Units.sats )}`} @@ -165,31 +166,31 @@ export default class Conversion extends React.Component< // an on-chain debit is a negative number, but a lightning debit isn't return ( <> - {units === 'fiat' && ( + {units === Units.fiat && ( this.toggleShowRate()}> {satsPending ? ( ) : ( )} )} - {units !== 'fiat' && ( + {units !== Units.fiat && ( this.toggleShowRate()}> {satsPending ? ( ) : ( )} diff --git a/components/HopPicker.tsx b/components/HopPicker.tsx index 7acd6e722..07a62f9f9 100644 --- a/components/HopPicker.tsx +++ b/components/HopPicker.tsx @@ -20,9 +20,11 @@ import { ChannelItem } from './Channels/ChannelItem'; import Channel from '../models/Channel'; import stores from '../stores/Stores'; -import ChannelsStore, { ChannelsType } from '../stores/ChannelsStore'; +import ChannelsStore from '../stores/ChannelsStore'; import UnitsStore from '../stores/UnitsStore'; +import { ChannelsType } from '../enums'; + interface ChannelPickerProps { title?: string; displayValue?: string; diff --git a/components/LayerBalances/OnchainSwipeableRow.tsx b/components/LayerBalances/OnchainSwipeableRow.tsx index 94d5a3e82..f8f024cf3 100644 --- a/components/LayerBalances/OnchainSwipeableRow.tsx +++ b/components/LayerBalances/OnchainSwipeableRow.tsx @@ -17,6 +17,7 @@ import { themeColor } from './../../utils/ThemeUtils'; import Coins from './../../assets/images/SVG/Coins.svg'; import Receive from './../../assets/images/SVG/Receive.svg'; import Send from './../../assets/images/SVG/Send.svg'; +import { TransactionType } from '../../enums'; interface OnchainSwipeableRowProps { navigation: any; @@ -138,7 +139,7 @@ export default class OnchainSwipeableRow extends Component< navigation.navigate('Send', { destination: value, amount, - transactionType: 'On-chain' + transactionType: TransactionType.OnChain }); }; diff --git a/components/LayerBalances/index.tsx b/components/LayerBalances/index.tsx index a3bae90a6..8890919c3 100644 --- a/components/LayerBalances/index.tsx +++ b/components/LayerBalances/index.tsx @@ -19,6 +19,7 @@ import BlueWalletWarning from '../../components/BlueWalletWarning'; import OnChainSvg from '../../assets/images/SVG/DynamicSVG/OnChainSvg'; import LightningSvg from '../../assets/images/SVG/DynamicSVG/LightningSvg'; +import { TransactionType } from '../../enums'; interface LayerBalancesProps { BalanceStore: BalanceStore; @@ -46,7 +47,11 @@ const Row = ({ item }: { item: DataRow }) => ( }} > - {item.layer === 'On-chain' ? : } + {item.layer === TransactionType.OnChain ? ( + + ) : ( + + )} {item.layer} @@ -110,18 +115,18 @@ export default class LayerBalances extends Component { if (!BackendUtils.supportsOnchainReceiving()) { DATA = [ { - layer: 'Lightning', + layer: TransactionType.Lightning, balance: lightningBalance } ]; } else { DATA = [ { - layer: 'Lightning', + layer: TransactionType.Lightning, balance: lightningBalance }, { - layer: 'On-chain', + layer: TransactionType.OnChain, balance: totalBlockchainBalance } ]; diff --git a/components/NodeIdenticon.tsx b/components/NodeIdenticon.tsx index 7dfe928ae..e7bb466d1 100644 --- a/components/NodeIdenticon.tsx +++ b/components/NodeIdenticon.tsx @@ -5,6 +5,7 @@ import { SvgXml } from 'react-native-svg'; import Base64Utils from './../utils/Base64Utils'; import PrivacyUtils from './../utils/PrivacyUtils'; +import { Implementation } from '../enums'; const hash = require('object-hash'); @@ -16,7 +17,7 @@ export const NodeTitle = ( const displayName = selectedNode && selectedNode.nickname ? selectedNode.nickname - : selectedNode && selectedNode.implementation === 'lndhub' + : selectedNode && selectedNode.implementation === Implementation.lndhub ? selectedNode.lndhubUrl .replace('https://', '') .replace('http://', '') @@ -47,7 +48,7 @@ export default function NodeIdenticon({ const data = new Identicon( hash.sha1( - selectedNode && selectedNode.implementation === 'lndhub' + selectedNode && selectedNode.implementation === Implementation.lndhub ? `${title}-${selectedNode.username}` : title ), diff --git a/components/SetFeesForm.tsx b/components/SetFeesForm.tsx index 046ee9ce8..75e508550 100644 --- a/components/SetFeesForm.tsx +++ b/components/SetFeesForm.tsx @@ -17,6 +17,7 @@ import { themeColor } from './../utils/ThemeUtils'; import ChannelsStore from './../stores/ChannelsStore'; import FeeStore from './../stores/FeeStore'; import SettingsStore from './../stores/SettingsStore'; +import { Implementation } from '../enums'; interface SetFeesFormProps { FeeStore: FeeStore; @@ -142,7 +143,7 @@ export default class SetFeesForm extends React.Component< }} > {`${localeString('components.SetFeesForm.feeRate')} (${ - implementation === 'c-lightning-REST' + implementation === Implementation.clightningREST ? localeString( 'components.SetFeesForm.ppmMilliMsat' ) @@ -152,7 +153,7 @@ export default class SetFeesForm extends React.Component< { return ( + + + + + + ); + } +} + +const styles = StyleSheet.create({ + buttons: { + width: '100%' + }, + button: { + marginBottom: 20, + width: 350 + } +}); diff --git a/components/Modals/ExternalLinkModal.tsx b/components/Modals/ExternalLinkModal.tsx new file mode 100644 index 000000000..26d2c8af3 --- /dev/null +++ b/components/Modals/ExternalLinkModal.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import { View, StyleSheet, Text } from 'react-native'; +import { inject, observer } from 'mobx-react'; + +import Button from '../Button'; +import ModalBox from '../ModalBox'; +import CopyBox from '../../components/CopyBox'; + +import ModalStore from '../../stores/ModalStore'; + +import { localeString } from '../../utils/LocaleUtils'; + +import Leaving from '../../assets/images/SVG/Leaving.svg'; + +interface ExternalLinkModalProps { + ModalStore: ModalStore; +} + +@inject('ModalStore') +@observer +export default class ExternalLinkModal extends React.Component< + ExternalLinkModalProps, + {} +> { + render() { + const { ModalStore } = this.props; + const { + showExternalLinkModal, + modalUrl, + toggleExternalLinkModal, + onPress + } = ModalStore; + + return ( + + + + + + {localeString( + 'components.ExternalLinkModal.externalLink' + )} + + + {localeString( + 'components.ExternalLinkModal.proceed' + )} + + + + + + + + + + + + + + ); + } +} + +const styles = StyleSheet.create({ + buttons: { + width: '100%' + }, + button: { + marginBottom: 20, + width: 350 + } +}); diff --git a/components/PaymentPath.tsx b/components/PaymentPath.tsx new file mode 100644 index 000000000..8f9ad944d --- /dev/null +++ b/components/PaymentPath.tsx @@ -0,0 +1,192 @@ +import React, { useState } from 'react'; +import { Text, TouchableOpacity, View } from 'react-native'; + +import PrivacyUtils from '../utils/PrivacyUtils'; +import { themeColor } from '../utils/ThemeUtils'; + +import Amount from './Amount'; +import KeyValue from './KeyValue'; +import { Row } from './layout/Row'; + +import CaretDown from '../assets/images/SVG/Caret Down.svg'; +import CaretRight from '../assets/images/SVG/Caret Right.svg'; + +interface PaymentPathProps { + value?: any; +} + +export default function PaymentPath(props: PaymentPathProps) { + const { value } = props; + const paths: any = []; + const [expanded, setExpanded] = useState(new Map()); + const updateMap = (k: number, v: boolean) => { + setExpanded(new Map(expanded.set(k, v))); + }; + value.map((path: any, index: number) => { + const hops: any = []; + let title = ''; + path.map((hop: any, key: number) => { + title += + hop.node.length >= 66 + ? `${PrivacyUtils.sensitiveValue(hop.node).slice(0, 6)}...` + : PrivacyUtils.sensitiveValue(hop.node); + if (key + 1 !== path.length) { + title += ', '; + } + }); + if (value.length > 1) { + hops.push( + updateMap(index, !expanded.get(index))} + > + + + + {expanded.get(index) ? ( + + ) : ( + + )} + + + {title} + + + + {path.length} + + + + + + ); + } + path.map((hop: any, key: number) => { + (expanded.get(index) || value.length === 1) && + hops.push( + + + + + {`${key + 1}`} + + + + {`${ + hop.node.length >= 66 + ? `${ + PrivacyUtils.sensitiveValue( + hop.node + ).slice(0, 14) + + '...' + + PrivacyUtils.sensitiveValue( + hop.node + ).slice(-14) + }` + : PrivacyUtils.sensitiveValue(hop.node) + }`} + + + + + + } + sensitive + /> + + } + sensitive + /> + + + ); + }); + paths.push(hops); + }); + + return {paths}; +} diff --git a/components/QRCodeScanner.tsx b/components/QRCodeScanner.tsx index 507d8f3c6..cc64a1d89 100644 --- a/components/QRCodeScanner.tsx +++ b/components/QRCodeScanner.tsx @@ -1,7 +1,17 @@ import * as React from 'react'; -import { Dimensions, StyleSheet, Text, View } from 'react-native'; -import { BarCodeReadEvent, FlashMode, RNCamera } from 'react-native-camera'; +import { + Dimensions, + StyleSheet, + Text, + View, + Platform, + TouchableOpacity, + PermissionsAndroid +} from 'react-native'; +import { Camera } from 'react-native-camera-kit'; +import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions'; import { launchImageLibrary } from 'react-native-image-picker'; + const LocalQRCode = require('@remobile/react-native-qrcode-local-image'); import Button from './../components/Button'; @@ -19,46 +29,31 @@ interface QRProps { text?: string; handleQRScanned: any; goBack: any; + navigation: any; } interface QRState { cameraStatus: any; - torch: FlashMode; + isTorchOn: boolean; } +const CameraAuthStatus = Object.freeze({ + AUTHORIZED: 'AUTHORIZED', + NOT_AUTHORIZED: 'NOT_AUTHORIZED', + UNKNOWN: 'UNKNOWN' +}); export default class QRCodeScanner extends React.Component { constructor(props: QRProps) { super(props); this.state = { - cameraStatus: null, - torch: RNCamera.Constants.FlashMode.off + cameraStatus: CameraAuthStatus.UNKNOWN, + isTorchOn: false }; } scannedCache: { [name: string]: number } = {}; maskLength = (Dimensions.get('window').width * 80) / 100; - handleCameraStatusChange = (event: any) => { - this.setState((state) => { - return { - ...state, - cameraStatus: event.cameraStatus - }; - }); - }; - - handleFlash = () => { - this.setState((state) => { - return { - ...state, - torch: - this.state.torch === RNCamera.Constants.FlashMode.torch - ? RNCamera.Constants.FlashMode.off - : RNCamera.Constants.FlashMode.torch - }; - }); - }; - handleRead = (data: any) => { const hash = createHash('sha256').update(data).digest().toString('hex'); if (this.scannedCache[hash]) { @@ -90,99 +85,161 @@ export default class QRCodeScanner extends React.Component { } ); }; + onQRCodeScan = (event: { nativeEvent: { codeStringValue: any } }) => { + this.handleRead(event.nativeEvent.codeStringValue); + }; + + toggleTorch = async () => { + const { isTorchOn } = this.state; + try { + this.setState({ isTorchOn: !isTorchOn }); + } catch (error) { + console.log('Error toggling torch: ', error); + } + }; + + async componentDidMount() { + // triggers when loaded from navigation or back action + this.props.navigation.addListener('didFocus', () => { + this.scannedCache = {}; + }); + + if (Platform.OS !== 'ios' && Platform.OS !== 'macos') { + // For android + // Returns true or false + const permissionAndroid = await PermissionsAndroid.check( + 'android.permission.CAMERA' + ); + if (permissionAndroid) { + this.setState({ + cameraStatus: CameraAuthStatus.AUTHORIZED + }); + } else + try { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.CAMERA, + { + title: localeString( + 'components.QRCodeScanner.cameraPermissionTitle' + ), + message: localeString( + 'components.QRCodeScanner.cameraPermission' + ), + buttonNegative: localeString('general.cancel'), + buttonPositive: localeString('general.ok') + } + ); + if (granted === PermissionsAndroid.RESULTS.GRANTED) { + this.setState({ + cameraStatus: CameraAuthStatus.AUTHORIZED + }); + } else { + this.setState({ + cameraStatus: CameraAuthStatus.NOT_AUTHORIZED + }); + } + } catch (err) { + console.warn(err); + } + + return; + } + // Camera permission for IOS + else { + const cameraPermission = PERMISSIONS.IOS.CAMERA; + const status = await check(cameraPermission); + + if (status === RESULTS.GRANTED) { + this.setState({ cameraStatus: CameraAuthStatus.AUTHORIZED }); + } else if (status === RESULTS.DENIED) { + const result = await request(cameraPermission); + + if (result === RESULTS.GRANTED) { + this.setState({ + cameraStatus: CameraAuthStatus.AUTHORIZED + }); + } else { + this.setState({ + cameraStatus: CameraAuthStatus.NOT_AUTHORIZED + }); + } + } else { + this.setState({ + cameraStatus: CameraAuthStatus.NOT_AUTHORIZED + }); + } + } + } + + componentWillUnmount() { + this.props.navigation.removeListener && + this.props.navigation.removeListener('didFocus'); + } render() { - const { cameraStatus } = this.state; + const { cameraStatus, isTorchOn } = this.state; const { text, goBack } = this.props; return ( <> - {cameraStatus !== - RNCamera.Constants.CameraStatus.NOT_AUTHORIZED && ( + {cameraStatus === CameraAuthStatus.AUTHORIZED && ( - - this.handleRead(ret.data) - } - androidCameraPermissionOptions={{ - title: localeString( - 'components.QRCodeScanner.cameraPermissionTitle' - ), - message: localeString( - 'components.QRCodeScanner.cameraPermission' - ), - buttonPositive: localeString('general.ok'), - buttonNegative: localeString('general.cancel') + scanBarcode={true} + torchMode={isTorchOn ? 'on' : 'off'} + onReadCode={this.onQRCodeScan} + focusMode="off" + /> + + + {isTorchOn ? ( + + ) : ( + + )} + + + + + + {text !== undefined && ( + {text} + )} + - - - - {this.state.torch === - RNCamera.Constants.FlashMode.torch ? ( - - ) : ( - - )} - - - - {text} - - - - - - + + - - -