From e4339e137453ee609644b4cabdb23b88729a8025 Mon Sep 17 00:00:00 2001 From: Evan Kaloudis Date: Wed, 15 Mar 2023 01:47:12 -0400 Subject: [PATCH] Settings: Invoices --- Navigation.ts | 4 + locales/en.json | 1 + stores/SettingsStore.ts | 20 +- views/Receive.tsx | 47 +++- views/Settings/InvoicesSettings.tsx | 348 ++++++++++++++++++++++++++++ views/Settings/Settings.tsx | 70 +++++- 6 files changed, 478 insertions(+), 12 deletions(-) create mode 100644 views/Settings/InvoicesSettings.tsx diff --git a/Navigation.ts b/Navigation.ts index 4062ee973..431b922ca 100644 --- a/Navigation.ts +++ b/Navigation.ts @@ -52,6 +52,7 @@ import PointOfSale from './views/Settings/PointOfSale'; import PointOfSaleRecon from './views/Settings/PointOfSaleRecon'; import PointOfSaleReconExport from './views/Settings/PointOfSaleReconExport'; import PaymentsSettings from './views/Settings/PaymentsSettings'; +import InvoicesSettings from './views/Settings/InvoicesSettings'; // Routing import Routing from './views/Routing/Routing'; @@ -261,6 +262,9 @@ const AppScenes = { }, PaymentsSettings: { screen: PaymentsSettings + }, + InvoicesSettings: { + screen: InvoicesSettings } }; diff --git a/locales/en.json b/locales/en.json index 37cd9d43b..15db84e4f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -482,6 +482,7 @@ "views.Settings.Privacy.title": "Privacy settings", "views.Settings.Payments.title": "Payments settings", "views.Settings.Payments.defaultFeeLimit": "Default Fee Limit", + "views.Settings.Invoices.title": "Invoices settings", "views.Settings.Privacy.blockExplorer": "Default Block explorer", "views.Settings.Privacy.customBlockExplorer": "Custom Block explorer", "views.Settings.Privacy.lurkerMode": "Lurker mode", diff --git a/stores/SettingsStore.ts b/stores/SettingsStore.ts index 344d94824..14c4c5ffe 100644 --- a/stores/SettingsStore.ts +++ b/stores/SettingsStore.ts @@ -50,12 +50,20 @@ interface PosSettings { squareDevMode?: boolean; } -interface PaymentSettings { +interface PaymentsSettings { defaultFeeMethod?: string; defaultFeePercentage?: string; defaultFeeFixed?: string; } +interface InvoicesSettings { + addressType?: string; + memo?: string; + expiry?: string; + routeHints?: boolean; + ampInvoice?: boolean; +} + export interface Settings { nodes?: Array; selectedNode?: number; @@ -71,7 +79,8 @@ export interface Settings { privacy: PrivacySettings; display: DisplaySettings; pos: PosSettings; - payments: PaymentSettings; + payments: PaymentsSettings; + invoices: InvoicesSettings; isBiometryEnabled: boolean; supportedBiometryType?: BiometryType; lndHubLnAuthMode?: string; @@ -234,6 +243,13 @@ export default class SettingsStore { defaultFeePercentage: '0.5', defaultFeeFixed: '100' }, + invoices: { + addressType: '1', + memo: '', + expiry: '3600', + routeHints: false, + ampInvoice: false + }, supportedBiometryType: undefined, isBiometryEnabled: false, scramblePin: true, diff --git a/views/Receive.tsx b/views/Receive.tsx index f91a83536..97fac90d5 100644 --- a/views/Receive.tsx +++ b/views/Receive.tsx @@ -113,6 +113,20 @@ export default class Receive extends React.Component< rate: 0 }; + async UNSAFE_componentWillMount() { + const { SettingsStore } = this.props; + const { getSettings } = SettingsStore; + const settings = await getSettings(); + + this.setState({ + addressType: settings.invoices.addressType || '1', + memo: settings.invoices.memo || '', + expiry: settings.invoices.expiry || '3600', + routeHints: settings.invoices.routeHints || false, + ampInvoice: settings.invoices.ampInvoice || false + }); + } + async componentDidMount() { const { navigation, InvoicesStore, SettingsStore } = this.props; const { reset } = InvoicesStore; @@ -132,8 +146,10 @@ export default class Receive extends React.Component< const amount: string = navigation.getParam('amount'); const autoGenerate: boolean = navigation.getParam('autoGenerate'); + const { expiry, routeHints, ampInvoice, addressType } = this.state; + // POS - const memo: string = navigation.getParam('memo'); + const memo: string = navigation.getParam('memo', this.state.memo); const orderId: string = navigation.getParam('orderId'); const orderTotal: string = navigation.getParam('orderTotal'); const orderTip: string = navigation.getParam('orderTip'); @@ -170,7 +186,14 @@ export default class Receive extends React.Component< } if (autoGenerate) - this.autoGenerateInvoice(this.getSatAmount(amount), memo); + this.autoGenerateInvoice( + this.getSatAmount(amount), + memo, + expiry, + routeHints, + ampInvoice, + addressType + ); if (Platform.OS === 'android') { await this.enableNfc(); @@ -219,19 +242,27 @@ export default class Receive extends React.Component< }); }; - autoGenerateInvoice = (amount?: string, memo?: string) => { + autoGenerateInvoice = ( + amount?: string, + memo?: string, + expiry?: string, + routeHints?: boolean, + ampInvoice?: boolean, + addressType?: string + ) => { const { InvoicesStore } = this.props; const { createUnifiedInvoice } = InvoicesStore; - const { expiry, ampInvoice, routeHints, addressType } = this.state; createUnifiedInvoice( memo || '', amount || '0', - expiry, + expiry || '3600', undefined, - ampInvoice, - routeHints, - BackendUtils.supportsAddressTypeSelection() ? addressType : null + ampInvoice || false, + routeHints || false, + BackendUtils.supportsAddressTypeSelection() + ? addressType || '1' + : undefined ).then( ({ rHash, diff --git a/views/Settings/InvoicesSettings.tsx b/views/Settings/InvoicesSettings.tsx new file mode 100644 index 000000000..49d393021 --- /dev/null +++ b/views/Settings/InvoicesSettings.tsx @@ -0,0 +1,348 @@ +import * as React from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { Header, Icon } from 'react-native-elements'; +import { inject, observer } from 'mobx-react'; +import _map from 'lodash/map'; + +import LoadingIndicator from '../../components/LoadingIndicator'; +import ModalBox from '../../components/ModalBox'; +import Switch from '../../components/Switch'; +import TextInput from '../../components/TextInput'; + +import SettingsStore from '../../stores/SettingsStore'; + +import BackendUtils from '../../utils/BackendUtils'; +import { localeString } from '../../utils/LocaleUtils'; +import { themeColor } from '../../utils/ThemeUtils'; + +interface InvoicesSettingsProps { + navigation: any; + SettingsStore: SettingsStore; +} + +interface InvoicesSettingsState { + addressType: string; + memo: string; + expiry: string; + routeHints: boolean; + ampInvoice: boolean; +} + +@inject('SettingsStore') +@observer +export default class InvoicesSettings extends React.Component< + InvoicesSettingsProps, + InvoicesSettingsState +> { + state = { + addressType: '0', + memo: '', + expiry: '3600', + routeHints: false, + ampInvoice: false + }; + + async UNSAFE_componentWillMount() { + const { SettingsStore } = this.props; + const { getSettings } = SettingsStore; + const settings = await getSettings(); + + this.setState({ + addressType: settings.invoices.addressType || '0', + memo: settings.invoices.memo || '', + expiry: settings.invoices.expiry || '3600', + routeHints: settings.invoices.routeHints || false, + ampInvoice: settings.invoices.ampInvoice || false + }); + } + + handleSave = async () => { + const { addressType, memo, expiry, routeHints, ampInvoice } = + this.state; + const { SettingsStore } = this.props; + const { updateSettings } = SettingsStore; + await updateSettings({ + invoices: { + addressType, + memo, + expiry, + routeHints, + ampInvoice + } + }); + }; + + renderSeparator = () => ( + + ); + + render() { + const { navigation, SettingsStore } = this.props; + const { addressType, memo, expiry, routeHints, ampInvoice } = + this.state; + const { implementation, loading }: any = SettingsStore; + + const ADDRESS_TYPES = BackendUtils.supportsTaproot() + ? [ + { + key: localeString('views.Receive.np2wkhKey'), + value: '1', + description: localeString( + 'views.Receive.np2wkhDescription' + ) + }, + { + key: localeString('views.Receive.p2wkhKey'), + value: '0', + description: localeString( + 'views.Receive.p2wkhDescription' + ) + }, + { + key: localeString('views.Receive.p2trKey'), + value: '4', + description: localeString('views.Receive.p2trDescription') + } + ] + : [ + { + key: localeString('views.Receive.np2wkhKey'), + value: '1', + description: localeString( + 'views.Receive.np2wkhDescriptionAlt' + ) + }, + { + key: localeString('views.Receive.p2wkhKey'), + value: '0', + description: localeString( + 'views.Receive.p2wkhDescription' + ) + } + ]; + + const BackButton = () => ( + { + this.handleSave(); + navigation.navigate('Settings', { + refresh: true + }); + }} + color={themeColor('text')} + underlayColor="transparent" + /> + ); + + const SettingsButton = () => ( + this.refs.modal.open()} + color={themeColor('text')} + underlayColor="transparent" + /> + ); + + return ( + +
} + centerComponent={{ + text: localeString('views.Settings.Invoices.title'), + style: { + color: themeColor('text'), + fontFamily: 'Lato-Regular' + } + }} + rightComponent={ + BackendUtils.supportsAddressTypeSelection() && ( + + ) + } + backgroundColor={themeColor('background')} + containerStyle={{ + borderBottomWidth: 0 + }} + /> + {loading ? ( + + ) : ( + + + {localeString('views.Receive.memo')} + + { + this.setState({ memo: text }); + }} + /> + + {implementation !== 'lndhub' && ( + <> + + {localeString('views.Receive.expiration')} + + + this.setState({ + expiry: text + }) + } + /> + + )} + + {BackendUtils.isLNDBased() && ( + <> + + {localeString('views.Receive.routeHints')} + + + this.setState({ + routeHints: !routeHints + }) + } + /> + + )} + + {BackendUtils.supportsAMP() && ( + <> + + {localeString('views.Receive.ampInvoice')} + + + this.setState({ + ampInvoice: !ampInvoice + }) + } + /> + + )} + + )} + + + {localeString('views.Receive.addressType')} + + {_map(ADDRESS_TYPES, (d, index) => ( + { + this.setState({ addressType: d.value }); + this.refs.modal.close(); + }} + style={{ + backgroundColor: themeColor('secondary'), + borderColor: + d.value === addressType + ? themeColor('highlight') + : themeColor('secondaryText'), + borderRadius: 4, + borderWidth: d.value === addressType ? 2 : 1, + padding: 16, + marginBottom: 24 + }} + > + + {d.key} + + + {d.description} + + + ))} + + + ); + } +} + +const styles = StyleSheet.create({ + secondaryText: { + fontFamily: 'Lato-Regular' + } +}); diff --git a/views/Settings/Settings.tsx b/views/Settings/Settings.tsx index c6eaa0efa..49cbd204b 100644 --- a/views/Settings/Settings.tsx +++ b/views/Settings/Settings.tsx @@ -22,6 +22,7 @@ import LanguageIcon from '../../assets/images/SVG/Globe.svg'; import HelpIcon from '../../assets/images/SVG/Help Icon.svg'; import NodeOn from '../../assets/images/SVG/Node On.svg'; import POS from '../../assets/images/SVG/POS.svg'; +import ReceiveIcon from '../../assets/images/SVG/Receive.svg'; import SendIcon from '../../assets/images/SVG/Send.svg'; import NodeIdenticon, { NodeTitle } from './../../components/NodeIdenticon'; @@ -64,7 +65,7 @@ export default class Settings extends React.Component< render() { const { navigation, SettingsStore } = this.props; const { showHiddenSettings, easterEggCount } = this.state; - const { settings } = SettingsStore; + const { implementation, settings } = SettingsStore; const selectedNode: any = (settings && @@ -283,7 +284,6 @@ export default class Settings extends React.Component< style={{ backgroundColor: themeColor('secondary'), width: '90%', - height: 45, borderRadius: 10, alignSelf: 'center', marginBottom: 15 @@ -310,8 +310,74 @@ export default class Settings extends React.Component< + + + + navigation.navigate('InvoicesSettings') + } + > + + + + + {localeString( + 'views.Wallet.Wallet.invoices' + )} + + + + + )} + {selectedNode && + !BackendUtils.isLNDBased() && + implementation !== 'lndhub' && ( + + + navigation.navigate('InvoicesSettings') + } + > + + + + + {localeString( + 'views.Wallet.Wallet.invoices' + )} + + + + + + + )} {selectedNode && BackendUtils.supportsMessageSigning() ? (