diff --git a/app/components/ShockAvatar.tsx b/app/components/ShockAvatar.tsx index 6a48c1a7..c4f29f16 100644 --- a/app/components/ShockAvatar.tsx +++ b/app/components/ShockAvatar.tsx @@ -27,9 +27,9 @@ interface OwnProps { interface StateProps { image: string | null - lastSeenApp: number | null isOwn: boolean displayName: string | null + showGreenRing: boolean } interface DispatchProps {} @@ -74,10 +74,9 @@ class ShockAvatar extends React.PureComponent { const { height, image, - lastSeenApp, - disableOnlineRing, nameAtBottom, displayName, + showGreenRing, } = this.props return ( @@ -95,11 +94,7 @@ class ShockAvatar extends React.PureComponent { } } onPress={this.onPress} - avatarStyle={ - lastSeenApp && isOnline(lastSeenApp) && !disableOnlineRing - ? styles.onlineRing - : undefined - } + avatarStyle={showGreenRing ? styles.onlineRing : undefined} /> {nameAtBottom ? ( <> @@ -135,15 +130,17 @@ const makeMapStateToProps = () => { const getUser = Store.makeGetUser() const f = (state: Reducers.State, ownProps: OwnProps): StateProps => { - const { publicKey } = ownProps + const { publicKey, disableOnlineRing } = ownProps const user = getUser(state, publicKey) const myPublicKey = Store.getMyPublicKey(state) + const isOwn = user.publicKey === myPublicKey + const appOnline = isOwn ? Store.isOnline(state) : isOnline(user.lastSeenApp) return { image: user.avatar, - lastSeenApp: user.lastSeenApp, - isOwn: user.publicKey === myPublicKey, + isOwn, displayName: user.displayName, + showGreenRing: appOnline && !disableOnlineRing, } } diff --git a/app/screens/Advanced/Accordion/Transaction.js b/app/screens/Advanced/Accordion/Transaction.js index 7296fe1e..897a205b 100644 --- a/app/screens/Advanced/Accordion/Transaction.js +++ b/app/screens/Advanced/Accordion/Transaction.js @@ -1,16 +1,14 @@ import React from 'react' import { View, Text, StyleSheet, Clipboard, ToastAndroid } from 'react-native' import EntypoIcons from 'react-native-vector-icons/Entypo' +import * as Common from 'shock-common' import * as CSS from '../../../res/css' import TimeText from '../../../components/time-text' -/** - * @typedef {import('../../../services/wallet').Transaction} ITransaction - */ /** * @typedef {object} Props - * @prop {ITransaction} data + * @prop {Common.Schema.ChainTransaction} data */ /** @@ -55,7 +53,7 @@ const _Transaction = ({ data }) => (( Payment{' '} - {parseInt(data.num_confirmations, 10) === 0 ? ( + {data.num_confirmations === 0 ? ( ) : null} diff --git a/app/screens/Advanced/Accordion/index.js b/app/screens/Advanced/Accordion/index.js index a4b8fecf..47ca336d 100644 --- a/app/screens/Advanced/Accordion/index.js +++ b/app/screens/Advanced/Accordion/index.js @@ -315,15 +315,7 @@ const styles = StyleSheet.create({ color: CSS.Colors.BLUE_DARK, fontWeight: 'bold', }, - // accordionMenuBtn: { - // width: 45, - // height: 45, - // borderRadius: 100, - // backgroundColor: CSS.Colors.ORANGE, - // alignItems: 'center', - // justifyContent: 'center', - // elevation: 3, - // }, + accordionMenuBtnDark: { width: 45, height: 45, diff --git a/app/screens/Advanced/index.tsx b/app/screens/Advanced/index.tsx index 7d760215..1e9436c5 100644 --- a/app/screens/Advanced/index.tsx +++ b/app/screens/Advanced/index.tsx @@ -19,6 +19,7 @@ import Big from 'big.js' import { connect } from 'react-redux' import Logger from 'react-native-file-log' import Modal from 'react-native-modalbox' +import * as Common from 'shock-common' import wavesBGDark from '../../assets/images/waves-bg-dark.png' import * as CSS from '../../res/css' @@ -27,11 +28,7 @@ import Nav from '../../components/Nav' import { fetchChannels, fetchPendingChannels, - fetchInvoices, - fetchPayments, fetchPeers, - fetchTransactions, - fetchRecentTransactions, fetchHistory, } from '../../store/actions/HistoryActions' import { fetchNodeInfo } from '../../store/actions/NodeActions' @@ -231,7 +228,6 @@ class AdvancedScreen extends React.PureComponent { if (USDRate !== null) { const parsedConfirmedBalance = new Big(confirmedBalance) const parsedChannelBalance = new Big(channelBalance) - //@ts-expect-error const parsedUSDRate = new Big(USDRate) const satoshiUnit = new Big(0.00000001) const confirmedBalanceUSD = parsedConfirmedBalance @@ -701,12 +697,11 @@ class AdvancedScreen extends React.PureComponent { this.setState({ modalLoading: false }) } - transactionKeyExtractor = ( - transaction: import('../../services/wallet').Transaction, - ) => transaction.tx_hash + transactionKeyExtractor = (transaction: Common.Schema.ChainTransaction) => + transaction.tx_hash render() { - const { node, wallet, history } = this.props + const { node, wallet, history, chainTXs } = this.props const { accordions, peerURI, @@ -887,7 +882,7 @@ class AdvancedScreen extends React.PureComponent { /> { } } -const mapStateToProps = ({ history, node, wallet, fees }: Store.State) => ({ - history, - node, - wallet, - fees, -}) +const mapStateToProps = (state: Store.State) => { + const { history, node, wallet, fees } = state + + return { + history, + node, + wallet, + fees, + chainTXs: Store.getLatestChainTransactions(state), + } +} const mapDispatchToProps = { fetchChannels, fetchPendingChannels, - fetchInvoices, - fetchPayments, fetchPeers, - fetchTransactions, - fetchRecentTransactions, fetchHistory, fetchNodeInfo, } diff --git a/app/screens/WalletOverview/index.js b/app/screens/WalletOverview/index.js index ed8f762a..89be54c0 100644 --- a/app/screens/WalletOverview/index.js +++ b/app/screens/WalletOverview/index.js @@ -8,7 +8,6 @@ import { TouchableHighlight, View, ImageBackground, - InteractionManager, StatusBar, } from 'react-native' import Logger from 'react-native-file-log' @@ -25,13 +24,7 @@ import wavesBGDark from '../../assets/images/waves-bg-dark.png' import ShockIcon from '../../res/icons' import btcConvert from '../../services/convertBitcoin' import * as CSS from '../../res/css' -import { getUSDRate, getWalletBalance } from '../../store/actions/WalletActions' import { fetchNodeInfo } from '../../store/actions/NodeActions' -import { - fetchRecentTransactions, - fetchRecentPayments, - fetchRecentInvoices, -} from '../../store/actions/HistoryActions' import { subscribeOnChats } from '../../store/actions/ChatActions' import { invoicesRefreshForced, @@ -61,13 +54,8 @@ import { Color } from 'shock-common/dist/constants' * @prop {string|null} totalBalance * @prop {number} USDRate * @prop {boolean} testnet - * @prop {() => Promise} fetchRecentTransactions - * @prop {() => Promise} fetchRecentPayments - * @prop {() => Promise} fetchRecentInvoices - * @prop {() => Promise} getWalletBalance * @prop {() => Promise} fetchNodeInfo * @prop {() => Promise} subscribeOnChats - * @prop {() => Promise} getUSDRate * @prop {{notifyDisconnect:boolean, notifyDisconnectAfterSeconds:number}} settings * @prop {() => void} forceInvoicesRefresh * @prop {boolean} isOnline @@ -106,12 +94,6 @@ class WalletOverview extends React.PureComponent { )), } - /** @type {null|ReturnType} */ - balanceIntervalID = null - - /** @type {null|ReturnType} */ - exchangeRateIntervalID = null - didFocus = { remove() {} } subs = [() => {}] @@ -122,104 +104,19 @@ class WalletOverview extends React.PureComponent { const { fetchNodeInfo, subscribeOnChats, - fetchRecentTransactions, - fetchRecentInvoices, - navigation, forceInvoicesRefresh, forcePaymentsRefresh, - getMoreFeed, forceChainTXsRefresh, } = this.props forcePaymentsRefresh() forceInvoicesRefresh() - getMoreFeed() forceChainTXsRefresh() - this.didFocus = navigation.addListener('didFocus', () => { - this.balanceIntervalID = setTimeout(this.getWalletBalance, 4000) - this.exchangeRateIntervalID = setTimeout(this.getUSDRate, 4000) - this.recentPaymentsIntervalID = setTimeout(this.fetchRecentPayments, 4000) - }) - - navigation.addListener('didBlur', () => { - if (this.balanceIntervalID) { - clearTimeout(this.balanceIntervalID) - } - - if (this.exchangeRateIntervalID) { - clearTimeout(this.exchangeRateIntervalID) - } - - if (this.recentPaymentsIntervalID) { - clearTimeout(this.recentPaymentsIntervalID) - } - }) - this.startNotificationService() subscribeOnChats() - await Promise.all([ - fetchRecentInvoices(), - fetchRecentTransactions(), - fetchRecentPayments(), - fetchNodeInfo(), - ]) - } - - fetchRecentPayments = () => - InteractionManager.runAfterInteractions(() => { - const { fetchRecentPayments } = this.props - try { - fetchRecentPayments() - this.recentPaymentsIntervalID = setTimeout( - this.fetchRecentPayments, - 4000, - ) - return - } catch (err) { - this.recentPaymentsIntervalID = setTimeout( - this.fetchRecentPayments, - 4000, - ) - } - }) - - getUSDRate = () => - InteractionManager.runAfterInteractions(() => { - const { getUSDRate } = this.props - try { - getUSDRate() - this.exchangeRateIntervalID = setTimeout(this.getUSDRate, 4000) - return - } catch (err) { - this.exchangeRateIntervalID = setTimeout(this.getUSDRate, 4000) - } - }) - - getWalletBalance = () => - InteractionManager.runAfterInteractions(() => { - const { getWalletBalance } = this.props - try { - getWalletBalance() - this.balanceIntervalID = setTimeout(this.getWalletBalance, 4000) - return - } catch (err) { - this.balanceIntervalID = setTimeout(this.getWalletBalance, 4000) - } - }) - - componentWillUnmount() { - if (this.balanceIntervalID) { - clearInterval(this.balanceIntervalID) - } - - if (this.exchangeRateIntervalID) { - clearInterval(this.exchangeRateIntervalID) - } - //if (!SocketManager.socket?.connected) { - // SocketManager.socket.disconnect() - //} + await fetchNodeInfo() } onPressRequest = () => { @@ -438,12 +335,7 @@ const mapStateToProps = state => { } const mapDispatchToProps = { - getUSDRate, - getWalletBalance, - fetchRecentTransactions, fetchNodeInfo, - fetchRecentInvoices, - fetchRecentPayments, subscribeOnChats, forceInvoicesRefresh: invoicesRefreshForced, forcePaymentsRefresh: paymentsRefreshForced, diff --git a/app/services/http.ts b/app/services/http.ts index 50aaa852..c7415bb9 100644 --- a/app/services/http.ts +++ b/app/services/http.ts @@ -1,4 +1,5 @@ import Http, { AxiosResponse } from 'axios' +import * as Common from 'shock-common' interface ErrorResponse { message?: string @@ -27,6 +28,43 @@ const isErrResponse = (u: unknown): u is ErrorResponse => { return false } +const processErr = (err: unknown) => { + if (typeof err === 'string') { + throw new Error(err) + } + + if (!Common.Schema.isObj(err)) { + throw new Error(`Malformed: ${JSON.stringify(err)}`) + } + + if (typeof err.message === 'string') { + throw err + } + + if (Common.Schema.isObj(err.response) && err.response.status === 401) { + throw new Error(Common.Constants.ErrorCode.NOT_AUTH) + } + + if (Common.Schema.isObj(err.response) && err.response.data) { + if (Common.Schema.isObj(err.response.data)) { + const { message, errorMessage } = err.response.data + if (typeof errorMessage === 'string') { + throw new Error(errorMessage) + } + + if (typeof message === 'string') { + throw new Error(message) + } + } + + if (typeof err.response.data === 'string') { + throw new Error(err.response.data) + } + } + + throw new Error(`Unknown Error (Malformed): ${JSON.stringify(err)}`) +} + /** * @param url * @param data @@ -52,6 +90,10 @@ export const post = async ( const { data: dataReceived, status } = axiosRes + if (status === 401) { + throw new Error(Common.Constants.ErrorCode.NOT_AUTH) + } + if (isErrResponse(dataReceived)) { throw new Error( dataReceived.errorMessage || @@ -73,29 +115,8 @@ export const post = async ( } return dataReceived - } catch (err) { - if (typeof err.message === 'string') { - throw err - } - - if (err.response && err.response.data) { - if (typeof err.response.data === 'object') { - const { message, errorMessage } = err.response.data - if (errorMessage) { - throw new Error(errorMessage) - } - - if (message) { - throw new Error(message) - } - } - - if (typeof err.response.data === 'string') { - throw new Error(err.response.data) - } - } - - throw new Error(`Unknown Error (Malformed): ${JSON.stringify(err)}`) + } catch (e) { + return processErr(e) } } @@ -118,6 +139,10 @@ export const get = async ( const { data: dataReceived, status } = axiosRes + if (status === 401) { + throw new Error(Common.Constants.ErrorCode.NOT_AUTH) + } + if (isErrResponse(dataReceived)) { throw new Error( dataReceived.errorMessage || @@ -139,28 +164,7 @@ export const get = async ( } return dataReceived - } catch (err) { - if (typeof err.message === 'string') { - throw err - } - - if (err.response && err.response.data) { - if (typeof err.response.data === 'object') { - const { message, errorMessage } = err.response.data - if (errorMessage) { - throw new Error(errorMessage) - } - - if (message) { - throw new Error(message) - } - } - - if (typeof err.response.data === 'string') { - throw new Error(err.response.data) - } - } - - throw new Error(`Unknown Error (Malformed): ${JSON.stringify(err)}`) + } catch (e) { + return processErr(e) } } diff --git a/app/store/actions/HistoryActions.js b/app/store/actions/HistoryActions.js index 730b7115..38cae2f6 100644 --- a/app/store/actions/HistoryActions.js +++ b/app/store/actions/HistoryActions.js @@ -1,24 +1,12 @@ +// @ts-check import Logger from 'react-native-file-log' import * as Wallet from '../../services/wallet' -import * as Cache from '../../services/cache' export const ACTIONS = { LOAD_CHANNELS: 'channels/load', LOAD_PENDING_CHANNELS: 'channels/pending/load', - LOAD_INVOICES: 'invoices/load', - LOAD_MORE_INVOICES: 'invoices/loadMore', LOAD_PEERS: 'peers/load', - LOAD_PAYMENTS: 'payments/load', - LOAD_MORE_PAYMENTS: 'payments/loadMore', - LOAD_TRANSACTIONS: 'transactions/load', - LOAD_MORE_TRANSACTIONS: 'transactions/loadMore', - LOAD_NEW_RECENT_TRANSACTION: 'transactions/new', - LOAD_RECENT_TRANSACTIONS: 'recentTransactions/load', - LOAD_RECENT_PAYMENTS: 'recentPayments/load', - LOAD_RECENT_INVOICES: 'recentInvoices/load', - LOAD_NEW_RECENT_INVOICE: 'recentInvoices/new', - UNIFY_TRANSACTIONS: 'unifiedTransactions/unify', } /** * Fetches the Node's info @@ -57,42 +45,6 @@ export const fetchPendingChannels = () => async dispatch => { } } -/** - * Fetches the Node's info - * @returns {import('redux-thunk').ThunkAction, {}, {}, import('redux').AnyAction>} - */ -export const fetchInvoices = ({ - page = 1, - itemsPerPage = 10, - reset = false, -}) => async dispatch => { - try { - const data = await Wallet.listInvoices({ page, itemsPerPage }) - - if (reset) { - dispatch({ - type: ACTIONS.LOAD_INVOICES, - data, - }) - return data - } - - dispatch({ - type: ACTIONS.LOAD_MORE_INVOICES, - data, - }) - - return data - } catch (e) { - Logger.log(`Error inside fetchInvoices thunk: ${e.message}`) - return { - content: [], - page: 1, - totalPages: 1, - } - } -} - /** * Fetches the Node's info * @returns {import('redux-thunk').ThunkAction, {}, {}, import('redux').AnyAction>} @@ -113,222 +65,24 @@ export const fetchPeers = () => async dispatch => { } } -/** - * Fetches the Node's info - * @returns {import('redux-thunk').ThunkAction, {}, {}, import('redux').AnyAction>} - */ -export const fetchPayments = ({ - page = 1, - itemsPerPage = 10, - reset = false, -}) => async dispatch => { - try { - const data = await Wallet.listPayments({ - page, - itemsPerPage, - paginate: true, - include_incomplete: false, - }) - - if (reset) { - dispatch({ - type: ACTIONS.LOAD_PAYMENTS, - data, - }) - return data - } - - dispatch({ - type: ACTIONS.LOAD_MORE_PAYMENTS, - data, - }) - - return data - } catch (e) { - return { - content: [], - page: 1, - totalItems: 0, - totalPages: 1, - } - } -} - -/** - * Fetches the Node's info - * @returns {import('redux-thunk').ThunkAction, {}, {}, import('redux').AnyAction>} - */ -export const fetchTransactions = ({ - page = 1, - itemsPerPage = 500, - reset = false, -}) => async dispatch => { - try { - const data = await Wallet.getTransactions({ - page, - itemsPerPage, - paginate: true, - }) - const revData = { - ...data, - content: data.content.reverse(), - } - if (reset) { - dispatch({ - type: ACTIONS.LOAD_TRANSACTIONS, - revData, - }) - return revData - } - - dispatch({ - type: ACTIONS.LOAD_MORE_TRANSACTIONS, - revData, - }) - - return revData - } catch (e) { - return { - content: [], - page: 1, - totalItems: 0, - totalPages: 1, - } - } -} - -/** - * Unifies and sorts all of the currently loaded transactions, payments and invoices - * @returns {import('redux-thunk').ThunkAction} - */ -export const unifyTransactions = () => dispatch => { - dispatch({ - type: ACTIONS.UNIFY_TRANSACTIONS, - }) -} - /** * Fetches the Node's info * @returns {import('redux-thunk').ThunkAction, {}, {}, import('redux').AnyAction>} */ export const fetchHistory = () => async dispatch => { try { const history = await Promise.all([ - dispatch(fetchInvoices({ reset: true })), dispatch(fetchPeers()), dispatch(fetchChannels()), - dispatch(fetchPayments({ reset: true })), - dispatch(fetchTransactions({ reset: true })), ]) - dispatch(unifyTransactions()) - return history } catch (e) { Logger.log(`Error inside fetchHistory thunk: ${e.message}`) - return [ - { content: [], totalPages: 1, page: 1 }, - [], - [], - { content: [], page: 1, totalItems: 1, totalPages: 1 }, - { content: [], page: 1, totalItems: 1, totalPages: 1 }, - ] - } -} - -/** - * Fetches the recent transactions - * @returns {import('redux-thunk').ThunkAction, {}, {}, import('redux').AnyAction>} - */ -export const fetchRecentInvoices = () => async dispatch => { - try { - const invoiceResponse = await Wallet.listInvoices({ - itemsPerPage: 100, - page: 1, - }) - - dispatch({ - type: ACTIONS.LOAD_RECENT_INVOICES, - data: invoiceResponse.content, - }) - - dispatch(unifyTransactions()) - } catch (e) { - Logger.log(`Error inside fetchRecentInvoices -> ${e.message}`) - } -} - -/** - * Fetches the latest payments - * @returns {import('redux-thunk').ThunkAction, {}, {}, import('redux').AnyAction>} - */ -export const fetchRecentPayments = () => async dispatch => { - try { - try { - await Cache.getToken() - // eslint-disable-next-line no-empty - } catch (_) { - return - } - - const payments = await Wallet.listPayments({ - include_incomplete: false, - itemsPerPage: 100, - page: 1, - paginate: true, - }) - - const decodedRequests = await Promise.all( - payments.content.map(payment => - Wallet.decodeInvoice({ payReq: payment.payment_request }).catch(e => { - Logger.log(`HistoryActions.fetchRecentPayments() -> ${e.message}`) - }), - ), - ) - - const recentPayments = payments.content.map((payment, key) => ({ - ...payment, - // @ts-expect-error - decodedPayment: decodedRequests[key]?.decodedRequest ?? null, - })) - - dispatch({ - type: ACTIONS.LOAD_RECENT_PAYMENTS, - data: recentPayments, - }) - - dispatch(unifyTransactions()) - } catch (err) { - Logger.log(`Error inside fetchRecentPayments() -> ${err.message}`) - } -} - -/** - * Fetches the recent transactions - * @returns {import('redux-thunk').ThunkAction, {}, {}, import('redux').AnyAction>} - */ -export const fetchRecentTransactions = () => async dispatch => { - try { - const invoiceResponse = await Wallet.getTransactions({ - itemsPerPage: 500, - page: 1, - paginate: true, - }) - - dispatch({ - type: ACTIONS.LOAD_RECENT_TRANSACTIONS, - data: invoiceResponse, - }) - - dispatch(unifyTransactions()) - } catch (e) { - Logger.log(`Error inside fetchRecentTransactions thunk: ${e.message}`) + return [[], []] } } diff --git a/app/store/actions/WalletActions.js b/app/store/actions/WalletActions.js deleted file mode 100644 index 65cbc78d..00000000 --- a/app/store/actions/WalletActions.js +++ /dev/null @@ -1,67 +0,0 @@ -import { ToastAndroid } from 'react-native' -import Logger from 'react-native-file-log' - -import * as Wallet from '../../services/wallet' - -export const ACTIONS = { - LOAD_WALLET_BALANCE: 'balance/load', - SET_USD_RATE: 'usdRate/load', -} - -/** - * @typedef {object} WalletBalance - * @prop {string} confirmedBalance - * @prop {string} pendingChannelBalance - * @prop {string} channelBalance - */ - -/** - * Fetches the Node's info - * @returns {import('redux-thunk').ThunkAction, {}, {}, import('redux').AnyAction>} - */ -export const getWalletBalance = () => async dispatch => { - try { - const balance = await Wallet.balance() - - const data = { - confirmedBalance: balance.confirmed_balance, - pendingChannelBalance: balance.pending_channel_balance, - channelBalance: balance.channel_balance, - } - - dispatch({ - type: ACTIONS.LOAD_WALLET_BALANCE, - data, - }) - - return data - } catch (e) { - Logger.log(`Error inside getWalletBalance thunks: ${e.message}`) - return { - channelBalance: '0', - confirmedBalance: '0', - pendingChannelBalance: '0', - } - } -} - -/** - * Fetches the Node's info - * @returns {import('redux-thunk').ThunkAction, {}, {}, import('redux').AnyAction>} - */ -export const getUSDRate = () => async dispatch => { - try { - const USDRate = await Wallet.USDExchangeRate() - - dispatch({ - type: ACTIONS.SET_USD_RATE, - data: USDRate, - }) - - return USDRate - } catch (e) { - ToastAndroid.show(`Could not get USD rate`, ToastAndroid.LONG) - Logger.log(`Error inside getUSDRate tip: ${e.message}`) - return 0 - } -} diff --git a/app/store/actions/index.ts b/app/store/actions/index.ts index 772eb809..0b83cd82 100644 --- a/app/store/actions/index.ts +++ b/app/store/actions/index.ts @@ -18,6 +18,7 @@ import { PaymentsAction } from './payments' import { ChainTXsAction } from './chain-txs' import { PostsAction } from './posts' import { DebugAction } from './debug' +import { WalletAction } from './wallet' export type Action = | UsersAction @@ -37,6 +38,7 @@ export type Action = | RehydrateAction | PostsAction | DebugAction + | WalletAction export const receivedBackfeed = Common.Store.Actions.receivedBackfeed export const receivedFeed = Common.Store.Actions.receivedFeed @@ -64,5 +66,6 @@ export { fetchNodeInfo } from './NodeActions' export * from './users' export * from './posts' export * from './debug' +export * from './wallet' export { ChatActions, RequestActions, Follows } diff --git a/app/store/actions/wallet.ts b/app/store/actions/wallet.ts new file mode 100644 index 00000000..826eec8a --- /dev/null +++ b/app/store/actions/wallet.ts @@ -0,0 +1,23 @@ +export const loadedUSDRate = (USDRate: number) => + ({ + type: 'usdRate/load', + data: USDRate, + } as const) + +export const loadedBalance = ( + confirmedBalance: string, + pendingChannelBalance: string, + channelBalance: string, +) => + ({ + type: 'balance/load', + data: { + confirmedBalance, + pendingChannelBalance, + channelBalance, + }, + } as const) + +export type WalletAction = + | ReturnType + | ReturnType diff --git a/app/store/reducers/HistoryReducer.js b/app/store/reducers/HistoryReducer.js index 9e95b415..ca1cf10c 100644 --- a/app/store/reducers/HistoryReducer.js +++ b/app/store/reducers/HistoryReducer.js @@ -1,4 +1,4 @@ -import reverse from 'lodash/reverse' +// @ts-check import { ACTIONS } from '../actions/HistoryActions' import * as Wallet from '../../services/wallet' @@ -10,14 +10,7 @@ import * as Wallet from '../../services/wallet' * @typedef {object} State * @prop {Wallet.Channel[]} channels * @prop {Wallet.pendingChannels[]} pendingChannels - * @prop {object} invoices * @prop {Wallet.Peer[]} peers - * @prop {object} transactions - * @prop {object} payments - * @prop {Wallet.Transaction[]} recentTransactions - * @prop {Wallet.Payment[]} recentPayments - * @prop {Wallet.Invoice[]} recentInvoices - * @prop {UnifiedTransaction[]} unifiedTransactions */ // TO DO: typings for data /** @@ -34,29 +27,7 @@ import * as Wallet from '../../services/wallet' const INITIAL_STATE = { channels: [], pendingChannels: [], - invoices: { - content: [], - page: 0, - totalPages: 0, - totalItems: 0, - }, peers: [], - transactions: { - content: [], - page: 0, - totalPages: 0, - totalItems: 0, - }, - payments: { - content: [], - page: 0, - totalPages: 0, - totalItems: 0, - }, - recentTransactions: [], - recentPayments: [], - recentInvoices: [], - unifiedTransactions: [], } /** * @param {State} state @@ -87,28 +58,7 @@ const history = (state = INITIAL_STATE, action) => { pendingChannels: data, } } - case ACTIONS.LOAD_INVOICES: { - const { data } = action - if (typeof data !== 'object') { - return state - } - return { - ...state, - invoices: data, - } - } - case ACTIONS.LOAD_MORE_INVOICES: { - const { data } = action - const { invoices } = state - return { - ...state, - invoices: { - ...data, - //@ts-expect-error - content: [...invoices.content, ...data.content], - }, - } - } + case ACTIONS.LOAD_PEERS: { const { data } = action if (!Array.isArray(data)) { @@ -120,194 +70,7 @@ const history = (state = INITIAL_STATE, action) => { peers: data, } } - case ACTIONS.LOAD_PAYMENTS: { - const { data } = action - if (typeof data !== 'object') { - return state - } - return { - ...state, - payments: data, - } - } - case ACTIONS.LOAD_MORE_PAYMENTS: { - const { data } = action - const { payments } = state - return { - ...state, - payments: { - ...data, - //@ts-expect-error - content: [...payments.content, ...data.content], - }, - } - } - case ACTIONS.LOAD_TRANSACTIONS: { - const { data } = action - if (typeof data !== 'object') { - return state - } - return { - ...state, - transactions: data, - } - } - case ACTIONS.LOAD_MORE_TRANSACTIONS: { - const { data } = action - const { transactions } = state - return { - ...state, - transactions: { - ...data, - //@ts-expect-error - content: [...transactions.content, ...data.content], - }, - } - } - case ACTIONS.LOAD_RECENT_TRANSACTIONS: { - /** - * @param {Wallet.Invoice | Wallet.Payment | Wallet.Transaction} unifiedTransaction - */ - const { data } = action - return { - ...state, - //@ts-expect-error - recentTransactions: data.content, - } - } - case ACTIONS.LOAD_RECENT_PAYMENTS: { - /** - * @param {Wallet.Invoice | Wallet.Payment | Wallet.Transaction} unifiedTransaction - */ - const { data } = action - - return { - ...state, - //@ts-expect-error - recentPayments: data, - } - } - case ACTIONS.LOAD_NEW_RECENT_INVOICE: { - const { data } = action - if (!Array.isArray(data)) { - return state - } - return { - ...state, - //@ts-expect-error - recentInvoices: [data, ...state.recentInvoices], - } - } - case ACTIONS.UNIFY_TRANSACTIONS: { - const filteredTransactions = [ - ...state.recentTransactions, - ...state.recentPayments, - ...state.recentInvoices, - ].filter( - /** - * @param {Wallet.Invoice | Wallet.Payment | Wallet.Transaction} unifiedTransaction - */ - unifiedTransaction => { - if (Wallet.isInvoice(unifiedTransaction)) { - return unifiedTransaction.settled - } - - if (Wallet.isPayment(unifiedTransaction)) { - return unifiedTransaction.status === 'SUCCEEDED' - } - - if (Wallet.isTransaction(unifiedTransaction)) { - return true - } - - return false - }, - ) - - const sortedTransactions = filteredTransactions.sort( - /** - * @param {Wallet.Invoice | Wallet.Payment | Wallet.Transaction} a - * @param {Wallet.Invoice | Wallet.Payment | Wallet.Transaction} b - */ - (a, b) => { - const _a = (() => { - if (Wallet.isInvoice(a)) { - return Number(a.settle_date) - } - - if (Wallet.isPayment(a)) { - return Number(a.creation_date) - } - - if (Wallet.isTransaction(a)) { - return Number(a.time_stamp) - } - - return 0 - })() - - const _b = (() => { - if (Wallet.isInvoice(b)) { - return Number(b.settle_date) - } - - if (Wallet.isPayment(b)) { - return Number(b.creation_date) - } - - if (Wallet.isTransaction(b)) { - return Number(b.time_stamp) - } - - return 0 - })() - - return _b - _a - }, - ) - - return { - ...state, - unifiedTransactions: sortedTransactions, - } - } - case ACTIONS.LOAD_NEW_RECENT_TRANSACTION: { - /** - * @type {{data:import('../../services/wallet').Transaction}} - */ - //@ts-expect-error - const { data } = action - const { recentTransactions } = state - const { tx_hash: txHash } = data - /** - * - * @param {import('../../services/wallet').Transaction} tx - */ - const sameTx = tx => tx.tx_hash === txHash - const txIndex = recentTransactions.findIndex(sameTx) - const newContent = [...recentTransactions] - if (txIndex !== -1) { - newContent[txIndex] = data - } else { - newContent.unshift(data) - } - return { - ...state, - recentTransactions: newContent, - } - } - case ACTIONS.LOAD_RECENT_INVOICES: { - const { data } = action - if (!Array.isArray(data)) { - return state - } - return { - ...state, - //@ts-expect-error - recentInvoices: reverse(data), - } - } default: return state } diff --git a/app/store/reducers/WalletReducer.js b/app/store/reducers/WalletReducer.js index c01c58f2..5e9a57a9 100644 --- a/app/store/reducers/WalletReducer.js +++ b/app/store/reducers/WalletReducer.js @@ -1,4 +1,7 @@ -import { ACTIONS } from '../actions/WalletActions' +/** + * @typedef {import('../actions').Action} Action + */ + import Big from 'big.js' /** * @typedef {object} State @@ -9,12 +12,6 @@ import Big from 'big.js' * @prop {string} pendingChannelBalance */ -/** - * @typedef {object} Action - * @prop {string} type - * @prop {State} data - */ - /** @type {State} */ const INITIAL_STATE = { totalBalance: '0', @@ -31,7 +28,7 @@ const INITIAL_STATE = { */ const wallet = (state = INITIAL_STATE, action) => { switch (action.type) { - case ACTIONS.LOAD_WALLET_BALANCE: { + case 'balance/load': { const { channelBalance, confirmedBalance, @@ -50,7 +47,7 @@ const wallet = (state = INITIAL_STATE, action) => { pendingChannelBalance, } } - case ACTIONS.SET_USD_RATE: { + case 'usdRate/load': { const { data } = action return { diff --git a/app/store/sagas/common.ts b/app/store/sagas/common.ts new file mode 100644 index 00000000..440da177 --- /dev/null +++ b/app/store/sagas/common.ts @@ -0,0 +1,4 @@ +type R = T extends Promise ? U : T +export type YieldReturn = R< + ReturnType any ? T : any> +> diff --git a/app/store/sagas/index.ts b/app/store/sagas/index.ts index 6537baef..b9b62088 100644 --- a/app/store/sagas/index.ts +++ b/app/store/sagas/index.ts @@ -8,6 +8,7 @@ import posts from './posts' import follows from './follows' import ping from './ping' import debug from './debug' +import wallet from './wallet' function* rootSaga() { yield all([ @@ -19,6 +20,7 @@ function* rootSaga() { call(follows), call(ping), call(debug), + call(wallet), ]) } diff --git a/app/store/sagas/wallet.ts b/app/store/sagas/wallet.ts new file mode 100644 index 00000000..17229805 --- /dev/null +++ b/app/store/sagas/wallet.ts @@ -0,0 +1,106 @@ +import { select, all, takeEvery } from 'redux-saga/effects' +import Logger from 'react-native-file-log' +import * as Common from 'shock-common' + +import * as Actions from '../actions' +import * as Services from '../../services' +import * as Selectors from '../selectors' +import { getStore } from '../store' + +let USDRateTimeoutID: ReturnType | null = null +const USD_RATE_INTERVAL_TIME = 15000 + +let balanceTimeoutID: ReturnType | null = null +const BALANCE_INTERVAL_TIME = 4000 + +const USDRateFetcher = () => { + Services.USDExchangeRate() + .then(rate => { + // Canary + Logger.log(`Received exchange rate: ${rate}`) + getStore().dispatch(Actions.loadedUSDRate(rate)) + }) + .catch(e => { + if (e.message === Common.Constants.ErrorCode.NOT_AUTH) { + getStore().dispatch(Actions.tokenDidInvalidate()) + } else { + Logger.log(`Error inside USDRateFetcher -> ${e.message}`) + } + }) + .finally(() => { + // check that poll wasn't killed + if (USDRateTimeoutID) { + USDRateTimeoutID = setTimeout(USDRateFetcher, USD_RATE_INTERVAL_TIME) + } + }) +} + +function* USDRateWatcher() { + try { + const state = Selectors.getStateRoot(yield select()) + const isReady = Selectors.isOnline(state) && Selectors.isAuth(state) + + if (isReady && !USDRateTimeoutID) { + USDRateTimeoutID = setTimeout(USDRateFetcher, USD_RATE_INTERVAL_TIME) + } + + if (!isReady && USDRateTimeoutID) { + clearTimeout(USDRateTimeoutID) + USDRateTimeoutID = null + } + } catch (e) { + Logger.log(`Error inside USDRateWatcher* ()`) + Logger.log(e.message) + } +} + +const balanceFetcher = () => { + Services.balance() + .then(({ channel_balance, confirmed_balance, pending_channel_balance }) => { + getStore().dispatch( + Actions.loadedBalance( + confirmed_balance, + pending_channel_balance, + channel_balance, + ), + ) + }) + .catch(e => { + if (e.message === Common.Constants.ErrorCode.NOT_AUTH) { + getStore().dispatch(Actions.tokenDidInvalidate()) + } else { + Logger.log(`Error inside balanceFetcher -> ${e.message}`) + } + }) + .finally(() => { + // check that poll wasn't killed + if (balanceTimeoutID) { + balanceTimeoutID = setTimeout(balanceFetcher, BALANCE_INTERVAL_TIME) + } + }) +} + +function* balanceWatcher() { + try { + const state = Selectors.getStateRoot(yield select()) + const isReady = Selectors.isOnline(state) && Selectors.isAuth(state) + + if (isReady && !balanceTimeoutID) { + balanceTimeoutID = setTimeout(balanceFetcher, BALANCE_INTERVAL_TIME) + } + + if (!isReady && balanceTimeoutID) { + clearTimeout(balanceTimeoutID) + balanceTimeoutID = null + } + } catch (e) { + Logger.log(`Error inside balanceWatcher* ()`) + Logger.log(e.message) + } +} + +function* rootSaga() { + yield all([takeEvery('*', USDRateWatcher), takeEvery('*', balanceWatcher)]) +} + +export default rootSaga