Prompt to attach receive wallet on post (#2059)
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									d3b81e4346
								
							
						
					
					
						commit
						719cb2d507
					
				@ -898,6 +898,14 @@ export default {
 | 
			
		||||
 | 
			
		||||
      await models.user.update({ where: { id: me.id }, data: { hideWelcomeBanner: true } })
 | 
			
		||||
      return true
 | 
			
		||||
    },
 | 
			
		||||
    hideWalletRecvPrompt: async (parent, data, { me, models }) => {
 | 
			
		||||
      if (!me) {
 | 
			
		||||
        throw new GqlAuthenticationError()
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await models.user.update({ where: { id: me.id }, data: { hideWalletRecvPrompt: true } })
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,7 @@ export default gql`
 | 
			
		||||
    unlinkAuth(authType: String!): AuthMethods!
 | 
			
		||||
    linkUnverifiedEmail(email: String!): Boolean
 | 
			
		||||
    hideWelcomeBanner: Boolean
 | 
			
		||||
    hideWalletRecvPrompt: Boolean
 | 
			
		||||
    subscribeUserPosts(id: ID): User
 | 
			
		||||
    subscribeUserComments(id: ID): User
 | 
			
		||||
    toggleMute(id: ID): User
 | 
			
		||||
@ -141,6 +142,7 @@ export default gql`
 | 
			
		||||
    """
 | 
			
		||||
    lastCheckedJobs: String
 | 
			
		||||
    hideWelcomeBanner: Boolean!
 | 
			
		||||
    hideWalletRecvPrompt: Boolean!
 | 
			
		||||
    tipPopover: Boolean!
 | 
			
		||||
    upvotePopover: Boolean!
 | 
			
		||||
    hasInvites: Boolean!
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import { RETRY_PAID_ACTION } from '@/fragments/paidAction'
 | 
			
		||||
import gql from 'graphql-tag'
 | 
			
		||||
import { USER_ID } from '@/lib/constants'
 | 
			
		||||
import { useMe } from './me'
 | 
			
		||||
import { useWalletRecvPrompt, WalletPromptClosed } from '@/wallets/prompt'
 | 
			
		||||
 | 
			
		||||
// this is intented to be compatible with upsert item mutations
 | 
			
		||||
// so that it can be reused for all post types and comments and we don't have
 | 
			
		||||
@ -22,9 +23,17 @@ export default function useItemSubmit (mutation,
 | 
			
		||||
  const crossposter = useCrossposter()
 | 
			
		||||
  const [upsertItem] = usePaidMutation(mutation)
 | 
			
		||||
  const { me } = useMe()
 | 
			
		||||
  const walletPrompt = useWalletRecvPrompt()
 | 
			
		||||
 | 
			
		||||
  return useCallback(
 | 
			
		||||
    async ({ boost, crosspost, title, options, bounty, status, ...values }, { resetForm }) => {
 | 
			
		||||
      try {
 | 
			
		||||
        await walletPrompt()
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        if (err instanceof WalletPromptClosed) return
 | 
			
		||||
        throw err
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (options) {
 | 
			
		||||
        // remove existing poll options since else they will be appended as duplicates
 | 
			
		||||
        options = options.slice(item?.poll?.options?.length || 0).filter(o => o.trim().length > 0)
 | 
			
		||||
@ -93,7 +102,7 @@ export default function useItemSubmit (mutation,
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }, [me, upsertItem, router, crossposter, item, sub, onSuccessfulSubmit,
 | 
			
		||||
      navigateOnSubmit, extraValues, paidMutationOptions]
 | 
			
		||||
      navigateOnSubmit, extraValues, paidMutationOptions, walletPrompt]
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,7 @@ ${STREAK_FIELDS}
 | 
			
		||||
      hideFromTopUsers
 | 
			
		||||
      hideWalletBalance
 | 
			
		||||
      hideWelcomeBanner
 | 
			
		||||
      hideWalletRecvPrompt
 | 
			
		||||
      imgproxyOnly
 | 
			
		||||
      showImagesAndVideos
 | 
			
		||||
      nostrCrossposting
 | 
			
		||||
@ -167,6 +168,11 @@ export const USER_SUGGESTIONS = gql`
 | 
			
		||||
    }
 | 
			
		||||
  }`
 | 
			
		||||
 | 
			
		||||
export const HIDE_WALLET_RECV_PROMPT_MUTATION = gql`
 | 
			
		||||
  mutation hideWalletRecvPrompt {
 | 
			
		||||
    hideWalletRecvPrompt
 | 
			
		||||
  }`
 | 
			
		||||
 | 
			
		||||
export const USER_SEARCH = gql`
 | 
			
		||||
${STREAK_FIELDS}
 | 
			
		||||
  query searchUsers($q: String!, $limit: Limit, $similarity: Float) {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,2 @@
 | 
			
		||||
-- AlterTable
 | 
			
		||||
ALTER TABLE "users" ADD COLUMN     "hideWalletRecvPrompt" BOOLEAN NOT NULL DEFAULT false;
 | 
			
		||||
@ -116,6 +116,7 @@ model User {
 | 
			
		||||
  followers                 UserSubscription[]   @relation("follower")
 | 
			
		||||
  followees                 UserSubscription[]   @relation("followee")
 | 
			
		||||
  hideWelcomeBanner         Boolean              @default(false)
 | 
			
		||||
  hideWalletRecvPrompt      Boolean              @default(false)
 | 
			
		||||
  diagnostics               Boolean              @default(false)
 | 
			
		||||
  hideIsContributor         Boolean              @default(false)
 | 
			
		||||
  lnAddr                    String?
 | 
			
		||||
 | 
			
		||||
@ -125,4 +125,28 @@
 | 
			
		||||
  color: var(--theme-toolbarHover) !important;
 | 
			
		||||
  background-color: var(--theme-toolbarHover) !important;
 | 
			
		||||
  border: 1px solid var(--theme-toolbarActive);
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.separator {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  color: var(--theme-grey);
 | 
			
		||||
  margin-top: 0.5rem;
 | 
			
		||||
  margin-bottom: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.separator::before,
 | 
			
		||||
.separator::after {
 | 
			
		||||
  content: '';
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  border-bottom: 1px solid var(--theme-grey);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.separator:not(:empty)::before {
 | 
			
		||||
  margin-right: .25em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.separator:not(:empty)::after {
 | 
			
		||||
  margin-left: .25em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										151
									
								
								wallets/prompt.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								wallets/prompt.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,151 @@
 | 
			
		||||
import { useCallback } from 'react'
 | 
			
		||||
import { boolean, object } from 'yup'
 | 
			
		||||
import { Button } from 'react-bootstrap'
 | 
			
		||||
import { Form, ClientInput, SubmitButton, Checkbox } from '@/components/form'
 | 
			
		||||
import { useMe } from '@/components/me'
 | 
			
		||||
import { useShowModal } from '@/components/modal'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import { useWallet } from '@/wallets/index'
 | 
			
		||||
import { useWalletConfigurator } from '@/wallets/config'
 | 
			
		||||
import styles from '@/styles/wallet.module.css'
 | 
			
		||||
import { externalLightningAddressValidator } from '@/lib/validate'
 | 
			
		||||
import { autowithdrawInitial } from '@/components/autowithdraw-shared'
 | 
			
		||||
import { useMutation } from '@apollo/client'
 | 
			
		||||
import { HIDE_WALLET_RECV_PROMPT_MUTATION } from '@/fragments/users'
 | 
			
		||||
import { useToast } from '@/components/toast'
 | 
			
		||||
 | 
			
		||||
export class WalletPromptClosed extends Error {
 | 
			
		||||
  constructor () {
 | 
			
		||||
    super('wallet prompt closed')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useWalletRecvPrompt () {
 | 
			
		||||
  const { me } = useMe()
 | 
			
		||||
  const showModal = useShowModal()
 | 
			
		||||
  const toaster = useToast()
 | 
			
		||||
 | 
			
		||||
  const onAttach = useCallback(({ onClose, resolve }) =>
 | 
			
		||||
    () => {
 | 
			
		||||
      toaster.success('lightning address saved', { persistOnNavigate: true })
 | 
			
		||||
      resolve()
 | 
			
		||||
      onClose()
 | 
			
		||||
    }, [toaster])
 | 
			
		||||
 | 
			
		||||
  const onSkip = useCallback(({ onClose, resolve }) =>
 | 
			
		||||
    () => {
 | 
			
		||||
      resolve()
 | 
			
		||||
      onClose()
 | 
			
		||||
    }, [])
 | 
			
		||||
 | 
			
		||||
  return useCallback((e) => {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      // TODO: check if user told us to not show again
 | 
			
		||||
      if (!me || me.optional?.hasRecvWallet || me.privates?.hideWalletRecvPrompt) return resolve()
 | 
			
		||||
 | 
			
		||||
      showModal(onClose => {
 | 
			
		||||
        return (
 | 
			
		||||
          <>
 | 
			
		||||
            <Header />
 | 
			
		||||
            <LnAddrForm onAttach={onAttach({ onClose, resolve })} className='mt-3' />
 | 
			
		||||
            <div className={styles.separator}>or</div>
 | 
			
		||||
            <WalletLink />
 | 
			
		||||
            <div className={styles.separator}>or</div>
 | 
			
		||||
            <SkipForm onSkip={onSkip({ onClose, resolve })} />
 | 
			
		||||
            <Footer />
 | 
			
		||||
          </>
 | 
			
		||||
        )
 | 
			
		||||
      }, { keepOpen: true, onClose: () => reject(new WalletPromptClosed()) })
 | 
			
		||||
    })
 | 
			
		||||
  }, [!!me, me?.optional?.hasRecvWallet, me?.privates?.hideWalletRecvPrompt, showModal, onAttach, onSkip])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Header = () => (
 | 
			
		||||
  <div className='fw-bold text-center mb-3'>
 | 
			
		||||
    You need to attach a<br />
 | 
			
		||||
    <span className='fw-bold text-primary fs-1' style={{ fontFamily: 'lightning' }}>lightning wallet</span>
 | 
			
		||||
    <br />
 | 
			
		||||
    to receive sats
 | 
			
		||||
  </div>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const LnAddrForm = ({ onAttach }) => {
 | 
			
		||||
  const { me } = useMe()
 | 
			
		||||
  const wallet = useWallet('lightning-address')
 | 
			
		||||
  const { save } = useWalletConfigurator(wallet)
 | 
			
		||||
 | 
			
		||||
  const schema = object({ lnAddr: externalLightningAddressValidator.required('required') })
 | 
			
		||||
 | 
			
		||||
  const onSubmit = useCallback(async ({ lnAddr }) => {
 | 
			
		||||
    await save({
 | 
			
		||||
      ...autowithdrawInitial({ me }),
 | 
			
		||||
      priority: 0,
 | 
			
		||||
      enabled: true,
 | 
			
		||||
      address: lnAddr
 | 
			
		||||
    }, true)
 | 
			
		||||
    onAttach()
 | 
			
		||||
  }, [save])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <span>You can enter a <span className='fw-bold'>lightning address</span>:</span>
 | 
			
		||||
      <Form
 | 
			
		||||
        schema={schema}
 | 
			
		||||
        onSubmit={onSubmit}
 | 
			
		||||
        initial={{ lnAddr: '' }}
 | 
			
		||||
      >
 | 
			
		||||
        <ClientInput
 | 
			
		||||
          name='lnAddr'
 | 
			
		||||
          groupClassName='mt-1 mb-3'
 | 
			
		||||
          append={<SubmitButton variant='primary' size='sm'>save</SubmitButton>}
 | 
			
		||||
        />
 | 
			
		||||
      </Form>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const WalletLink = () => <span>visit <Link href='/wallets'>wallets</Link> to set up a different wallet</span>
 | 
			
		||||
 | 
			
		||||
const SkipForm = ({ onSkip }) => {
 | 
			
		||||
  const { me } = useMe()
 | 
			
		||||
  const [hideWalletRecvPrompt] = useMutation(HIDE_WALLET_RECV_PROMPT_MUTATION, {
 | 
			
		||||
    update (cache) {
 | 
			
		||||
      cache.modify({
 | 
			
		||||
        id: `User:${me.id}`,
 | 
			
		||||
        fields: {
 | 
			
		||||
          hideWalletRecvPrompt () {
 | 
			
		||||
            return true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const onSubmit = useCallback(({ dontShowAgain }) => {
 | 
			
		||||
    if (dontShowAgain) {
 | 
			
		||||
      // XXX this is not so important to wait for it to complete or make sure it succeeds
 | 
			
		||||
      hideWalletRecvPrompt().catch(err => console.error('hideWalletRecvPrompt error:', err))
 | 
			
		||||
    }
 | 
			
		||||
    onSkip()
 | 
			
		||||
  }, [hideWalletRecvPrompt])
 | 
			
		||||
 | 
			
		||||
  const schema = object({ dontShowAgain: boolean().required() })
 | 
			
		||||
  return (
 | 
			
		||||
    <Form
 | 
			
		||||
      initial={{ dontShowAgain: false }}
 | 
			
		||||
      className='d-flex justify-content-between align-items-center mt-3'
 | 
			
		||||
      onSubmit={onSubmit}
 | 
			
		||||
      schema={schema}
 | 
			
		||||
    >
 | 
			
		||||
      <Checkbox label="don't show again" name='dontShowAgain' groupClassName='mb-0' />
 | 
			
		||||
      <Button type='submit' variant='secondary' size='sm'>skip</Button>
 | 
			
		||||
    </Form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Footer = () => (
 | 
			
		||||
  <div className='mt-3 text-center text-muted small'>
 | 
			
		||||
    Stacker News is non-custodial. If you don't attach a wallet, you will receive credits when zapped.
 | 
			
		||||
    See the <Link href='/faq#wallets'>FAQ</Link> for the details.
 | 
			
		||||
  </div>
 | 
			
		||||
)
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user