Toast flows (#856)

* Use toast flows

"Toast flows" are a group of toasts that should be shown after each other.

Before this commit, they were implemented by manually removing previous toasts in the same flow.

Now a flowId can be passed and ToastProvider will make sure that there always only exists one toast with the same flowId.

This is different to toast tags since tags don't replace toasts with the same tag, they only are shown "above" them.

* Create wrapper for toast flows
This commit is contained in:
ekzyis 2024-02-20 02:03:30 +01:00 committed by GitHub
parent fe0d960208
commit 5de014cba8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 72 additions and 34 deletions

View File

@ -13,13 +13,26 @@ export const ToastProvider = ({ children }) => {
const [toasts, setToasts] = useState([]) const [toasts, setToasts] = useState([])
const toastId = useRef(0) const toastId = useRef(0)
const dispatchToast = useCallback((toastConfig) => { const dispatchToast = useCallback((toast) => {
toastConfig = { toast = {
...toastConfig, ...toast,
id: toastId.current++ id: toastId.current++
} }
setToasts(toasts => [...toasts, toastConfig]) const { flowId } = toast
return () => removeToast(toastConfig) setToasts(toasts => {
if (flowId) {
// replace previous toast with same flow id
const idx = toasts.findIndex(toast => toast.flowId === flowId)
if (idx === -1) return [...toasts, toast]
return [
...toasts.slice(0, idx),
toast,
...toasts.slice(idx + 1)
]
}
return [...toasts, toast]
})
return () => removeToast(toast)
}, []) }, [])
const removeToast = useCallback(({ id, onCancel, tag }) => { const removeToast = useCallback(({ id, onCancel, tag }) => {
@ -153,3 +166,46 @@ export const ToastProvider = ({ children }) => {
} }
export const useToast = () => useContext(ToastContext) export const useToast = () => useContext(ToastContext)
export const withToastFlow = (toaster) => flowFn => {
const wrapper = async (...args) => {
const {
flowId,
type: t,
onPending,
onSuccess,
onCancel,
onError
} = flowFn(...args)
let canceled
toaster.warning(`${t} pending`, {
autohide: false,
onCancel: async () => {
try {
await onCancel?.()
canceled = true
toaster.warning(`${t} canceled`, { flowId })
} catch (err) {
toaster.danger(`failed to cancel ${t}`, { flowId })
}
},
flowId
})
try {
const ret = await onPending()
if (!canceled) {
toaster.success(`${t} successful`, { flowId })
await onSuccess?.()
}
return ret
} catch (err) {
// ignore errors if canceled since they might be caused by cancellation
if (canceled) return
const reason = err?.message?.toString().toLowerCase() || 'unknown reason'
toaster.danger(`${t} failed: ${reason}`, { flowId })
await onError?.()
throw err
}
}
return wrapper
}

View File

@ -1,7 +1,7 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react' import { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { LNbitsProvider, useLNbits } from './lnbits' import { LNbitsProvider, useLNbits } from './lnbits'
import { NWCProvider, useNWC } from './nwc' import { NWCProvider, useNWC } from './nwc'
import { useToast } from '../toast' import { useToast, withToastFlow } from '../toast'
import { gql, useMutation } from '@apollo/client' import { gql, useMutation } from '@apollo/client'
const WebLNContext = createContext({}) const WebLNContext = createContext({})
@ -81,35 +81,17 @@ function RawWebLNProvider ({ children }) {
} }
`) `)
const sendPaymentWithToast = function ({ bolt11, hash, hmac }) { const sendPaymentWithToast = withToastFlow(toaster)(
let canceled = false ({ bolt11, hash, hmac }) => {
let removeToast = toaster.warning('payment pending', { return {
autohide: false, flowId: hash,
onCancel: async () => { type: 'payment',
try { onPending: () => provider.sendPayment(bolt11),
// hash and hmac are only passed for JIT invoices // hash and hmac are only passed for JIT invoices
if (hash && hmac) await cancelInvoice({ variables: { hash, hmac } }) onCancel: () => hash && hmac ? cancelInvoice({ variables: { hash, hmac } }) : undefined
canceled = true
toaster.warning('payment canceled')
removeToast = undefined
} catch (err) {
toaster.danger('failed to cancel payment')
} }
} }
}) )
return provider.sendPayment(bolt11)
.then(({ preimage }) => {
removeToast?.()
toaster.success('payment successful')
return { preimage }
}).catch((err) => {
if (canceled) return
removeToast?.()
const reason = err?.message?.toString().toLowerCase() || 'unknown reason'
toaster.danger(`payment failed: ${reason}`)
throw err
})
}
const setProvider = useCallback((defaultProvider) => { const setProvider = useCallback((defaultProvider) => {
// move provider to the start to set it as default // move provider to the start to set it as default