diff --git a/components/form.js b/components/form.js
index d271da979..2eda65d50 100644
--- a/components/form.js
+++ b/components/form.js
@@ -33,6 +33,7 @@ import EyeClose from '@/svgs/eye-close-line.svg'
import Info from './info'
import { InvoiceCanceledError, usePayment } from './payment'
import { useMe } from './me'
+import { ActCanceledError } from './item-act'
export class SessionRequiredError extends Error {
constructor () {
@@ -841,7 +842,7 @@ export function Form ({
}
let hash, hmac
if (invoiceable) {
- revert = optimisticUpdate?.({ amount, ...values }, ...args);
+ revert = await optimisticUpdate?.({ amount, ...values }, ...args);
[{ hash, hmac }, cancel] = await payment.request(amount)
}
await onSubmit({ hash, hmac, amount, ...values }, ...args)
@@ -850,7 +851,7 @@ export function Form ({
}
} catch (err) {
revert?.()
- if (err instanceof InvoiceCanceledError) {
+ if (err instanceof InvoiceCanceledError || err instanceof ActCanceledError) {
return
}
const msg = err.message || err.toString?.()
diff --git a/components/item-act.js b/components/item-act.js
index 42fc92b82..8505b1187 100644
--- a/components/item-act.js
+++ b/components/item-act.js
@@ -6,7 +6,7 @@ import { useMe } from './me'
import UpBolt from '@/svgs/bolt.svg'
import { amountSchema } from '@/lib/validate'
import { gql, useApolloClient, useMutation } from '@apollo/client'
-import { useToast } from './toast'
+import { TOAST_DEFAULT_DELAY_MS, useToast } from './toast'
import { useLightning } from './lightning'
import { nextTip } from './upvote'
import { InvoiceCanceledError, PaymentProvider, usePayment } from './payment'
@@ -47,6 +47,7 @@ export default function ItemAct ({ onClose, item, down, children }) {
const [oValue, setOValue] = useState()
const strike = useLightning()
const cache = useApolloClient().cache
+ const toaster = useToast()
useEffect(() => {
inputRef.current?.focus()
@@ -67,10 +68,19 @@ export default function ItemAct ({ onClose, item, down, children }) {
addCustomTip(Number(amount))
}, [me, act, down, item.id, strike, onClose])
- const optimisticUpdate = useCallback(({ amount }) => {
+ const optimisticUpdate = useCallback(async ({ amount }) => {
onClose()
strike()
- return actOptimisticUpdate(cache, { ...item, sats: Number(amount), act: down ? 'DONT_LIKE_THIS' : 'TIP' }, { me })
+ const revert = actOptimisticUpdate(cache, { ...item, sats: Number(amount), act: down ? 'DONT_LIKE_THIS' : 'TIP' }, { me })
+ if (zapUndoTrigger(me, amount)) {
+ try {
+ await zapUndo(toaster)
+ } catch (err) {
+ revert()
+ throw err
+ }
+ };
+ return revert
}, [cache, strike, onClose])
// we need to wrap with PaymentProvider here since modals don't have access to PaymentContext by default
@@ -276,12 +286,15 @@ export function useZap () {
try {
const variables = { path: item.path, id: item.id, sats, act: 'TIP' }
revert = zapOptimisticUpdate(cache, variables)
- strike();
+ strike()
+ if (zapUndoTrigger(me, sats)) {
+ await zapUndo(toaster)
+ };
[{ hash, hmac }, cancel] = await payment.request(sats - meSats)
await zap({ variables: { ...variables, hash, hmac } })
} catch (error) {
revert?.()
- if (error instanceof InvoiceCanceledError) {
+ if (error instanceof InvoiceCanceledError || error instanceof ActCanceledError) {
return
}
console.error(error)
@@ -290,3 +303,24 @@ export function useZap () {
}
}, [strike, payment])
}
+
+export class ActCanceledError extends Error {
+ constructor () {
+ super('act canceled')
+ this.name = 'ActCanceledError'
+ }
+}
+
+const zapUndoTrigger = (me, amount) => {
+ if (!me) return false
+ const enabled = me.privates.zapUndos !== null
+ return enabled ? amount >= me.privates.zapUndos : false
+}
+
+const zapUndo = async (toaster) => {
+ return await new Promise((resolve, reject) => {
+ const delay = TOAST_DEFAULT_DELAY_MS
+ toaster.undo({ onClick: () => reject(new ActCanceledError()) })
+ setTimeout(resolve, delay)
+ })
+}
diff --git a/components/toast.js b/components/toast.js
index e71548306..8ecfc9ed6 100644
--- a/components/toast.js
+++ b/components/toast.js
@@ -75,6 +75,16 @@ export const ToastProvider = ({ children }) => {
...options
}
return dispatchToast(toast)
+ },
+ undo: (options) => {
+ const toast = {
+ body: (onClose) =>