From 5de014cba8d92ef9bcbb98e826cc6ad5b7c13952 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Tue, 20 Feb 2024 02:03:30 +0100 Subject: [PATCH] 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 --- components/toast.js | 66 ++++++++++++++++++++++++++++++++++++--- components/webln/index.js | 40 +++++++----------------- 2 files changed, 72 insertions(+), 34 deletions(-) diff --git a/components/toast.js b/components/toast.js index e1f01488..4a0614a7 100644 --- a/components/toast.js +++ b/components/toast.js @@ -13,13 +13,26 @@ export const ToastProvider = ({ children }) => { const [toasts, setToasts] = useState([]) const toastId = useRef(0) - const dispatchToast = useCallback((toastConfig) => { - toastConfig = { - ...toastConfig, + const dispatchToast = useCallback((toast) => { + toast = { + ...toast, id: toastId.current++ } - setToasts(toasts => [...toasts, toastConfig]) - return () => removeToast(toastConfig) + const { flowId } = toast + 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 }) => { @@ -153,3 +166,46 @@ export const ToastProvider = ({ children }) => { } 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 +} diff --git a/components/webln/index.js b/components/webln/index.js index e0204ea3..256ffa7b 100644 --- a/components/webln/index.js +++ b/components/webln/index.js @@ -1,7 +1,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'react' import { LNbitsProvider, useLNbits } from './lnbits' import { NWCProvider, useNWC } from './nwc' -import { useToast } from '../toast' +import { useToast, withToastFlow } from '../toast' import { gql, useMutation } from '@apollo/client' const WebLNContext = createContext({}) @@ -81,35 +81,17 @@ function RawWebLNProvider ({ children }) { } `) - const sendPaymentWithToast = function ({ bolt11, hash, hmac }) { - let canceled = false - let removeToast = toaster.warning('payment pending', { - autohide: false, - onCancel: async () => { - try { - // hash and hmac are only passed for JIT invoices - if (hash && hmac) await cancelInvoice({ variables: { hash, hmac } }) - canceled = true - toaster.warning('payment canceled') - removeToast = undefined - } catch (err) { - toaster.danger('failed to cancel payment') - } + const sendPaymentWithToast = withToastFlow(toaster)( + ({ bolt11, hash, hmac }) => { + return { + flowId: hash, + type: 'payment', + onPending: () => provider.sendPayment(bolt11), + // hash and hmac are only passed for JIT invoices + onCancel: () => hash && hmac ? cancelInvoice({ variables: { hash, hmac } }) : undefined } - }) - 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) => { // move provider to the start to set it as default