Add threshold for zap undos
This commit is contained in:
		
							parent
							
								
									42d7a31584
								
							
						
					
					
						commit
						c2aef34ba2
					
				@ -92,7 +92,7 @@ export default gql`
 | 
				
			|||||||
    nsfwMode: Boolean!
 | 
					    nsfwMode: Boolean!
 | 
				
			||||||
    tipDefault: Int!
 | 
					    tipDefault: Int!
 | 
				
			||||||
    turboTipping: Boolean!
 | 
					    turboTipping: Boolean!
 | 
				
			||||||
    zapUndos: Boolean!
 | 
					    zapUndos: Int
 | 
				
			||||||
    wildWestMode: Boolean!
 | 
					    wildWestMode: Boolean!
 | 
				
			||||||
    withdrawMaxFeeDefault: Int!
 | 
					    withdrawMaxFeeDefault: Int!
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -157,7 +157,7 @@ export default gql`
 | 
				
			|||||||
    nsfwMode: Boolean!
 | 
					    nsfwMode: Boolean!
 | 
				
			||||||
    tipDefault: Int!
 | 
					    tipDefault: Int!
 | 
				
			||||||
    turboTipping: Boolean!
 | 
					    turboTipping: Boolean!
 | 
				
			||||||
    zapUndos: Boolean!
 | 
					    zapUndos: Int
 | 
				
			||||||
    wildWestMode: Boolean!
 | 
					    wildWestMode: Boolean!
 | 
				
			||||||
    withdrawMaxFeeDefault: Int!
 | 
					    withdrawMaxFeeDefault: Int!
 | 
				
			||||||
    autoWithdrawThreshold: Int
 | 
					    autoWithdrawThreshold: Int
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import Dropdown from 'react-bootstrap/Dropdown'
 | 
					import Dropdown from 'react-bootstrap/Dropdown'
 | 
				
			||||||
import { useShowModal } from './modal'
 | 
					import { useShowModal } from './modal'
 | 
				
			||||||
import { useToast } from './toast'
 | 
					import { useToast } from './toast'
 | 
				
			||||||
import ItemAct from './item-act'
 | 
					import ItemAct, { zapUndosThresholdReached } from './item-act'
 | 
				
			||||||
import AccordianItem from './accordian-item'
 | 
					import AccordianItem from './accordian-item'
 | 
				
			||||||
import Flag from '@/svgs/flag-fill.svg'
 | 
					import Flag from '@/svgs/flag-fill.svg'
 | 
				
			||||||
import { useMemo } from 'react'
 | 
					import { useMemo } from 'react'
 | 
				
			||||||
@ -32,12 +32,11 @@ function DownZapper ({ id, As, children }) {
 | 
				
			|||||||
        try {
 | 
					        try {
 | 
				
			||||||
          showModal(onClose =>
 | 
					          showModal(onClose =>
 | 
				
			||||||
            <ItemAct
 | 
					            <ItemAct
 | 
				
			||||||
              onClose={() => {
 | 
					              onClose={(amount) => {
 | 
				
			||||||
                onClose()
 | 
					                onClose()
 | 
				
			||||||
                // undo prompt was toasted before closing modal if zap undos are enabled
 | 
					                // undo prompt was toasted before closing modal if zap undos are enabled
 | 
				
			||||||
                // so an additional success toast would be confusing
 | 
					                // so an additional success toast would be confusing
 | 
				
			||||||
                const zapUndosEnabled = me && me?.privates?.zapUndos
 | 
					                if (!zapUndosThresholdReached(me, amount)) toaster.success('item downzapped')
 | 
				
			||||||
                if (!zapUndosEnabled) toaster.success('item downzapped')
 | 
					 | 
				
			||||||
              }} itemId={id} down
 | 
					              }} itemId={id} down
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <AccordianItem
 | 
					              <AccordianItem
 | 
				
			||||||
 | 
				
			|||||||
@ -41,6 +41,12 @@ const addCustomTip = (amount) => {
 | 
				
			|||||||
  window.localStorage.setItem('custom-tips', JSON.stringify(customTips))
 | 
					  window.localStorage.setItem('custom-tips', JSON.stringify(customTips))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const zapUndosThresholdReached = (me, amount) => {
 | 
				
			||||||
 | 
					  if (!me) return false
 | 
				
			||||||
 | 
					  const enabled = me.privates.zapUndos !== null
 | 
				
			||||||
 | 
					  return enabled ? amount >= me.privates.zapUndos : false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function ItemAct ({ onClose, itemId, down, children }) {
 | 
					export default function ItemAct ({ onClose, itemId, down, children }) {
 | 
				
			||||||
  const inputRef = useRef(null)
 | 
					  const inputRef = useRef(null)
 | 
				
			||||||
  const me = useMe()
 | 
					  const me = useMe()
 | 
				
			||||||
@ -73,9 +79,9 @@ export default function ItemAct ({ onClose, itemId, down, children }) {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
    // only strike when zap undos not enabled
 | 
					    // only strike when zap undos not enabled
 | 
				
			||||||
    // due to optimistic UX on zap undos
 | 
					    // due to optimistic UX on zap undos
 | 
				
			||||||
    if (!me || !me.privates.zapUndos) await strike()
 | 
					    if (!zapUndosThresholdReached(me, Number(amount))) await strike()
 | 
				
			||||||
    addCustomTip(Number(amount))
 | 
					    addCustomTip(Number(amount))
 | 
				
			||||||
    if (!keepOpen) onClose()
 | 
					    if (!keepOpen) onClose(Number(amount))
 | 
				
			||||||
  }, [me, act, down, itemId, strike])
 | 
					  }, [me, act, down, itemId, strike])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onSubmitWithUndos = withToastFlow(toaster)(
 | 
					  const onSubmitWithUndos = withToastFlow(toaster)(
 | 
				
			||||||
@ -123,7 +129,7 @@ export default function ItemAct ({ onClose, itemId, down, children }) {
 | 
				
			|||||||
            return onSubmit(values, { flowId, ...args, update: null })
 | 
					            return onSubmit(values, { flowId, ...args, update: null })
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          await strike()
 | 
					          await strike()
 | 
				
			||||||
          onClose()
 | 
					          onClose(sats)
 | 
				
			||||||
          return new Promise((resolve, reject) => {
 | 
					          return new Promise((resolve, reject) => {
 | 
				
			||||||
            undoUpdate = update()
 | 
					            undoUpdate = update()
 | 
				
			||||||
            setTimeout(() => {
 | 
					            setTimeout(() => {
 | 
				
			||||||
@ -156,7 +162,12 @@ export default function ItemAct ({ onClose, itemId, down, children }) {
 | 
				
			|||||||
      }}
 | 
					      }}
 | 
				
			||||||
      schema={amountSchema}
 | 
					      schema={amountSchema}
 | 
				
			||||||
      invoiceable
 | 
					      invoiceable
 | 
				
			||||||
      onSubmit={me?.privates?.zapUndos ? onSubmitWithUndos : onSubmit}
 | 
					      onSubmit={(values, ...args) => {
 | 
				
			||||||
 | 
					        if (zapUndosThresholdReached(me, values.amount)) {
 | 
				
			||||||
 | 
					          return onSubmitWithUndos(values, ...args)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return onSubmit(values, ...args)
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <Input
 | 
					      <Input
 | 
				
			||||||
        label='amount'
 | 
					        label='amount'
 | 
				
			||||||
@ -376,16 +387,17 @@ export function useZap () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // add current sats to next tip since idempotent zaps use desired total zap not difference
 | 
					    // add current sats to next tip since idempotent zaps use desired total zap not difference
 | 
				
			||||||
    const sats = meSats + nextTip(meSats, { ...me?.privates })
 | 
					    const sats = meSats + nextTip(meSats, { ...me?.privates })
 | 
				
			||||||
 | 
					    const amount = sats - meSats
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const variables = { id: item.id, sats, act: 'TIP', amount: sats - meSats }
 | 
					    const variables = { id: item.id, sats, act: 'TIP', amount }
 | 
				
			||||||
    const insufficientFunds = me?.privates.sats < (sats - meSats)
 | 
					    const insufficientFunds = me?.privates.sats < amount
 | 
				
			||||||
    const optimisticResponse = { act: { path: item.path, ...variables } }
 | 
					    const optimisticResponse = { act: { path: item.path, ...variables } }
 | 
				
			||||||
    const flowId = (+new Date()).toString(16)
 | 
					    const flowId = (+new Date()).toString(16)
 | 
				
			||||||
    const zapArgs = { variables, optimisticResponse: insufficientFunds ? null : optimisticResponse, update, flowId }
 | 
					    const zapArgs = { variables, optimisticResponse: insufficientFunds ? null : optimisticResponse, update, flowId }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      if (insufficientFunds) throw new Error('insufficient funds')
 | 
					      if (insufficientFunds) throw new Error('insufficient funds')
 | 
				
			||||||
      strike()
 | 
					      strike()
 | 
				
			||||||
      if (me?.privates?.zapUndos) {
 | 
					      if (zapUndosThresholdReached(me, amount)) {
 | 
				
			||||||
        await zapWithUndos(zapArgs)
 | 
					        await zapWithUndos(zapArgs)
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        await zap(zapArgs)
 | 
					        await zap(zapArgs)
 | 
				
			||||||
 | 
				
			|||||||
@ -530,7 +530,8 @@ export const settingsSchema = object({
 | 
				
			|||||||
  hideWalletBalance: boolean(),
 | 
					  hideWalletBalance: boolean(),
 | 
				
			||||||
  diagnostics: boolean(),
 | 
					  diagnostics: boolean(),
 | 
				
			||||||
  noReferralLinks: boolean(),
 | 
					  noReferralLinks: boolean(),
 | 
				
			||||||
  hideIsContributor: boolean()
 | 
					  hideIsContributor: boolean(),
 | 
				
			||||||
 | 
					  zapUndos: intValidator.required('required').min(0, 'must be greater or equal to 0')
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again'
 | 
					const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again'
 | 
				
			||||||
 | 
				
			|||||||
@ -28,6 +28,7 @@ import { useMe } from '@/components/me'
 | 
				
			|||||||
import { INVOICE_RETENTION_DAYS } from '@/lib/constants'
 | 
					import { INVOICE_RETENTION_DAYS } from '@/lib/constants'
 | 
				
			||||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
 | 
					import { OverlayTrigger, Tooltip } from 'react-bootstrap'
 | 
				
			||||||
import DeleteIcon from '@/svgs/delete-bin-line.svg'
 | 
					import DeleteIcon from '@/svgs/delete-bin-line.svg'
 | 
				
			||||||
 | 
					import { useField } from 'formik'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true })
 | 
					export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -65,7 +66,8 @@ export default function Settings ({ ssrData }) {
 | 
				
			|||||||
          initial={{
 | 
					          initial={{
 | 
				
			||||||
            tipDefault: settings?.tipDefault || 21,
 | 
					            tipDefault: settings?.tipDefault || 21,
 | 
				
			||||||
            turboTipping: settings?.turboTipping,
 | 
					            turboTipping: settings?.turboTipping,
 | 
				
			||||||
            zapUndos: settings?.zapUndos,
 | 
					            zapUndos: settings?.zapUndos || settings?.tipDefault || 0,
 | 
				
			||||||
 | 
					            zapUndosEnabled: settings?.zapUndos !== null || false,
 | 
				
			||||||
            fiatCurrency: settings?.fiatCurrency || 'USD',
 | 
					            fiatCurrency: settings?.fiatCurrency || 'USD',
 | 
				
			||||||
            withdrawMaxFeeDefault: settings?.withdrawMaxFeeDefault,
 | 
					            withdrawMaxFeeDefault: settings?.withdrawMaxFeeDefault,
 | 
				
			||||||
            noteItemSats: settings?.noteItemSats,
 | 
					            noteItemSats: settings?.noteItemSats,
 | 
				
			||||||
@ -98,7 +100,7 @@ export default function Settings ({ ssrData }) {
 | 
				
			|||||||
            noReferralLinks: settings?.noReferralLinks
 | 
					            noReferralLinks: settings?.noReferralLinks
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
          schema={settingsSchema}
 | 
					          schema={settingsSchema}
 | 
				
			||||||
          onSubmit={async ({ tipDefault, withdrawMaxFeeDefault, nostrPubkey, nostrRelays, ...values }) => {
 | 
					          onSubmit={async ({ tipDefault, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => {
 | 
				
			||||||
            if (nostrPubkey.length === 0) {
 | 
					            if (nostrPubkey.length === 0) {
 | 
				
			||||||
              nostrPubkey = null
 | 
					              nostrPubkey = null
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
@ -116,6 +118,7 @@ export default function Settings ({ ssrData }) {
 | 
				
			|||||||
                  settings: {
 | 
					                  settings: {
 | 
				
			||||||
                    tipDefault: Number(tipDefault),
 | 
					                    tipDefault: Number(tipDefault),
 | 
				
			||||||
                    withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
 | 
					                    withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
 | 
				
			||||||
 | 
					                    zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
 | 
				
			||||||
                    nostrPubkey,
 | 
					                    nostrPubkey,
 | 
				
			||||||
                    nostrRelays: nostrRelaysFiltered,
 | 
					                    nostrRelays: nostrRelaysFiltered,
 | 
				
			||||||
                    ...values
 | 
					                    ...values
 | 
				
			||||||
@ -171,25 +174,7 @@ export default function Settings ({ ssrData }) {
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    groupClassName='mb-0'
 | 
					                    groupClassName='mb-0'
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                  <Checkbox
 | 
					                  <ZapUndoField />
 | 
				
			||||||
                    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>
 | 
					 | 
				
			||||||
                            <li>
 | 
					 | 
				
			||||||
                              The button is only shown for zaps from the custodial wallet
 | 
					 | 
				
			||||||
                            </li>
 | 
					 | 
				
			||||||
                            <li>
 | 
					 | 
				
			||||||
                              Use a budget or manual approval with attached wallets
 | 
					 | 
				
			||||||
                            </li>
 | 
					 | 
				
			||||||
                          </ul>
 | 
					 | 
				
			||||||
                        </Info>
 | 
					 | 
				
			||||||
                      </div>
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                </>
 | 
					                </>
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
@ -920,3 +905,36 @@ I estimate that I will call the GraphQL API this many times (rough estimate is f
 | 
				
			|||||||
    </>
 | 
					    </>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ZapUndoField = () => {
 | 
				
			||||||
 | 
					  const [checkboxField] = useField({ name: 'zapUndosEnabled' })
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className='d-flex flex-row align-items-center'>
 | 
				
			||||||
 | 
					      <Input
 | 
				
			||||||
 | 
					        name='zapUndos'
 | 
				
			||||||
 | 
					        disabled={!checkboxField.value}
 | 
				
			||||||
 | 
					        label={
 | 
				
			||||||
 | 
					          <Checkbox
 | 
				
			||||||
 | 
					            name='zapUndosEnabled'
 | 
				
			||||||
 | 
					            groupClassName='mb-0'
 | 
				
			||||||
 | 
					            label={
 | 
				
			||||||
 | 
					              <div className='d-flex align-items-center'>
 | 
				
			||||||
 | 
					                zap undos
 | 
				
			||||||
 | 
					                <Info>
 | 
				
			||||||
 | 
					                  <ul className='fw-bold'>
 | 
				
			||||||
 | 
					                    <li>An undo button is shown after every zap that exceeds or is equal to the threshold</li>
 | 
				
			||||||
 | 
					                    <li>The button is shown for 5 seconds</li>
 | 
				
			||||||
 | 
					                    <li>The button is only shown for zaps from the custodial wallet</li>
 | 
				
			||||||
 | 
					                    <li>Use a budget or manual approval with attached wallets</li>
 | 
				
			||||||
 | 
					                  </ul>
 | 
				
			||||||
 | 
					                </Info>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
 | 
				
			||||||
 | 
					        hint={<small className='text-muted'>threshold at which undo button is shown</small>}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					ALTER TABLE "users" ADD COLUMN "zapUndosTmp" INTEGER;
 | 
				
			||||||
 | 
					UPDATE "users" SET "zapUndosTmp" = CASE WHEN "zapUndos" = false THEN NULL ELSE 0::INTEGER END;
 | 
				
			||||||
 | 
					ALTER TABLE "users" DROP COLUMN "zapUndos";
 | 
				
			||||||
 | 
					ALTER TABLE "users" RENAME COLUMN "zapUndosTmp" TO "zapUndos";
 | 
				
			||||||
@ -56,7 +56,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)
 | 
					  zapUndos                  Int?
 | 
				
			||||||
  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