11"use client" ;
22
33import { useQueryState } from "nuqs" ;
4- import { useMemo , useTransition } from "react" ;
4+ import { useMemo , useState , useTransition } from "react" ;
55import { toast } from "sonner" ;
66import { getChainInfraCheckoutURL } from "@/actions/billing" ;
7+ import { reportChainInfraRpcOmissionAgreed } from "@/analytics/report" ;
78import { Badge } from "@/components/ui/badge" ;
89import { Button } from "@/components/ui/button" ;
10+ import {
11+ Dialog ,
12+ DialogContent ,
13+ DialogDescription ,
14+ DialogFooter ,
15+ DialogHeader ,
16+ DialogTitle ,
17+ } from "@/components/ui/dialog" ;
918import { Switch } from "@/components/ui/switch" ;
1019import { InsightIcon } from "@/icons/InsightIcon" ;
1120import { RPCIcon } from "@/icons/RPCIcon" ;
@@ -44,7 +53,7 @@ const SERVICE_CONFIG = {
4453 icon : "RPCIcon" ,
4554 label : "RPC" ,
4655 monthlyPrice : 1500 ,
47- required : true ,
56+ required : false ,
4857 sku : "chain:infra:rpc" as const ,
4958 } ,
5059} satisfies Record <
@@ -80,19 +89,26 @@ export function DeployInfrastructureForm(props: {
8089 searchParams . addons . withOptions ( { history : "replace" , startTransition } ) ,
8190 ) ;
8291
92+ const [ rpcParam , setRpcParam ] = useQueryState (
93+ "rpc" ,
94+ searchParams . rpc . withOptions ( { history : "replace" , startTransition } ) ,
95+ ) ;
96+
8397 const addons = useMemo ( ( ) => {
8498 return addonsStr ? addonsStr . split ( "," ) . filter ( Boolean ) : [ ] ;
8599 } , [ addonsStr ] ) ;
86100
87101 const includeInsight = addons . includes ( "insight" ) ;
88102 const includeAA = addons . includes ( "aa" ) ;
103+ const includeRPC = rpcParam === "on" ;
89104
90105 const selectedOrder = useMemo ( ( ) => {
91- const arr : ( keyof typeof SERVICE_CONFIG ) [ ] = [ "rpc" ] ;
106+ const arr : ( keyof typeof SERVICE_CONFIG ) [ ] = [ ] ;
107+ if ( includeRPC ) arr . push ( "rpc" ) ;
92108 if ( includeInsight ) arr . push ( "insight" ) ;
93109 if ( includeAA ) arr . push ( "accountAbstraction" ) ;
94110 return arr ;
95- } , [ includeInsight , includeAA ] ) ;
111+ } , [ includeInsight , includeAA , includeRPC ] ) ;
96112
97113 // NEW: count selected services and prepare bundle discount hint
98114 const selectedCount = selectedOrder . length ;
@@ -112,9 +128,9 @@ export function DeployInfrastructureForm(props: {
112128 return {
113129 accountAbstraction : includeAA ,
114130 insight : includeInsight ,
115- rpc : true ,
131+ rpc : includeRPC ,
116132 } as const ;
117- } , [ includeInsight , includeAA ] ) ;
133+ } , [ includeInsight , includeAA , includeRPC ] ) ;
118134
119135 const pricePerService = useMemo ( ( ) => {
120136 const isAnnual = frequency === "annual" ;
@@ -172,10 +188,13 @@ export function DeployInfrastructureForm(props: {
172188
173189 const chainId = props . chain . chainId ;
174190
175- const checkout = ( ) => {
191+ const [ showRpcWarning , setShowRpcWarning ] = useState ( false ) ;
192+
193+ const proceedToCheckout = ( ) => {
176194 startTransition ( async ( ) => {
177195 try {
178- const skus : ChainInfraSKU [ ] = [ SERVICE_CONFIG . rpc . sku ] ;
196+ const skus : ChainInfraSKU [ ] = [ ] ;
197+ if ( includeRPC ) skus . push ( SERVICE_CONFIG . rpc . sku ) ;
179198 if ( includeInsight ) skus . push ( SERVICE_CONFIG . insight . sku ) ;
180199 if ( includeAA ) skus . push ( SERVICE_CONFIG . accountAbstraction . sku ) ;
181200
@@ -186,11 +205,9 @@ export function DeployInfrastructureForm(props: {
186205 teamSlug : props . teamSlug ,
187206 } ) ;
188207
189- // If the action returns, it means redirect did not happen and we have an error
190208 if ( res . status === "error" ) {
191209 toast . error ( res . error ) ;
192210 } else if ( res . status === "success" ) {
193- // replace the current page with the checkout page (which will then redirect back to us)
194211 window . location . href = res . data ;
195212 }
196213 } catch ( err ) {
@@ -202,6 +219,15 @@ export function DeployInfrastructureForm(props: {
202219 } ) ;
203220 } ;
204221
222+ const checkout = ( ) => {
223+ const hasAddons = includeInsight || includeAA ;
224+ if ( ! includeRPC && hasAddons ) {
225+ setShowRpcWarning ( true ) ;
226+ return ;
227+ }
228+ proceedToCheckout ( ) ;
229+ } ;
230+
205231 const periodLabel = frequency === "annual" ? "/yr" : "/mo" ;
206232 const isAnnual = frequency === "annual" ;
207233
@@ -211,21 +237,22 @@ export function DeployInfrastructureForm(props: {
211237 < div className = "flex flex-col gap-4" >
212238 < h3 className = "text-lg font-semibold" > Select Services</ h3 >
213239
214- { /* Required service */ }
240+ { /* RPC (now optional) */ }
215241 < div className = "flex flex-col gap-2 mb-6" >
216242 < ServiceCard
217243 description = { SERVICE_CONFIG . rpc . description }
218- disabled
219244 icon = { SERVICE_CONFIG . rpc . icon }
220245 label = { SERVICE_CONFIG . rpc . label }
221- onToggle = { ( ) => { } }
246+ onToggle = { ( ) => {
247+ const newVal = ! includeRPC ;
248+ setRpcParam ( newVal ? "on" : "off" ) ;
249+ } }
222250 originalPrice = {
223251 isAnnual ? SERVICE_CONFIG . rpc . monthlyPrice * 12 : undefined
224252 }
225253 periodLabel = { periodLabel }
226254 price = { pricePerService . rpc }
227- required
228- selected
255+ selected = { includeRPC }
229256 />
230257 </ div >
231258
@@ -350,7 +377,11 @@ export function DeployInfrastructureForm(props: {
350377
351378 < Button
352379 className = "w-full"
353- disabled = { isTransitionPending || ! props . isOwner }
380+ disabled = {
381+ isTransitionPending ||
382+ ! props . isOwner ||
383+ selectedOrder . length === 0
384+ }
354385 onClick = { checkout }
355386 >
356387 Proceed to Checkout
@@ -362,6 +393,54 @@ export function DeployInfrastructureForm(props: {
362393 ) }
363394 </ div >
364395 </ div >
396+ { /* RPC Omission Warning Modal */ }
397+ < Dialog open = { showRpcWarning } onOpenChange = { setShowRpcWarning } >
398+ < DialogContent >
399+ < DialogHeader >
400+ < DialogTitle > Proceed without RPC (not recommended)</ DialogTitle >
401+ < DialogDescription className = "space-y-3" >
402+ < p >
403+ RPC powers core functionality used by < strong > Insight</ strong > { " " }
404+ and < strong > Account Abstraction</ strong > .
405+ </ p >
406+ < div className = "space-y-1" >
407+ < p > Without RPC, you may experience:</ p >
408+ < ul className = "list-disc pl-5 space-y-1" >
409+ < li > Delayed or missing data in Insight</ li >
410+ < li >
411+ Transaction failures or degraded reliability for Account
412+ Abstraction
413+ </ li >
414+ < li > Limited or unsupported features across both services</ li >
415+ </ ul >
416+ </ div >
417+ < p >
418+ thirdweb < strong > cannot guarantee</ strong > that Insight or
419+ Account Abstraction will work as expected without RPC. To ensure
420+ reliability, keep RPC enabled.
421+ </ p >
422+ < p > If you still want to continue, confirm below.</ p >
423+ </ DialogDescription >
424+ </ DialogHeader >
425+ < DialogFooter className = "gap-2" >
426+ < Button
427+ onClick = { ( ) => {
428+ reportChainInfraRpcOmissionAgreed ( {
429+ chainId,
430+ frequency : frequency === "annual" ? "annual" : "monthly" ,
431+ includeInsight,
432+ includeAccountAbstraction : includeAA ,
433+ } ) ;
434+ setShowRpcWarning ( false ) ;
435+ proceedToCheckout ( ) ;
436+ } }
437+ variant = "destructive"
438+ >
439+ I understand — proceed without RPC
440+ </ Button >
441+ </ DialogFooter >
442+ </ DialogContent >
443+ </ Dialog >
365444 </ div >
366445 ) ;
367446}
0 commit comments