Skip to content

Commit a4f0071

Browse files
authored
feat: load routes pending and docs-faq (#83)
* add loading state for configs, and pending vpn instance * add docs-faqs route Signed-off-by: JaeBrian <[email protected]> --------- Signed-off-by: JaeBrian <[email protected]>
1 parent 227f1ae commit a4f0071

File tree

8 files changed

+380
-29
lines changed

8 files changed

+380
-29
lines changed

src/api/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export { useSignup } from './useSignup'
33
export { useClientList } from './useClientList'
44
export { useClientAvailable } from './useClientAvailable'
55
export { useClientProfile } from './useClientProfile'
6-
export { useMultipleClientAvailable } from './useMultipleClientAvailable'
6+
export { useMultipleClientAvailable } from './useMultipleClientAvailable'
7+
export { useClientPolling } from './useClientPolling'

src/api/hooks/useClientPolling.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { useState, useEffect, useRef } from 'react'
2+
import { useQueryClient } from '@tanstack/react-query'
3+
import { checkClientAvailable } from '../client'
4+
5+
interface PollingState {
6+
isPolling: boolean
7+
clientId: string | null
8+
attempts: number
9+
maxAttempts: number
10+
}
11+
12+
export function useClientPolling() {
13+
const [pollingState, setPollingState] = useState<PollingState>({
14+
isPolling: false,
15+
clientId: null,
16+
attempts: 0,
17+
maxAttempts: 20 // 20 minutes max (20 * 60 seconds)
18+
})
19+
20+
const queryClient = useQueryClient()
21+
const intervalRef = useRef<NodeJS.Timeout | null>(null)
22+
23+
const startPolling = (clientId: string) => {
24+
setPollingState({
25+
isPolling: true,
26+
clientId,
27+
attempts: 0,
28+
maxAttempts: 20
29+
})
30+
}
31+
32+
const stopPolling = () => {
33+
setPollingState(prev => ({
34+
...prev,
35+
isPolling: false,
36+
clientId: null,
37+
attempts: 0
38+
}))
39+
40+
if (intervalRef.current) {
41+
clearInterval(intervalRef.current)
42+
intervalRef.current = null
43+
}
44+
}
45+
46+
useEffect(() => {
47+
if (!pollingState.isPolling || !pollingState.clientId) {
48+
return
49+
}
50+
51+
const pollClient = async () => {
52+
try {
53+
const response = await checkClientAvailable({ id: pollingState.clientId! })
54+
55+
// Check if client is available (assuming success means available)
56+
if (response && !response.msg) {
57+
// Client is available, stop polling and refresh client list
58+
stopPolling()
59+
60+
// Invalidate and refetch client list to get the new instance
61+
await queryClient.invalidateQueries({ queryKey: ['clientList'] })
62+
63+
return
64+
}
65+
66+
// Increment attempts
67+
setPollingState(prev => ({
68+
...prev,
69+
attempts: prev.attempts + 1
70+
}))
71+
72+
// Stop polling if max attempts reached
73+
if (pollingState.attempts >= pollingState.maxAttempts) {
74+
stopPolling()
75+
}
76+
77+
} catch (error) {
78+
console.error('Error polling client availability:', error)
79+
80+
// Increment attempts even on error
81+
setPollingState(prev => ({
82+
...prev,
83+
attempts: prev.attempts + 1
84+
}))
85+
86+
// Stop polling if max attempts reached
87+
if (pollingState.attempts >= pollingState.maxAttempts) {
88+
stopPolling()
89+
}
90+
}
91+
}
92+
93+
// Poll immediately, then every 60 seconds
94+
pollClient()
95+
96+
intervalRef.current = setInterval(pollClient, 60000) // 60 seconds
97+
98+
return () => {
99+
if (intervalRef.current) {
100+
clearInterval(intervalRef.current)
101+
intervalRef.current = null
102+
}
103+
}
104+
}, [pollingState.isPolling, pollingState.clientId, pollingState.attempts, pollingState.maxAttempts, queryClient])
105+
106+
return {
107+
isPolling: pollingState.isPolling,
108+
clientId: pollingState.clientId,
109+
attempts: pollingState.attempts,
110+
maxAttempts: pollingState.maxAttempts,
111+
startPolling,
112+
stopPolling
113+
}
114+
}

src/components/HeroSection.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ const HeroSection = ({ onGetStarted }: HeroSectionProps) => {
5252
>
5353
How It Works
5454
</button>
55+
<button
56+
onClick={() => navigate('/docs-faqs')}
57+
className="flex py-3 sm:py-4 px-6 sm:px-10 justify-center items-center gap-2.5 rounded-full border border-white/20 backdrop-blur-sm text-white font-medium text-sm sm:text-base hover:bg-white/10 transition-colors cursor-pointer"
58+
>
59+
FAQs
60+
</button>
5561
</div>
5662
</div>
5763
</div>

src/components/VpnInstance.tsx

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
interface VpnInstanceProps {
22
region: string
33
duration: string
4-
status: "Active" | "Expired"
4+
status: "Active" | "Expired" | "Pending"
55
expires: string
66
onDelete?: () => void
77
onAction?: () => void
@@ -12,7 +12,9 @@ const VpnInstance = ({ region, duration, status, expires, onAction }: VpnInstanc
1212
<div className={`flex p-4 flex-col justify-center items-start gap-3 w-full rounded-md backdrop-blur-xs ${
1313
status === "Active"
1414
? "bg-[linear-gradient(180deg,rgba(148,0,255,0.60)_0%,rgba(104,0,178,0.60)_100%)]"
15-
: "bg-[rgba(255,255,255,0.20)]"
15+
: status === "Pending"
16+
? "bg-[rgba(128,128,128,0.30)]"
17+
: "bg-[rgba(255,255,255,0.20)]"
1618
}`}>
1719
<div className="flex flex-col items-start gap-1 w-full">
1820
<div className="flex justify-between items-start w-full gap-2">
@@ -22,20 +24,34 @@ const VpnInstance = ({ region, duration, status, expires, onAction }: VpnInstanc
2224
<div className="flex justify-between items-start w-full">
2325
<div className="flex items-center gap-2">
2426
<p className="text-sm md:text-base">Status: {status}</p>
25-
<span className={`w-2 h-2 rounded-full ${status === "Active" ? "bg-[#86EA64]" : "bg-red-500"}`}></span>
27+
<span className={`w-2 h-2 rounded-full ${
28+
status === "Active"
29+
? "bg-[#86EA64]"
30+
: status === "Pending"
31+
? "bg-yellow-500 animate-pulse"
32+
: "bg-red-500"
33+
}`}></span>
2634
</div>
2735
<p className="text-sm md:text-base">Expires: {expires}</p>
2836
</div>
2937
</div>
3038
<div className="flex justify-end items-center w-full">
31-
<button
32-
className="flex items-center justify-center gap-3 rounded-md py-1.5 px-3.5 backdrop-blur-xs box-shadow-sm cursor-pointer bg-white text-black"
33-
onClick={onAction}
34-
>
35-
<p className="font-light text-black text-sm">
36-
{status === "Active" ? "Get Config" : "Renew Access"}
37-
</p>
38-
</button>
39+
{status !== "Pending" && (
40+
<button
41+
className="flex items-center justify-center gap-3 rounded-md py-1.5 px-3.5 backdrop-blur-xs box-shadow-sm cursor-pointer bg-white text-black"
42+
onClick={onAction}
43+
>
44+
<p className="font-light text-black text-sm">
45+
{status === "Active" ? "Get Config" : "Renew Access"}
46+
</p>
47+
</button>
48+
)}
49+
{status === "Pending" && (
50+
<div className="flex items-center justify-center gap-2 rounded-md py-1.5 px-3.5 backdrop-blur-xs bg-gray-400 text-gray-600">
51+
<div className="w-4 h-4 border-2 border-gray-600 border-t-transparent rounded-full animate-spin"></div>
52+
<p className="font-light text-sm">Setting up...</p>
53+
</div>
54+
)}
3955
</div>
4056
</div>
4157
)

src/pages/Account.tsx

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useEffect, useMemo } from "react"
22
import { useWalletStore } from "../stores/walletStore"
3-
import { useRefData, useSignup, useClientList, useClientProfile } from "../api/hooks"
3+
import { useRefData, useSignup, useClientList, useClientProfile, useClientPolling } from "../api/hooks"
44
import VpnInstance from "../components/VpnInstance"
55
import WalletModal from "../components/WalletModal"
66
import { showSuccess, showError } from "../utils/toast"
@@ -20,6 +20,12 @@ const Account = () => {
2020
} = useWalletStore()
2121
const [selectedDuration, setSelectedDuration] = useState<number>(0)
2222
const [selectedRegion, setSelectedRegion] = useState<string>("")
23+
const [pendingClients, setPendingClients] = useState<Array<{
24+
id: string
25+
region: string
26+
duration: string
27+
purchaseTime: Date
28+
}>>([])
2329

2430
const tooltipSteps: TooltipStep[] = [
2531
{
@@ -64,8 +70,20 @@ const Account = () => {
6470
console.log('Transaction built successfully:', data)
6571

6672
try {
67-
const txHash = await signAndSubmitTransaction(data.txCbor)
68-
showSuccess(`VPN purchase successful! Transaction: ${txHash}`)
73+
await signAndSubmitTransaction(data.txCbor)
74+
showSuccess(`VPN purchase successful! Setting up your instance...`)
75+
76+
// Add pending client to the list
77+
const pendingClient = {
78+
id: data.clientId,
79+
region: selectedRegion,
80+
duration: selectedOption ? formatDuration(selectedOption.value) : 'Unknown',
81+
purchaseTime: new Date()
82+
}
83+
setPendingClients(prev => [...prev, pendingClient])
84+
85+
// Start polling for this client
86+
startPolling(data.clientId)
6987

7088
} catch (error) {
7189
console.error('Transaction error details:', error)
@@ -144,6 +162,16 @@ const Account = () => {
144162
}
145163
}, [refData?.regions, selectedRegion])
146164

165+
// Remove pending clients when they become available in the client list
166+
useEffect(() => {
167+
if (dedupedClientList && pendingClients.length > 0) {
168+
const availableClientIds = new Set(dedupedClientList.map(client => client.id))
169+
setPendingClients(prev =>
170+
prev.filter(pending => !availableClientIds.has(pending.id))
171+
)
172+
}
173+
}, [dedupedClientList, pendingClients.length])
174+
147175
const selectedOption = durationOptions.find((option: { value: number }) => option.value === selectedDuration)
148176

149177
const handlePurchase = () => {
@@ -177,6 +205,7 @@ const Account = () => {
177205
}
178206

179207
const clientProfileMutation = useClientProfile()
208+
const { startPolling } = useClientPolling()
180209

181210
const handleAction = async (instanceId: string, action: string) => {
182211
if (action === 'Get Config') {
@@ -221,9 +250,7 @@ const Account = () => {
221250
}
222251

223252
const vpnInstances = useMemo(() => {
224-
if (!dedupedClientList) return []
225-
226-
return dedupedClientList
253+
const activeInstances = dedupedClientList ? dedupedClientList
227254
.map((client: ClientInfo) => {
228255
const isActive = new Date(client.expiration) > new Date()
229256

@@ -245,8 +272,32 @@ const Account = () => {
245272
} else {
246273
return b.expirationDate.getTime() - a.expirationDate.getTime()
247274
}
248-
})
249-
}, [dedupedClientList])
275+
}) : []
276+
277+
// Add pending instances
278+
const pendingInstances = pendingClients.map(pending => ({
279+
id: pending.id,
280+
region: pending.region,
281+
duration: pending.duration,
282+
status: 'Pending' as const,
283+
expires: 'Setting up...',
284+
expirationDate: new Date(pending.purchaseTime.getTime() + 24 * 60 * 60 * 1000) // Placeholder
285+
}))
286+
287+
// Combine and sort: Pending first, then Active, then Expired
288+
return [...pendingInstances, ...activeInstances].sort((a, b) => {
289+
if (a.status === 'Pending' && b.status !== 'Pending') return -1
290+
if (a.status !== 'Pending' && b.status === 'Pending') return 1
291+
if (a.status === 'Active' && b.status === 'Expired') return -1
292+
if (a.status === 'Expired' && b.status === 'Active') return 1
293+
294+
if (a.status === 'Active') {
295+
return b.expirationDate.getTime() - a.expirationDate.getTime()
296+
} else {
297+
return b.expirationDate.getTime() - a.expirationDate.getTime()
298+
}
299+
})
300+
}, [dedupedClientList, pendingClients])
250301

251302
return (
252303
<TooltipGuide
@@ -258,9 +309,21 @@ const Account = () => {
258309
<div className="min-h-screen min-w-screen flex flex-col items-center justify-start bg-[linear-gradient(180deg,#1C246E_0%,#040617_12.5%)] pt-16">
259310
<div className="flex flex-col items-center justify-center pt-8 gap-6 md:pt-12 md:gap-8 z-20 text-white w-full max-w-none md:max-w-[80rem] px-4 md:px-8">
260311
<LoadingOverlay
261-
isVisible={signupMutation.isPending}
262-
messageTop={signupMutation.isPending ? 'Awaiting Transaction Confirmation' : ''}
263-
messageBottom="Processing Purchase"
312+
isVisible={signupMutation.isPending || clientProfileMutation.isPending}
313+
messageTop={
314+
signupMutation.isPending
315+
? 'Awaiting Transaction Confirmation'
316+
: clientProfileMutation.isPending
317+
? 'Preparing VPN Configuration'
318+
: ''
319+
}
320+
messageBottom={
321+
signupMutation.isPending
322+
? 'Processing Purchase'
323+
: clientProfileMutation.isPending
324+
? 'Downloading Config File'
325+
: ''
326+
}
264327
/>
265328

266329
{/* VPN PURCHASE SECTION */}

0 commit comments

Comments
 (0)