index.tsx raw

   1  import { Button } from '@/components/ui/button'
   2  import { Input } from '@/components/ui/input'
   3  import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
   4  import { createProfileDraftEvent } from '@/lib/draft-event'
   5  import { isEmail } from '@/lib/utils'
   6  import { useNostr } from '@/providers/NostrProvider'
   7  import { useZap } from '@/providers/ZapProvider'
   8  import { connectNWC, WebLNProviders } from '@getalby/bitcoin-connect'
   9  import { Check, CheckCircle2, Copy, ExternalLink, Loader2 } from 'lucide-react'
  10  import { forwardRef, useEffect, useState } from 'react'
  11  import { useTranslation } from 'react-i18next'
  12  import { toast } from 'sonner'
  13  
  14  const RIZFUL_URL = 'https://rizful.com'
  15  const RIZFUL_SIGNUP_URL = `${RIZFUL_URL}/create-account`
  16  const RIZFUL_GET_TOKEN_URL = `${RIZFUL_URL}/nostr_onboarding_auth_token/get_token`
  17  const RIZFUL_TOKEN_EXCHANGE_URL = `${RIZFUL_URL}/nostr_onboarding_auth_token/post_for_secrets`
  18  
  19  const RizfulPage = forwardRef(({ index }: { index?: number }, ref) => {
  20    const { t } = useTranslation()
  21    const { pubkey, profile, profileEvent, publish, updateProfileEvent } = useNostr()
  22    const { provider } = useZap()
  23    const [token, setToken] = useState('')
  24    const [connecting, setConnecting] = useState(false)
  25    const [connected, setConnected] = useState(false)
  26    const [copiedLightningAddress, setCopiedLightningAddress] = useState(false)
  27    const [lightningAddress, setLightningAddress] = useState('')
  28  
  29    useEffect(() => {
  30      if (provider instanceof WebLNProviders.NostrWebLNProvider) {
  31        const lud16 = provider.client.lud16
  32        const domain = lud16?.split('@')[1]
  33        if (domain !== 'rizful.com') return
  34  
  35        if (lud16) {
  36          setConnected(true)
  37          setLightningAddress(lud16)
  38        }
  39      }
  40    }, [provider])
  41  
  42    const updateUserProfile = async (address: string) => {
  43      try {
  44        // If the profile already has a lightning address, do nothing
  45        if (profile?.lightningAddress) {
  46          return
  47        }
  48  
  49        const profileContent = profileEvent ? JSON.parse(profileEvent.content) : {}
  50        if (isEmail(address)) {
  51          profileContent.lud16 = address
  52        } else if (address.startsWith('lnurl')) {
  53          profileContent.lud06 = address
  54        } else {
  55          throw new Error(t('Invalid Lightning Address'))
  56        }
  57  
  58        if (!profileContent.nip05) {
  59          profileContent.nip05 = address
  60        }
  61  
  62        const profileDraftEvent = createProfileDraftEvent(
  63          JSON.stringify(profileContent),
  64          profileEvent?.tags
  65        )
  66        const newProfileEvent = await publish(profileDraftEvent)
  67        await updateProfileEvent(newProfileEvent)
  68      } catch (e: unknown) {
  69        toast.error(e instanceof Error ? e.message : String(e))
  70      }
  71    }
  72  
  73    const connectRizful = async () => {
  74      setConnecting(true)
  75      try {
  76        const r = await fetch(RIZFUL_TOKEN_EXCHANGE_URL, {
  77          method: 'POST',
  78          headers: { 'Content-Type': 'application/json' },
  79          credentials: 'omit',
  80          body: JSON.stringify({
  81            secret_code: token.trim(),
  82            nostr_public_key: pubkey
  83          })
  84        })
  85  
  86        if (!r.ok) {
  87          const errorText = await r.text()
  88          throw new Error(errorText || 'Exchange failed')
  89        }
  90  
  91        const j = (await r.json()) as {
  92          nwc_uri?: string
  93          lightning_address?: string
  94        }
  95  
  96        if (j.nwc_uri) {
  97          connectNWC(j.nwc_uri)
  98        }
  99        if (j.lightning_address) {
 100          updateUserProfile(j.lightning_address)
 101        }
 102      } catch (e: unknown) {
 103        toast.error(e instanceof Error ? e.message : String(e))
 104      } finally {
 105        setTimeout(() => setConnecting(false), 5000)
 106      }
 107    }
 108  
 109    if (connected) {
 110      return (
 111        <SecondaryPageLayout ref={ref} index={index} title={t('Rizful Vault')}>
 112          <div className="px-4 pt-3 space-y-6 flex flex-col items-center">
 113            <CheckCircle2 className="size-40 fill-green-400 text-background" />
 114            <div className="font-semibold text-2xl">{t('Rizful Vault connected!')}</div>
 115            <div className="text-center text-sm text-muted-foreground">
 116              {t('You can now use your Rizful Vault to zap your favorite notes and creators.')}
 117            </div>
 118            {lightningAddress && (
 119              <div className="flex flex-col items-center gap-2">
 120                <div>{t('Your Lightning Address')}:</div>
 121                <div
 122                  className="font-semibold text-lg rounded-lg px-4 py-1 flex justify-center items-center gap-2 cursor-pointer hover:bg-accent/80"
 123                  onClick={() => {
 124                    navigator.clipboard.writeText(lightningAddress)
 125                    setCopiedLightningAddress(true)
 126                    setTimeout(() => setCopiedLightningAddress(false), 2000)
 127                  }}
 128                >
 129                  {lightningAddress}{' '}
 130                  {copiedLightningAddress ? (
 131                    <Check className="size-4" />
 132                  ) : (
 133                    <Copy className="size-4" />
 134                  )}
 135                </div>
 136              </div>
 137            )}
 138          </div>
 139        </SecondaryPageLayout>
 140      )
 141    }
 142  
 143    return (
 144      <SecondaryPageLayout ref={ref} index={index} title={t('Rizful Vault')}>
 145        <div className="px-4 pt-3 space-y-6">
 146          <div className="space-y-2">
 147            <div className="font-semibold">1. {t('New to Rizful?')}</div>
 148            <Button
 149              className="bg-lime-500 hover:bg-lime-500/90 w-64"
 150              onClick={() => window.open(RIZFUL_SIGNUP_URL, '_blank')}
 151            >
 152              {t('Sign up for Rizful')} <ExternalLink />
 153            </Button>
 154            <div className="text-sm text-muted-foreground">
 155              {t('If you already have a Rizful account, you can skip this step.')}
 156            </div>
 157          </div>
 158  
 159          <div className="space-y-2">
 160            <div className="font-semibold">2. {t('Get your one-time code')}</div>
 161            <Button
 162              className="bg-orange-500 hover:bg-orange-500/90 w-64"
 163              onClick={() => openPopup(RIZFUL_GET_TOKEN_URL, 'rizful_codes')}
 164            >
 165              {t('Get code')}
 166              <ExternalLink />
 167            </Button>
 168          </div>
 169  
 170          <div className="space-y-2">
 171            <div className="font-semibold">3. {t('Connect to your Rizful Vault')}</div>
 172            <Input
 173              placeholder={t('Paste your one-time code here')}
 174              value={token}
 175              onChange={(e) => {
 176                setToken(e.target.value.trim())
 177              }}
 178            />
 179            <Button
 180              className="bg-sky-500 hover:bg-sky-500/90 w-64"
 181              disabled={!token || connecting}
 182              onClick={() => connectRizful()}
 183            >
 184              {connecting && <Loader2 className="animate-spin" />}
 185              {t('Connect')}
 186            </Button>
 187          </div>
 188        </div>
 189      </SecondaryPageLayout>
 190    )
 191  })
 192  RizfulPage.displayName = 'RizfulPage'
 193  export default RizfulPage
 194  
 195  function openPopup(url: string, name: string, width = 520, height = 700) {
 196    const left = Math.max((window.screenX || 0) + (window.innerWidth - width) / 2, 0)
 197    const top = Math.max((window.screenY || 0) + (window.innerHeight - height) / 2, 0)
 198  
 199    return window.open(
 200      url,
 201      name,
 202      `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,menubar=no,toolbar=no,location=no,status=no`
 203    )
 204  }
 205