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