From 3f33a5c2ee540f1f6da1640dc8399b978f9c42d1 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 26 Jan 2024 16:36:39 +0100 Subject: [PATCH] feat(settings): add app status screen in support --- src/assets/icons/settings.ts | 28 ++ src/navigation/settings/SettingsNavigator.tsx | 3 + src/screens/Settings/AppStatus/index.tsx | 251 ++++++++++++++++++ src/screens/Settings/SettingsView.tsx | 4 +- .../Settings/SupportSettings/index.tsx | 6 + src/styles/icons.ts | 33 +++ src/utils/i18n/locales/en/settings.json | 34 +++ src/utils/lightning/index.ts | 2 + 8 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 src/screens/Settings/AppStatus/index.tsx diff --git a/src/assets/icons/settings.ts b/src/assets/icons/settings.ts index fcb8281da..49977bacc 100644 --- a/src/assets/icons/settings.ts +++ b/src/assets/icons/settings.ts @@ -45,6 +45,34 @@ export const githubIcon = (color = 'white'): string => export const globeIcon = (color = 'white'): string => ``; +export const globeSimpleIcon = (color = 'white'): string => + ` + + + + + + `; + +export const broadcastIcon = (color = 'white'): string => + ` + + + + + + + + `; + +export const cloudCheckIcon = (color = 'white'): string => + ` + + + + + `; + export const mediumIcon = (color = 'white'): string => ``; diff --git a/src/navigation/settings/SettingsNavigator.tsx b/src/navigation/settings/SettingsNavigator.tsx index ec29c84d4..4180886db 100644 --- a/src/navigation/settings/SettingsNavigator.tsx +++ b/src/navigation/settings/SettingsNavigator.tsx @@ -53,6 +53,7 @@ import LightningNavigator, { } from '../lightning/LightningNavigator'; import WebRelay from '../../screens/Settings/WebRelay'; import { __E2E__ } from '../../constants/env'; +import AppStatus from '../../screens/Settings/AppStatus'; export type SettingsNavigationProp = StackNavigationProp; @@ -73,6 +74,7 @@ export type SettingsStackParamList = { AdvancedSettings: undefined; AboutSettings: undefined; SupportSettings: undefined; + AppStatus: undefined; ReportIssue: undefined; FormSuccess: undefined; FormError: undefined; @@ -131,6 +133,7 @@ const SettingsNavigator = (): ReactElement => { + diff --git a/src/screens/Settings/AppStatus/index.tsx b/src/screens/Settings/AppStatus/index.tsx new file mode 100644 index 000000000..b19fd6c31 --- /dev/null +++ b/src/screens/Settings/AppStatus/index.tsx @@ -0,0 +1,251 @@ +import { SettingsScreenProps } from '../../../navigation/types'; +import React, { memo, ReactElement, useEffect, useMemo, useState } from 'react'; +import { ScrollView, View as ThemedView } from '../../../styles/components'; +import SettingsView from '../SettingsView'; +import { Caption13M, Text01M } from '../../../styles/text'; +import { StyleSheet, View } from 'react-native'; +import { useTranslation } from 'react-i18next'; +import { + BitcoinSlantedIcon, + BroadcastIcon, + CloudCheckIcon, + GlobeSimpleIcon, + LightningHollow, +} from '../../../styles/icons'; +import { IColors } from '../../../styles/colors'; +import { useAppSelector } from '../../../hooks/redux'; +import { + isConnectedToElectrumSelector, + isLDKReadySelector, + isOnlineSelector, +} from '../../../store/reselect/ui'; +import { + openChannelsSelector, + pendingChannelsSelector, +} from '../../../store/reselect/lightning'; +import { blocktankPaidOrdersFullSelector } from '../../../store/reselect/blocktank'; +import { backupSelector } from '../../../store/reselect/backup'; +import { i18nTime } from '../../../utils/i18n'; +import { FAILED_BACKUP_CHECK_TIME } from '../../../utils/backup/backups-subscriber'; + +type TStatusItem = + | 'internet' + | 'bitcoin_node' + | 'lightning_node' + | 'lightning_connection' + | 'full_backup'; + +type TItemState = 'ready' | 'pending' | 'error'; + +interface IStatusItemProps { + Icon: React.FunctionComponent; + item: TStatusItem; + state: TItemState; + subtitle?: string; +} + +const Status = ({ + Icon, + item, + state, + subtitle, +}: IStatusItemProps): ReactElement => { + const { t } = useTranslation('settings'); + const { bg, fg }: { fg: keyof IColors; bg: keyof IColors } = useMemo(() => { + switch (state) { + case 'ready': + return { bg: 'green16', fg: 'green' }; + case 'pending': + return { bg: 'yellow16', fg: 'yellow' }; + case 'error': + return { bg: 'red16', fg: 'red' }; + } + }, [state]); + + subtitle = subtitle || t(`status.${item}.${state}`); + + return ( + + + + + + + + {t(`status.${item}.title`)} + {subtitle} + + + ); +}; + +const AppStatus = ({}: SettingsScreenProps<'AppStatus'>): ReactElement => { + const { t } = useTranslation('settings'); + const { t: tTime } = useTranslation('intl', { i18n: i18nTime }); + const isOnline = useAppSelector(isOnlineSelector); + const isConnectedToElectrum = useAppSelector(isConnectedToElectrumSelector); + const isLDKReady = useAppSelector(isLDKReadySelector); + const openChannels = useAppSelector(openChannelsSelector); + const pendingChannels = useAppSelector(pendingChannelsSelector); + const paidOrders = useAppSelector(blocktankPaidOrdersFullSelector); + const backup = useAppSelector(backupSelector); + const [now, setNow] = useState(new Date().getTime()); + + const internetState: TItemState = useMemo(() => { + return isOnline ? 'ready' : 'error'; + }, [isOnline]); + + const bitcoinNodeState: TItemState = useMemo(() => { + if (isOnline && !isConnectedToElectrum) { + return 'pending'; + } + return isConnectedToElectrum ? 'ready' : 'error'; + }, [isConnectedToElectrum, isOnline]); + + const lightningNodeState: TItemState = useMemo(() => { + return isLDKReady ? 'ready' : 'error'; + }, [isLDKReady]); + + const lightningConnectionState: TItemState = useMemo(() => { + if (openChannels.length > 0) { + return 'ready'; + } else if ( + pendingChannels.length > 0 || + Object.keys(paidOrders.created).length > 0 + ) { + return 'pending'; + } + return 'error'; + }, [openChannels, pendingChannels, paidOrders]); + + // Keep checking backup status + useEffect(() => { + const timer = setInterval(() => { + setNow(new Date().getTime()); + }, FAILED_BACKUP_CHECK_TIME); + + return (): void => clearInterval(timer); + }, []); + + const isBackupSyncOk = useMemo(() => { + const isSyncOk = (key: number | undefined): boolean => { + // undefined = no sync required = ok + return key ? now - key < FAILED_BACKUP_CHECK_TIME : true; + }; + + return ( + isSyncOk(backup.remoteLdkBackupLastSyncRequired) && + isSyncOk(backup.remoteLdkActivityBackupSyncRequired) && + isSyncOk(backup.remoteBlocktankBackupSyncRequired) && + isSyncOk(backup.remoteSettingsBackupSyncRequired) && + isSyncOk(backup.remoteMetadataBackupSyncRequired) && + isSyncOk(backup.remoteWidgetsBackupSyncRequired) + ); + }, [backup, now]); + + const fullBackupState: { state: TItemState; subtitle?: string } = + useMemo(() => { + if (!isBackupSyncOk) { + return { state: 'error' }; + } + const syncTimes = [ + backup.remoteLdkBackupLastSync, + backup.remoteLdkActivityBackupLastSync, + backup.remoteBlocktankBackupLastSync, + backup.remoteSettingsBackupLastSync, + backup.remoteMetadataBackupLastSync, + backup.remoteWidgetsBackupLastSync, + ].filter((i) => i !== undefined) as Array; + const max = Math.max(...syncTimes); + let subtitle = tTime('dateTime', { + v: new Date(max), + formatParams: { + v: { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + }, + }, + }); + return { state: 'ready', subtitle }; + }, [tTime, backup, isBackupSyncOk]); + + const items: IStatusItemProps[] = [ + { + Icon: GlobeSimpleIcon, + item: 'internet', + state: internetState, + }, + { + Icon: BitcoinSlantedIcon, + item: 'bitcoin_node', + state: bitcoinNodeState, + }, + { + Icon: BroadcastIcon, + item: 'lightning_node', + state: lightningNodeState, + }, + { + Icon: LightningHollow, + item: 'lightning_connection', + state: lightningConnectionState, + }, + { + Icon: CloudCheckIcon, + item: 'full_backup', + state: fullBackupState.state, + subtitle: fullBackupState.subtitle, + }, + ]; + + return ( + + + + {items.map((it) => ( + + ))} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + statusRoot: { + flex: 1, + }, + status: { + marginHorizontal: 16, + borderBottomWidth: 1, + borderBottomColor: 'rgba(255, 255, 255, 0.1)', + height: 56, + flexDirection: 'row', + alignItems: 'center', + }, + iconContainer: { + marginRight: 16, + alignItems: 'center', + }, + icon: { + alignItems: 'center', + justifyContent: 'center', + borderRadius: 16, + width: 32, + height: 32, + }, + desc: { + flex: 1, + }, +}); + +export default memo(AppStatus); diff --git a/src/screens/Settings/SettingsView.tsx b/src/screens/Settings/SettingsView.tsx index bb7576ca5..9adc37002 100644 --- a/src/screens/Settings/SettingsView.tsx +++ b/src/screens/Settings/SettingsView.tsx @@ -81,7 +81,9 @@ const SettingsView = ({ )} - {children && childrenPosition === 'top' && {children}} + {children && childrenPosition === 'top' && ( + {children} + )} {listData && ( navigation.navigate('AppStatus'), + testID: 'AppStatus', + }, ], }, ], diff --git a/src/styles/icons.ts b/src/styles/icons.ts index 19993d6e8..4d525fc7d 100644 --- a/src/styles/icons.ts +++ b/src/styles/icons.ts @@ -88,6 +88,9 @@ import { emailIcon, githubIcon, globeIcon, + globeSimpleIcon, + broadcastIcon, + cloudCheckIcon, mediumIcon, twitterIcon, listIcon, @@ -727,6 +730,36 @@ export const GlobeIcon = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); +export const GlobeSimpleIcon = styled(SvgXml).attrs((props) => ({ + xml: globeSimpleIcon( + props.color ? props.theme.colors[props.color] : props.theme.colors.brand, + ), + height: props.height ?? '16px', + width: props.width ?? '16px', +}))((props) => ({ + color: props.color ? props.theme.colors[props.color] : 'white', +})); + +export const BroadcastIcon = styled(SvgXml).attrs((props) => ({ + xml: broadcastIcon( + props.color ? props.theme.colors[props.color] : props.theme.colors.brand, + ), + height: props.height ?? '16px', + width: props.width ?? '16px', +}))((props) => ({ + color: props.color ? props.theme.colors[props.color] : 'white', +})); + +export const CloudCheckIcon = styled(SvgXml).attrs((props) => ({ + xml: cloudCheckIcon( + props.color ? props.theme.colors[props.color] : props.theme.colors.brand, + ), + height: props.height ?? '16px', + width: props.width ?? '16px', +}))((props) => ({ + color: props.color ? props.theme.colors[props.color] : 'white', +})); + export const MediumIcon = styled(SvgXml).attrs((props) => ({ xml: mediumIcon( props.color ? props.theme.colors[props.color] : props.theme.colors.brand, diff --git a/src/utils/i18n/locales/en/settings.json b/src/utils/i18n/locales/en/settings.json index 9f3d47976..483da67bb 100644 --- a/src/utils/i18n/locales/en/settings.json +++ b/src/utils/i18n/locales/en/settings.json @@ -85,6 +85,7 @@ "text": "Need help? Report your issue from within Bitkit, visit the help center or reach out to us on our social channels.", "report": "Report Issue", "help": "Help Center", + "status": "App Status", "help_url": "https://help.bitkit.to", "report_text": "Please describe the issue you are experiencing or ask a general question.", "label_address": "Email address", @@ -99,6 +100,39 @@ "text_unsuccess": "Something went wrong while trying to send your issue or question. Please try again.", "text_unsuccess_button": "Try Again" }, + "status": { + "title": "App Status", + "internet": { + "title": "Internet", + "ready": "Connected", + "pending": "Reconnecting...", + "error": "Disconnected" + }, + "bitcoin_node" : { + "title": "Bitcoin Node", + "ready": "Connected", + "pending": "Connecting...", + "error": "Could not connect to Electrum" + }, + "lightning_node" : { + "title": "Lightning Node", + "ready": "Synced", + "pending": "Syncing...", + "error": "Could not initiate" + }, + "lightning_connection" : { + "title": "Lightning Connection", + "ready": "Open", + "pending": "Opening...", + "error": "No open connections" + }, + "full_backup" : { + "title": "Latest Full Data Backup", + "ready": "Backed up", + "pending": "Backing up...", + "error": "Failed to complete a full backup" + } + }, "adv": { "section_payments": "Payments", "section_networks": "Networks", diff --git a/src/utils/lightning/index.ts b/src/utils/lightning/index.ts index 840f09d26..05a2e471f 100644 --- a/src/utils/lightning/index.ts +++ b/src/utils/lightning/index.ts @@ -195,6 +195,7 @@ export const setupLdk = async ({ if (shouldPreemptivelyStopLdk) { // start from a clean slate await ldk.stop(); + dispatch(updateUi({ isLDKReady: false })); } const accountVersion = await checkAccountVersion( @@ -535,6 +536,7 @@ export const refreshLdk = async ({ const isRunning = await isLdkRunning(); if (!isRunning) { + dispatch(updateUi({ isLDKReady: false })); // Attempt to setup and start LDK. const setupResponse = await setupLdk({ selectedNetwork,