Allow zap undo's for short period of time (#857)
* Cancel zaps * Hide zap error toast * Immediately throw error about insufficient funds * Optimistic UX * Also hide success zap toast * Show undo instead of cancel * Include sat amount in toast * Fix undo toasts removed on navigation * Add setting for zap undos * Add undo to custom zaps * Use WithUndos suffix * Fix toast flow transition * Fix setting not respected * Skip undo flow if funds insufficient * Remove brackets around undo * Fix insufficient funds detection * Fix downzap undo * Add progress bar to toasts * Use 'button' instead of 'notification' in zap undo info * Remove console.log * Fix toast progress bar restarts --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									46a0af19eb
								
							
						
					
					
						commit
						c57fcd6518
					
				@ -84,6 +84,7 @@ export default gql`
 | 
				
			|||||||
    nsfwMode: Boolean!
 | 
					    nsfwMode: Boolean!
 | 
				
			||||||
    tipDefault: Int!
 | 
					    tipDefault: Int!
 | 
				
			||||||
    turboTipping: Boolean!
 | 
					    turboTipping: Boolean!
 | 
				
			||||||
 | 
					    zapUndos: Boolean!
 | 
				
			||||||
    wildWestMode: Boolean!
 | 
					    wildWestMode: Boolean!
 | 
				
			||||||
    withdrawMaxFeeDefault: Int!
 | 
					    withdrawMaxFeeDefault: Int!
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -146,6 +147,7 @@ export default gql`
 | 
				
			|||||||
    nsfwMode: Boolean!
 | 
					    nsfwMode: Boolean!
 | 
				
			||||||
    tipDefault: Int!
 | 
					    tipDefault: Int!
 | 
				
			||||||
    turboTipping: Boolean!
 | 
					    turboTipping: Boolean!
 | 
				
			||||||
 | 
					    zapUndos: Boolean!
 | 
				
			||||||
    wildWestMode: Boolean!
 | 
					    wildWestMode: Boolean!
 | 
				
			||||||
    withdrawMaxFeeDefault: Int!
 | 
					    withdrawMaxFeeDefault: Int!
 | 
				
			||||||
    autoWithdrawThreshold: Int
 | 
					    autoWithdrawThreshold: Int
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@ import Flag from '../svgs/flag-fill.svg'
 | 
				
			|||||||
import { useMemo } from 'react'
 | 
					import { useMemo } from 'react'
 | 
				
			||||||
import getColor from '../lib/rainbow'
 | 
					import getColor from '../lib/rainbow'
 | 
				
			||||||
import { gql, useMutation } from '@apollo/client'
 | 
					import { gql, useMutation } from '@apollo/client'
 | 
				
			||||||
 | 
					import { useMe } from './me'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function DownZap ({ id, meDontLikeSats, ...props }) {
 | 
					export function DownZap ({ id, meDontLikeSats, ...props }) {
 | 
				
			||||||
  const style = useMemo(() => (meDontLikeSats
 | 
					  const style = useMemo(() => (meDontLikeSats
 | 
				
			||||||
@ -23,6 +24,7 @@ export function DownZap ({ id, meDontLikeSats, ...props }) {
 | 
				
			|||||||
function DownZapper ({ id, As, children }) {
 | 
					function DownZapper ({ id, As, children }) {
 | 
				
			||||||
  const toaster = useToast()
 | 
					  const toaster = useToast()
 | 
				
			||||||
  const showModal = useShowModal()
 | 
					  const showModal = useShowModal()
 | 
				
			||||||
 | 
					  const me = useMe()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <As
 | 
					    <As
 | 
				
			||||||
@ -32,7 +34,10 @@ function DownZapper ({ id, As, children }) {
 | 
				
			|||||||
            <ItemAct
 | 
					            <ItemAct
 | 
				
			||||||
              onClose={() => {
 | 
					              onClose={() => {
 | 
				
			||||||
                onClose()
 | 
					                onClose()
 | 
				
			||||||
                toaster.success('item downzapped')
 | 
					                // undo prompt was toasted before closing modal if zap undos are enabled
 | 
				
			||||||
 | 
					                // so an additional success toast would be confusing
 | 
				
			||||||
 | 
					                const zapUndosEnabled = me && me?.privates?.zapUndos
 | 
				
			||||||
 | 
					                if (!zapUndosEnabled) toaster.success('item downzapped')
 | 
				
			||||||
              }} itemId={id} down
 | 
					              }} itemId={id} down
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <AccordianItem
 | 
					              <AccordianItem
 | 
				
			||||||
 | 
				
			|||||||
@ -186,12 +186,15 @@ export const useInvoiceable = (onSubmit, options = defaultOptions) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const onSubmitWrapper = useCallback(async (
 | 
					  const onSubmitWrapper = useCallback(async (
 | 
				
			||||||
    { cost, ...formValues },
 | 
					    { cost, ...formValues },
 | 
				
			||||||
    { variables, optimisticResponse, update, ...submitArgs }) => {
 | 
					    { variables, optimisticResponse, update, flowId, ...submitArgs }) => {
 | 
				
			||||||
    // some actions require a session
 | 
					    // some actions require a session
 | 
				
			||||||
    if (!me && options.requireSession) {
 | 
					    if (!me && options.requireSession) {
 | 
				
			||||||
      throw new Error('you must be logged in')
 | 
					      throw new Error('you must be logged in')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // id for toast flows
 | 
				
			||||||
 | 
					    if (!flowId) flowId = (+new Date()).toString(16)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // educated guesses where action might pass in the invoice amount
 | 
					    // educated guesses where action might pass in the invoice amount
 | 
				
			||||||
    // (field 'cost' has highest precedence)
 | 
					    // (field 'cost' has highest precedence)
 | 
				
			||||||
    cost ??= formValues.amount
 | 
					    cost ??= formValues.amount
 | 
				
			||||||
@ -201,7 +204,7 @@ export const useInvoiceable = (onSubmit, options = defaultOptions) => {
 | 
				
			|||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const insufficientFunds = me?.privates.sats < cost
 | 
					        const insufficientFunds = me?.privates.sats < cost
 | 
				
			||||||
        return await onSubmit(formValues,
 | 
					        return await onSubmit(formValues,
 | 
				
			||||||
          { ...submitArgs, variables, optimisticsResponse: insufficientFunds ? null : optimisticResponse })
 | 
					          { ...submitArgs, flowId, variables, optimisticsResponse: insufficientFunds ? null : optimisticResponse, update })
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        if (!payOrLoginError(error) || !cost) {
 | 
					        if (!payOrLoginError(error) || !cost) {
 | 
				
			||||||
          // can't handle error here - bail
 | 
					          // can't handle error here - bail
 | 
				
			||||||
@ -249,12 +252,14 @@ export const useInvoiceable = (onSubmit, options = defaultOptions) => {
 | 
				
			|||||||
      showModal,
 | 
					      showModal,
 | 
				
			||||||
      provider,
 | 
					      provider,
 | 
				
			||||||
      pollInvoice,
 | 
					      pollInvoice,
 | 
				
			||||||
      gqlCacheUpdate: _update
 | 
					      gqlCacheUpdate: _update,
 | 
				
			||||||
 | 
					      flowId
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const retry = () => onSubmit(
 | 
					    const retry = () => onSubmit(
 | 
				
			||||||
      { hash: inv.hash, hmac: inv.hmac, ...formValues },
 | 
					      { hash: inv.hash, hmac: inv.hmac, ...formValues },
 | 
				
			||||||
      // unset update function since we already ran an cache update if we paid using WebLN
 | 
					      // unset update function since we already ran an cache update if we paid using WebLN
 | 
				
			||||||
 | 
					      // also unset update function if null was explicitly passed in
 | 
				
			||||||
      { ...submitArgs, variables, update: webLn ? null : undefined })
 | 
					      { ...submitArgs, variables, update: webLn ? null : undefined })
 | 
				
			||||||
    // first retry
 | 
					    // first retry
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@ -294,10 +299,10 @@ export const useInvoiceable = (onSubmit, options = defaultOptions) => {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const INVOICE_CANCELED_ERROR = 'invoice canceled'
 | 
					const INVOICE_CANCELED_ERROR = 'invoice canceled'
 | 
				
			||||||
const waitForPayment = async ({ invoice, showModal, provider, pollInvoice, gqlCacheUpdate }) => {
 | 
					const waitForPayment = async ({ invoice, showModal, provider, pollInvoice, gqlCacheUpdate, flowId }) => {
 | 
				
			||||||
  if (provider.enabled) {
 | 
					  if (provider.enabled) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      return await waitForWebLNPayment({ provider, invoice, pollInvoice, gqlCacheUpdate })
 | 
					      return await waitForWebLNPayment({ provider, invoice, pollInvoice, gqlCacheUpdate, flowId })
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      // check for errors which mean that QR code will also fail
 | 
					      // check for errors which mean that QR code will also fail
 | 
				
			||||||
      if (err.message === INVOICE_CANCELED_ERROR) {
 | 
					      if (err.message === INVOICE_CANCELED_ERROR) {
 | 
				
			||||||
@ -319,7 +324,7 @@ const waitForPayment = async ({ invoice, showModal, provider, pollInvoice, gqlCa
 | 
				
			|||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const waitForWebLNPayment = async ({ provider, invoice, pollInvoice, gqlCacheUpdate }) => {
 | 
					const waitForWebLNPayment = async ({ provider, invoice, pollInvoice, gqlCacheUpdate, flowId }) => {
 | 
				
			||||||
  let undoUpdate
 | 
					  let undoUpdate
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // try WebLN provider first
 | 
					    // try WebLN provider first
 | 
				
			||||||
@ -329,7 +334,7 @@ const waitForWebLNPayment = async ({ provider, invoice, pollInvoice, gqlCacheUpd
 | 
				
			|||||||
      // can't use await here since we might be paying JIT invoices
 | 
					      // can't use await here since we might be paying JIT invoices
 | 
				
			||||||
      // and sendPaymentAsync is not supported yet.
 | 
					      // and sendPaymentAsync is not supported yet.
 | 
				
			||||||
      // see https://www.webln.guide/building-lightning-apps/webln-reference/webln.sendpaymentasync
 | 
					      // see https://www.webln.guide/building-lightning-apps/webln-reference/webln.sendpaymentasync
 | 
				
			||||||
      provider.sendPayment(invoice)
 | 
					      provider.sendPayment({ ...invoice, flowId })
 | 
				
			||||||
        // WebLN payment will never resolve here for JIT invoices
 | 
					        // WebLN payment will never resolve here for JIT invoices
 | 
				
			||||||
        // since they only get resolved after settlement which can't happen here
 | 
					        // since they only get resolved after settlement which can't happen here
 | 
				
			||||||
        .then(() => resolve({ webLn: true, gqlCacheUpdateUndo: undoUpdate }))
 | 
					        .then(() => resolve({ webLn: true, gqlCacheUpdateUndo: undoUpdate }))
 | 
				
			||||||
 | 
				
			|||||||
@ -5,9 +5,9 @@ import { Form, Input, SubmitButton } from './form'
 | 
				
			|||||||
import { useMe } from './me'
 | 
					import { useMe } from './me'
 | 
				
			||||||
import UpBolt from '../svgs/bolt.svg'
 | 
					import UpBolt from '../svgs/bolt.svg'
 | 
				
			||||||
import { amountSchema } from '../lib/validate'
 | 
					import { amountSchema } from '../lib/validate'
 | 
				
			||||||
import { gql, useMutation } from '@apollo/client'
 | 
					import { gql, useApolloClient, useMutation } from '@apollo/client'
 | 
				
			||||||
import { payOrLoginError, useInvoiceModal } from './invoice'
 | 
					import { payOrLoginError, useInvoiceModal } from './invoice'
 | 
				
			||||||
import { useToast } from './toast'
 | 
					import { TOAST_DEFAULT_DELAY_MS, useToast, withToastFlow } from './toast'
 | 
				
			||||||
import { useLightning } from './lightning'
 | 
					import { useLightning } from './lightning'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const defaultTips = [100, 1000, 10000, 100000]
 | 
					const defaultTips = [100, 1000, 10000, 100000]
 | 
				
			||||||
@ -45,14 +45,16 @@ export default function ItemAct ({ onClose, itemId, down, children }) {
 | 
				
			|||||||
  const me = useMe()
 | 
					  const me = useMe()
 | 
				
			||||||
  const [oValue, setOValue] = useState()
 | 
					  const [oValue, setOValue] = useState()
 | 
				
			||||||
  const strike = useLightning()
 | 
					  const strike = useLightning()
 | 
				
			||||||
 | 
					  const toaster = useToast()
 | 
				
			||||||
 | 
					  const client = useApolloClient()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    inputRef.current?.focus()
 | 
					    inputRef.current?.focus()
 | 
				
			||||||
  }, [onClose, itemId])
 | 
					  }, [onClose, itemId])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [act] = useAct()
 | 
					  const [act, actUpdate] = useAct()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onSubmit = useCallback(async ({ amount, hash, hmac }) => {
 | 
					  const onSubmit = useCallback(async ({ amount, hash, hmac }, { update }) => {
 | 
				
			||||||
    if (!me) {
 | 
					    if (!me) {
 | 
				
			||||||
      const storageKey = `TIP-item:${itemId}`
 | 
					      const storageKey = `TIP-item:${itemId}`
 | 
				
			||||||
      const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
 | 
					      const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
 | 
				
			||||||
@ -65,12 +67,75 @@ export default function ItemAct ({ onClose, itemId, down, children }) {
 | 
				
			|||||||
        act: down ? 'DONT_LIKE_THIS' : 'TIP',
 | 
					        act: down ? 'DONT_LIKE_THIS' : 'TIP',
 | 
				
			||||||
        hash,
 | 
					        hash,
 | 
				
			||||||
        hmac
 | 
					        hmac
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
 | 
					      update
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    await strike()
 | 
					    // only strike when zap undos not enabled
 | 
				
			||||||
 | 
					    // due to optimistic UX on zap undos
 | 
				
			||||||
 | 
					    if (!me || !me.privates.zapUndos) await strike()
 | 
				
			||||||
    addCustomTip(Number(amount))
 | 
					    addCustomTip(Number(amount))
 | 
				
			||||||
    onClose()
 | 
					    onClose()
 | 
				
			||||||
  }, [act, down, itemId, strike])
 | 
					  }, [me, act, down, itemId, strike])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onSubmitWithUndos = withToastFlow(toaster)(
 | 
				
			||||||
 | 
					    (values, args) => {
 | 
				
			||||||
 | 
					      const { flowId } = args
 | 
				
			||||||
 | 
					      let canceled
 | 
				
			||||||
 | 
					      const sats = values.amount
 | 
				
			||||||
 | 
					      const insufficientFunds = me?.privates?.sats < sats
 | 
				
			||||||
 | 
					      if (insufficientFunds) throw new Error('insufficient funds')
 | 
				
			||||||
 | 
					      // update function for optimistic UX
 | 
				
			||||||
 | 
					      const update = () => {
 | 
				
			||||||
 | 
					        const fragment = {
 | 
				
			||||||
 | 
					          id: `Item:${itemId}`,
 | 
				
			||||||
 | 
					          fragment: gql`
 | 
				
			||||||
 | 
					          fragment ItemMeSats on Item {
 | 
				
			||||||
 | 
					            path
 | 
				
			||||||
 | 
					            sats
 | 
				
			||||||
 | 
					            meSats
 | 
				
			||||||
 | 
					            meDontLikeSats
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        `
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const item = client.cache.readFragment(fragment)
 | 
				
			||||||
 | 
					        const optimisticResponse = {
 | 
				
			||||||
 | 
					          act: {
 | 
				
			||||||
 | 
					            id: itemId, sats, path: item.path, act: down ? 'DONT_LIKE_THIS' : 'TIP'
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        actUpdate(client.cache, { data: optimisticResponse })
 | 
				
			||||||
 | 
					        return () => client.cache.writeFragment({ ...fragment, data: item })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      let undoUpdate
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        flowId,
 | 
				
			||||||
 | 
					        type: 'zap',
 | 
				
			||||||
 | 
					        pendingMessage: `${down ? 'down' : ''}zapped ${sats} sats`,
 | 
				
			||||||
 | 
					        onPending: async () => {
 | 
				
			||||||
 | 
					          await strike()
 | 
				
			||||||
 | 
					          onClose()
 | 
				
			||||||
 | 
					          return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					            undoUpdate = update()
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					              if (canceled) return resolve()
 | 
				
			||||||
 | 
					              onSubmit(values, { flowId, ...args, update: null })
 | 
				
			||||||
 | 
					                .then(resolve)
 | 
				
			||||||
 | 
					                .catch((err) => {
 | 
				
			||||||
 | 
					                  undoUpdate()
 | 
				
			||||||
 | 
					                  reject(err)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }, TOAST_DEFAULT_DELAY_MS)
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        onUndo: () => {
 | 
				
			||||||
 | 
					          canceled = true
 | 
				
			||||||
 | 
					          undoUpdate?.()
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        hideSuccess: true,
 | 
				
			||||||
 | 
					        hideError: true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Form
 | 
					    <Form
 | 
				
			||||||
@ -80,7 +145,7 @@ export default function ItemAct ({ onClose, itemId, down, children }) {
 | 
				
			|||||||
      }}
 | 
					      }}
 | 
				
			||||||
      schema={amountSchema}
 | 
					      schema={amountSchema}
 | 
				
			||||||
      invoiceable
 | 
					      invoiceable
 | 
				
			||||||
      onSubmit={onSubmit}
 | 
					      onSubmit={me?.privates?.zapUndos ? onSubmitWithUndos : onSubmit}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <Input
 | 
					      <Input
 | 
				
			||||||
        label='amount'
 | 
					        label='amount'
 | 
				
			||||||
@ -158,7 +223,7 @@ export function useAct ({ onUpdate } = {}) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [!!me, onUpdate])
 | 
					  }, [!!me, onUpdate])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return useMutation(
 | 
					  const [act] = useMutation(
 | 
				
			||||||
    gql`
 | 
					    gql`
 | 
				
			||||||
      mutation act($id: ID!, $sats: Int!, $act: String, $hash: String, $hmac: String) {
 | 
					      mutation act($id: ID!, $sats: Int!, $act: String, $hash: String, $hmac: String) {
 | 
				
			||||||
        act(id: $id, sats: $sats, act: $act, hash: $hash, hmac: $hmac) {
 | 
					        act(id: $id, sats: $sats, act: $act, hash: $hash, hmac: $hmac) {
 | 
				
			||||||
@ -169,6 +234,7 @@ export function useAct ({ onUpdate } = {}) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }`, { update }
 | 
					      }`, { update }
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					  return [act, update]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useZap () {
 | 
					export function useZap () {
 | 
				
			||||||
@ -227,12 +293,13 @@ export function useZap () {
 | 
				
			|||||||
          sats
 | 
					          sats
 | 
				
			||||||
          path
 | 
					          path
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }`, { update }
 | 
					      }`
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const toaster = useToast()
 | 
					  const toaster = useToast()
 | 
				
			||||||
  const strike = useLightning()
 | 
					  const strike = useLightning()
 | 
				
			||||||
  const [act] = useAct()
 | 
					  const [act] = useAct()
 | 
				
			||||||
 | 
					  const client = useApolloClient()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const invoiceableAct = useInvoiceModal(
 | 
					  const invoiceableAct = useInvoiceModal(
 | 
				
			||||||
    async ({ hash, hmac }, { variables, ...apolloArgs }) => {
 | 
					    async ({ hash, hmac }, { variables, ...apolloArgs }) => {
 | 
				
			||||||
@ -240,6 +307,57 @@ export function useZap () {
 | 
				
			|||||||
      strike()
 | 
					      strike()
 | 
				
			||||||
    }, [act, strike])
 | 
					    }, [act, strike])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const zapWithUndos = withToastFlow(toaster)(
 | 
				
			||||||
 | 
					    ({ variables, optimisticResponse, update, flowId }) => {
 | 
				
			||||||
 | 
					      const { id: itemId, amount } = variables
 | 
				
			||||||
 | 
					      let canceled
 | 
				
			||||||
 | 
					      // update function for optimistic UX
 | 
				
			||||||
 | 
					      const _update = () => {
 | 
				
			||||||
 | 
					        const fragment = {
 | 
				
			||||||
 | 
					          id: `Item:${itemId}`,
 | 
				
			||||||
 | 
					          fragment: gql`
 | 
				
			||||||
 | 
					          fragment ItemMeSats on Item {
 | 
				
			||||||
 | 
					            sats
 | 
				
			||||||
 | 
					            meSats
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        `
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const item = client.cache.readFragment(fragment)
 | 
				
			||||||
 | 
					        update(client.cache, { data: optimisticResponse })
 | 
				
			||||||
 | 
					        // undo function
 | 
				
			||||||
 | 
					        return () => client.cache.writeFragment({ ...fragment, data: item })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      let undoUpdate
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        flowId,
 | 
				
			||||||
 | 
					        type: 'zap',
 | 
				
			||||||
 | 
					        pendingMessage: `zapped ${amount} sats`,
 | 
				
			||||||
 | 
					        onPending: () =>
 | 
				
			||||||
 | 
					          new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					            undoUpdate = _update()
 | 
				
			||||||
 | 
					            setTimeout(
 | 
				
			||||||
 | 
					              () => {
 | 
				
			||||||
 | 
					                if (canceled) return resolve()
 | 
				
			||||||
 | 
					                zap({ variables, optimisticResponse, update: null }).then(resolve).catch((err) => {
 | 
				
			||||||
 | 
					                  undoUpdate()
 | 
				
			||||||
 | 
					                  reject(err)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              TOAST_DEFAULT_DELAY_MS
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        onUndo: () => {
 | 
				
			||||||
 | 
					          // we can't simply clear the timeout on cancel since
 | 
				
			||||||
 | 
					          // the onPending promise would never settle in that case
 | 
				
			||||||
 | 
					          canceled = true
 | 
				
			||||||
 | 
					          undoUpdate?.()
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        hideSuccess: true,
 | 
				
			||||||
 | 
					        hideError: true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return useCallback(async ({ item, me }) => {
 | 
					  return useCallback(async ({ item, me }) => {
 | 
				
			||||||
    const meSats = (item?.meSats || 0)
 | 
					    const meSats = (item?.meSats || 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -253,12 +371,19 @@ export function useZap () {
 | 
				
			|||||||
      sats = meSats + sats
 | 
					      sats = meSats + sats
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const variables = { id: item.id, sats, act: 'TIP' }
 | 
					    const variables = { id: item.id, sats, act: 'TIP', amount: sats - meSats }
 | 
				
			||||||
    const insufficientFunds = me?.privates.sats < sats
 | 
					    const insufficientFunds = me?.privates.sats < (sats - meSats)
 | 
				
			||||||
    const optimisticResponse = { act: { path: item.path, ...variables } }
 | 
					    const optimisticResponse = { act: { path: item.path, ...variables } }
 | 
				
			||||||
 | 
					    const flowId = (+new Date()).toString(16)
 | 
				
			||||||
 | 
					    const zapArgs = { variables, optimisticResponse: insufficientFunds ? null : optimisticResponse, update, flowId }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      if (!insufficientFunds) strike()
 | 
					      if (insufficientFunds) throw new Error('insufficient funds')
 | 
				
			||||||
      await zap({ variables, optimisticResponse: insufficientFunds ? null : optimisticResponse })
 | 
					      strike()
 | 
				
			||||||
 | 
					      if (me?.privates?.zapUndos) {
 | 
				
			||||||
 | 
					        await zapWithUndos(zapArgs)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        await zap(zapArgs)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      if (payOrLoginError(error)) {
 | 
					      if (payOrLoginError(error)) {
 | 
				
			||||||
        // call non-idempotent version
 | 
					        // call non-idempotent version
 | 
				
			||||||
@ -268,7 +393,8 @@ export function useZap () {
 | 
				
			|||||||
          await invoiceableAct({ amount }, {
 | 
					          await invoiceableAct({ amount }, {
 | 
				
			||||||
            variables: { ...variables, sats: amount },
 | 
					            variables: { ...variables, sats: amount },
 | 
				
			||||||
            optimisticResponse,
 | 
					            optimisticResponse,
 | 
				
			||||||
            update
 | 
					            update,
 | 
				
			||||||
 | 
					            flowId
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        } catch (error) {}
 | 
					        } catch (error) {}
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,8 @@ import styles from './toast.module.css'
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const ToastContext = createContext(() => {})
 | 
					const ToastContext = createContext(() => {})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const TOAST_DEFAULT_DELAY_MS = 5000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ToastProvider = ({ children }) => {
 | 
					export const ToastProvider = ({ children }) => {
 | 
				
			||||||
  const router = useRouter()
 | 
					  const router = useRouter()
 | 
				
			||||||
  const [toasts, setToasts] = useState([])
 | 
					  const [toasts, setToasts] = useState([])
 | 
				
			||||||
@ -16,6 +18,7 @@ export const ToastProvider = ({ children }) => {
 | 
				
			|||||||
  const dispatchToast = useCallback((toast) => {
 | 
					  const dispatchToast = useCallback((toast) => {
 | 
				
			||||||
    toast = {
 | 
					    toast = {
 | 
				
			||||||
      ...toast,
 | 
					      ...toast,
 | 
				
			||||||
 | 
					      createdAt: +new Date(),
 | 
				
			||||||
      id: toastId.current++
 | 
					      id: toastId.current++
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const { flowId } = toast
 | 
					    const { flowId } = toast
 | 
				
			||||||
@ -46,7 +49,7 @@ export const ToastProvider = ({ children }) => {
 | 
				
			|||||||
        // don't touch toasts with different tags
 | 
					        // don't touch toasts with different tags
 | 
				
			||||||
        return true
 | 
					        return true
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const toRemoveHasCancel = !!toast.onCancel
 | 
					      const toRemoveHasCancel = !!toast.onCancel || !!toast.onUndo
 | 
				
			||||||
      if (toRemoveHasCancel) {
 | 
					      if (toRemoveHasCancel) {
 | 
				
			||||||
        // don't remove this toast so the user can decide to cancel this toast now
 | 
					        // don't remove this toast so the user can decide to cancel this toast now
 | 
				
			||||||
        return true
 | 
					        return true
 | 
				
			||||||
@ -62,7 +65,7 @@ export const ToastProvider = ({ children }) => {
 | 
				
			|||||||
        body,
 | 
					        body,
 | 
				
			||||||
        variant: 'success',
 | 
					        variant: 'success',
 | 
				
			||||||
        autohide: true,
 | 
					        autohide: true,
 | 
				
			||||||
        delay: 5000,
 | 
					        delay: TOAST_DEFAULT_DELAY_MS,
 | 
				
			||||||
        tag: options?.tag || body,
 | 
					        tag: options?.tag || body,
 | 
				
			||||||
        ...options
 | 
					        ...options
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -73,7 +76,7 @@ export const ToastProvider = ({ children }) => {
 | 
				
			|||||||
        body,
 | 
					        body,
 | 
				
			||||||
        variant: 'warning',
 | 
					        variant: 'warning',
 | 
				
			||||||
        autohide: true,
 | 
					        autohide: true,
 | 
				
			||||||
        delay: 5000,
 | 
					        delay: TOAST_DEFAULT_DELAY_MS,
 | 
				
			||||||
        tag: options?.tag || body,
 | 
					        tag: options?.tag || body,
 | 
				
			||||||
        ...options
 | 
					        ...options
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -94,7 +97,7 @@ export const ToastProvider = ({ children }) => {
 | 
				
			|||||||
  // Only clear toasts with no cancel function on page navigation
 | 
					  // Only clear toasts with no cancel function on page navigation
 | 
				
			||||||
  // since navigation should not interfere with being able to cancel an action.
 | 
					  // since navigation should not interfere with being able to cancel an action.
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const handleRouteChangeStart = () => setToasts(toasts => toasts.filter(({ onCancel }) => onCancel), [])
 | 
					    const handleRouteChangeStart = () => setToasts(toasts => toasts.filter(({ onCancel, onUndo }) => onCancel || onUndo), [])
 | 
				
			||||||
    router.events.on('routeChangeStart', handleRouteChangeStart)
 | 
					    router.events.on('routeChangeStart', handleRouteChangeStart)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
@ -135,6 +138,18 @@ export const ToastProvider = ({ children }) => {
 | 
				
			|||||||
      <ToastContainer className={`pb-3 pe-3 ${styles.toastContainer}`} position='bottom-end' containerPosition='fixed'>
 | 
					      <ToastContainer className={`pb-3 pe-3 ${styles.toastContainer}`} position='bottom-end' containerPosition='fixed'>
 | 
				
			||||||
        {visibleToasts.map(toast => {
 | 
					        {visibleToasts.map(toast => {
 | 
				
			||||||
          const textStyle = toast.variant === 'warning' ? 'text-dark' : ''
 | 
					          const textStyle = toast.variant === 'warning' ? 'text-dark' : ''
 | 
				
			||||||
 | 
					          const onClose = () => {
 | 
				
			||||||
 | 
					            toast.onUndo?.()
 | 
				
			||||||
 | 
					            toast.onCancel?.()
 | 
				
			||||||
 | 
					            toast.onClose?.()
 | 
				
			||||||
 | 
					            removeToast(toast)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          const buttonElement = toast.onUndo
 | 
				
			||||||
 | 
					            ? <div className={`${styles.toastUndo} ${textStyle}`}>undo</div>
 | 
				
			||||||
 | 
					            : toast.onCancel
 | 
				
			||||||
 | 
					              ? <div className={`${styles.toastCancel} ${textStyle}`}>cancel</div>
 | 
				
			||||||
 | 
					              : <div className={`${styles.toastClose} ${textStyle}`}>X</div>
 | 
				
			||||||
 | 
					          const elapsed = (+new Date() - toast.createdAt)
 | 
				
			||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <Toast
 | 
					            <Toast
 | 
				
			||||||
              key={toast.id} bg={toast.variant} show autohide={toast.autohide}
 | 
					              key={toast.id} bg={toast.variant} show autohide={toast.autohide}
 | 
				
			||||||
@ -147,15 +162,12 @@ export const ToastProvider = ({ children }) => {
 | 
				
			|||||||
                    variant={null}
 | 
					                    variant={null}
 | 
				
			||||||
                    className='p-0 ps-2'
 | 
					                    className='p-0 ps-2'
 | 
				
			||||||
                    aria-label='close'
 | 
					                    aria-label='close'
 | 
				
			||||||
                    onClick={() => {
 | 
					                    onClick={onClose}
 | 
				
			||||||
                      toast.onCancel?.()
 | 
					                  >{buttonElement}
 | 
				
			||||||
                      toast.onClose?.()
 | 
					 | 
				
			||||||
                      removeToast(toast)
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  >{toast.onCancel ? <div className={`${styles.toastCancel} ${textStyle}`}>cancel</div> : <div className={`${styles.toastClose} ${textStyle}`}>X</div>}
 | 
					 | 
				
			||||||
                  </Button>
 | 
					                  </Button>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </ToastBody>
 | 
					              </ToastBody>
 | 
				
			||||||
 | 
					              {toast.delay > 0 && <div className={`${styles.progressBar} ${styles[toast.variant]}`} style={{ animationDelay: `-${elapsed}ms` }} />}
 | 
				
			||||||
            </Toast>
 | 
					            </Toast>
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
        })}
 | 
					        })}
 | 
				
			||||||
@ -173,28 +185,54 @@ export const withToastFlow = (toaster) => flowFn => {
 | 
				
			|||||||
      flowId,
 | 
					      flowId,
 | 
				
			||||||
      type: t,
 | 
					      type: t,
 | 
				
			||||||
      onPending,
 | 
					      onPending,
 | 
				
			||||||
 | 
					      pendingMessage,
 | 
				
			||||||
      onSuccess,
 | 
					      onSuccess,
 | 
				
			||||||
      onCancel,
 | 
					      onCancel,
 | 
				
			||||||
      onError
 | 
					      onError,
 | 
				
			||||||
 | 
					      onUndo,
 | 
				
			||||||
 | 
					      hideError,
 | 
				
			||||||
 | 
					      hideSuccess,
 | 
				
			||||||
 | 
					      ...toastProps
 | 
				
			||||||
    } = flowFn(...args)
 | 
					    } = flowFn(...args)
 | 
				
			||||||
    let canceled
 | 
					    let canceled
 | 
				
			||||||
    toaster.warning(`${t} pending`, {
 | 
					
 | 
				
			||||||
 | 
					    // XXX HACK this ends the flow by using flow toast which immediately closes itself
 | 
				
			||||||
 | 
					    const endFlow = () => toaster.warning('', { ...toastProps, delay: 0, autohide: true, flowId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    toaster.warning(pendingMessage || `${t} pending`, {
 | 
				
			||||||
      autohide: false,
 | 
					      autohide: false,
 | 
				
			||||||
      onCancel: async () => {
 | 
					      onCancel: onCancel
 | 
				
			||||||
        try {
 | 
					        ? async () => {
 | 
				
			||||||
          await onCancel?.()
 | 
					          try {
 | 
				
			||||||
          canceled = true
 | 
					            await onCancel()
 | 
				
			||||||
          toaster.warning(`${t} canceled`, { flowId })
 | 
					            canceled = true
 | 
				
			||||||
        } catch (err) {
 | 
					            toaster.warning(`${t} canceled`, { ...toastProps, flowId })
 | 
				
			||||||
          toaster.danger(`failed to cancel ${t}`, { flowId })
 | 
					          } catch (err) {
 | 
				
			||||||
 | 
					            toaster.danger(`failed to cancel ${t}`, { ...toastProps, flowId })
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      },
 | 
					        : undefined,
 | 
				
			||||||
      flowId
 | 
					      onUndo: onUndo
 | 
				
			||||||
 | 
					        ? async () => {
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            await onUndo()
 | 
				
			||||||
 | 
					            canceled = true
 | 
				
			||||||
 | 
					          } catch (err) {
 | 
				
			||||||
 | 
					            toaster.danger(`failed to undo ${t}`, { ...toastProps, flowId })
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        : undefined,
 | 
				
			||||||
 | 
					      flowId,
 | 
				
			||||||
 | 
					      ...toastProps
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const ret = await onPending()
 | 
					      const ret = await onPending()
 | 
				
			||||||
      if (!canceled) {
 | 
					      if (!canceled) {
 | 
				
			||||||
        toaster.success(`${t} successful`, { flowId })
 | 
					        if (hideSuccess) {
 | 
				
			||||||
 | 
					          endFlow()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          toaster.success(`${t} successful`, { ...toastProps, flowId })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        await onSuccess?.()
 | 
					        await onSuccess?.()
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return ret
 | 
					      return ret
 | 
				
			||||||
@ -202,7 +240,11 @@ export const withToastFlow = (toaster) => flowFn => {
 | 
				
			|||||||
      // ignore errors if canceled since they might be caused by cancellation
 | 
					      // ignore errors if canceled since they might be caused by cancellation
 | 
				
			||||||
      if (canceled) return
 | 
					      if (canceled) return
 | 
				
			||||||
      const reason = err?.message?.toString().toLowerCase() || 'unknown reason'
 | 
					      const reason = err?.message?.toString().toLowerCase() || 'unknown reason'
 | 
				
			||||||
      toaster.danger(`${t} failed: ${reason}`, { flowId })
 | 
					      if (hideError) {
 | 
				
			||||||
 | 
					        endFlow()
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        toaster.danger(`${t} failed: ${reason}`, { ...toastProps, flowId })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      await onError?.()
 | 
					      await onError?.()
 | 
				
			||||||
      throw err
 | 
					      throw err
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
.toastContainer {
 | 
					.toastContainer {
 | 
				
			||||||
  transform: translate3d(0,0,0);
 | 
					  transform: translate3d(0, 0, 0);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.toast {
 | 
					.toast {
 | 
				
			||||||
@ -21,6 +21,13 @@
 | 
				
			|||||||
  border-color: var(--bs-warning-border-subtle);
 | 
					  border-color: var(--bs-warning-border-subtle);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toastUndo {
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.toastCancel {
 | 
					.toastCancel {
 | 
				
			||||||
  font-style: italic;
 | 
					  font-style: italic;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
@ -39,6 +46,38 @@
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.progressBar {
 | 
				
			||||||
 | 
					  width: 0;
 | 
				
			||||||
 | 
					  height: 5px;
 | 
				
			||||||
 | 
					  filter: brightness(66%);
 | 
				
			||||||
 | 
					  /* same duration as toast delay */
 | 
				
			||||||
 | 
					  animation: progressBar 5s linear;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.progressBar.success {
 | 
				
			||||||
 | 
					  background-color: var(--bs-success);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.progressBar.danger {
 | 
				
			||||||
 | 
					  background-color: var(--bs-danger);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.progressBar.warning {
 | 
				
			||||||
 | 
					  background-color: var(--bs-warning);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes progressBar {
 | 
				
			||||||
 | 
					  0% {
 | 
				
			||||||
 | 
					    width: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  100% {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.toastClose:hover {
 | 
					.toastClose:hover {
 | 
				
			||||||
  opacity: 0.7;
 | 
					  opacity: 0.7;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -47,4 +86,4 @@
 | 
				
			|||||||
  .toast {
 | 
					  .toast {
 | 
				
			||||||
    width: var(--bs-toast-max-width);
 | 
					    width: var(--bs-toast-max-width);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -82,9 +82,9 @@ function RawWebLNProvider ({ children }) {
 | 
				
			|||||||
  `)
 | 
					  `)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const sendPaymentWithToast = withToastFlow(toaster)(
 | 
					  const sendPaymentWithToast = withToastFlow(toaster)(
 | 
				
			||||||
    ({ bolt11, hash, hmac }) => {
 | 
					    ({ bolt11, hash, hmac, flowId }) => {
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        flowId: hash,
 | 
					        flowId: flowId || hash,
 | 
				
			||||||
        type: 'payment',
 | 
					        type: 'payment',
 | 
				
			||||||
        onPending: () => provider.sendPayment(bolt11),
 | 
					        onPending: () => provider.sendPayment(bolt11),
 | 
				
			||||||
        // hash and hmac are only passed for JIT invoices
 | 
					        // hash and hmac are only passed for JIT invoices
 | 
				
			||||||
 | 
				
			|||||||
@ -39,6 +39,7 @@ export const ME = gql`
 | 
				
			|||||||
        tipDefault
 | 
					        tipDefault
 | 
				
			||||||
        tipPopover
 | 
					        tipPopover
 | 
				
			||||||
        turboTipping
 | 
					        turboTipping
 | 
				
			||||||
 | 
					        zapUndos
 | 
				
			||||||
        upvotePopover
 | 
					        upvotePopover
 | 
				
			||||||
        wildWestMode
 | 
					        wildWestMode
 | 
				
			||||||
        withdrawMaxFeeDefault
 | 
					        withdrawMaxFeeDefault
 | 
				
			||||||
@ -62,6 +63,7 @@ export const SETTINGS_FIELDS = gql`
 | 
				
			|||||||
    privates {
 | 
					    privates {
 | 
				
			||||||
      tipDefault
 | 
					      tipDefault
 | 
				
			||||||
      turboTipping
 | 
					      turboTipping
 | 
				
			||||||
 | 
					      zapUndos
 | 
				
			||||||
      fiatCurrency
 | 
					      fiatCurrency
 | 
				
			||||||
      withdrawMaxFeeDefault
 | 
					      withdrawMaxFeeDefault
 | 
				
			||||||
      noteItemSats
 | 
					      noteItemSats
 | 
				
			||||||
 | 
				
			|||||||
@ -63,6 +63,7 @@ export default function Settings ({ ssrData }) {
 | 
				
			|||||||
          initial={{
 | 
					          initial={{
 | 
				
			||||||
            tipDefault: settings?.tipDefault || 21,
 | 
					            tipDefault: settings?.tipDefault || 21,
 | 
				
			||||||
            turboTipping: settings?.turboTipping,
 | 
					            turboTipping: settings?.turboTipping,
 | 
				
			||||||
 | 
					            zapUndos: settings?.zapUndos,
 | 
				
			||||||
            fiatCurrency: settings?.fiatCurrency || 'USD',
 | 
					            fiatCurrency: settings?.fiatCurrency || 'USD',
 | 
				
			||||||
            withdrawMaxFeeDefault: settings?.withdrawMaxFeeDefault,
 | 
					            withdrawMaxFeeDefault: settings?.withdrawMaxFeeDefault,
 | 
				
			||||||
            noteItemSats: settings?.noteItemSats,
 | 
					            noteItemSats: settings?.noteItemSats,
 | 
				
			||||||
@ -139,32 +140,50 @@ export default function Settings ({ ssrData }) {
 | 
				
			|||||||
            <AccordianItem
 | 
					            <AccordianItem
 | 
				
			||||||
              show={settings?.turboTipping}
 | 
					              show={settings?.turboTipping}
 | 
				
			||||||
              header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>advanced</div>}
 | 
					              header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>advanced</div>}
 | 
				
			||||||
              body={<Checkbox
 | 
					              body={
 | 
				
			||||||
                name='turboTipping'
 | 
					                <>
 | 
				
			||||||
                label={
 | 
					                  <Checkbox
 | 
				
			||||||
                  <div className='d-flex align-items-center'>turbo zapping
 | 
					                    name='turboTipping'
 | 
				
			||||||
                    <Info>
 | 
					                    label={
 | 
				
			||||||
                      <ul className='fw-bold'>
 | 
					                      <div className='d-flex align-items-center'>turbo zapping
 | 
				
			||||||
                        <li>Makes every additional bolt click raise your total zap to another 10x multiple of your default zap</li>
 | 
					                        <Info>
 | 
				
			||||||
                        <li>e.g. if your zap default is 10 sats
 | 
					                          <ul className='fw-bold'>
 | 
				
			||||||
                          <ul>
 | 
					                            <li>Makes every additional bolt click raise your total zap to another 10x multiple of your default zap</li>
 | 
				
			||||||
                            <li>1st click: 10 sats total zapped</li>
 | 
					                            <li>e.g. if your zap default is 10 sats
 | 
				
			||||||
                            <li>2nd click: 100 sats total zapped</li>
 | 
					                              <ul>
 | 
				
			||||||
                            <li>3rd click: 1000 sats total zapped</li>
 | 
					                                <li>1st click: 10 sats total zapped</li>
 | 
				
			||||||
                            <li>4th click: 10000 sats total zapped</li>
 | 
					                                <li>2nd click: 100 sats total zapped</li>
 | 
				
			||||||
                            <li>and so on ...</li>
 | 
					                                <li>3rd click: 1000 sats total zapped</li>
 | 
				
			||||||
 | 
					                                <li>4th click: 10000 sats total zapped</li>
 | 
				
			||||||
 | 
					                                <li>and so on ...</li>
 | 
				
			||||||
 | 
					                              </ul>
 | 
				
			||||||
 | 
					                            </li>
 | 
				
			||||||
 | 
					                            <li>You can still custom zap via long press
 | 
				
			||||||
 | 
					                              <ul>
 | 
				
			||||||
 | 
					                                <li>the next bolt click rounds up to the next greatest 10x multiple of your default</li>
 | 
				
			||||||
 | 
					                              </ul>
 | 
				
			||||||
 | 
					                            </li>
 | 
				
			||||||
                          </ul>
 | 
					                          </ul>
 | 
				
			||||||
                        </li>
 | 
					                        </Info>
 | 
				
			||||||
                        <li>You can still custom zap via long press
 | 
					                      </div>
 | 
				
			||||||
                          <ul>
 | 
					                    }
 | 
				
			||||||
                            <li>the next bolt click rounds up to the next greatest 10x multiple of your default</li>
 | 
					                    groupClassName='mb-0'
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                  <Checkbox
 | 
				
			||||||
 | 
					                    name='zapUndos'
 | 
				
			||||||
 | 
					                    label={
 | 
				
			||||||
 | 
					                      <div className='d-flex align-items-center'>zap undos
 | 
				
			||||||
 | 
					                        <Info>
 | 
				
			||||||
 | 
					                          <ul className='fw-bold'>
 | 
				
			||||||
 | 
					                            <li>An undo button is shown after every zap</li>
 | 
				
			||||||
 | 
					                            <li>The button is shown for 5 seconds</li>
 | 
				
			||||||
                          </ul>
 | 
					                          </ul>
 | 
				
			||||||
                        </li>
 | 
					                        </Info>
 | 
				
			||||||
                      </ul>
 | 
					                      </div>
 | 
				
			||||||
                    </Info>
 | 
					                    }
 | 
				
			||||||
                  </div>
 | 
					                  />
 | 
				
			||||||
                  }
 | 
					                </>
 | 
				
			||||||
                    />}
 | 
					              }
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <Select
 | 
					          <Select
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								prisma/migrations/20240219225338_zap_undos/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								prisma/migrations/20240219225338_zap_undos/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					-- AlterTable
 | 
				
			||||||
 | 
					ALTER TABLE "users" ADD COLUMN     "zapUndos" BOOLEAN NOT NULL DEFAULT false;
 | 
				
			||||||
@ -55,6 +55,7 @@ model User {
 | 
				
			|||||||
  autoDropBolt11s           Boolean              @default(false)
 | 
					  autoDropBolt11s           Boolean              @default(false)
 | 
				
			||||||
  hideFromTopUsers          Boolean              @default(false)
 | 
					  hideFromTopUsers          Boolean              @default(false)
 | 
				
			||||||
  turboTipping              Boolean              @default(false)
 | 
					  turboTipping              Boolean              @default(false)
 | 
				
			||||||
 | 
					  zapUndos                  Boolean              @default(false)
 | 
				
			||||||
  imgproxyOnly              Boolean              @default(false)
 | 
					  imgproxyOnly              Boolean              @default(false)
 | 
				
			||||||
  hideWalletBalance         Boolean              @default(false)
 | 
					  hideWalletBalance         Boolean              @default(false)
 | 
				
			||||||
  referrerId                Int?
 | 
					  referrerId                Int?
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user