@@ -117,26 +117,20 @@ export function PurchaseSection({ onRequireEnterprise }: PurchaseSectionProps) {
117117 const sessionId = params . get ( "session_id" ) ;
118118 if ( ! sessionId || ! allowManage ) return ;
119119
120- let cancelled = false ;
120+ const controller = new AbortController ( ) ;
121121 ( async ( ) => {
122122 try {
123123 const status = await subscriptionStore . verifyCheckoutSession ( sessionId ) ;
124- if ( status !== "complete" || cancelled ) return ;
124+ if ( status !== "complete" || controller . signal . aborted ) return ;
125125
126- // Poll without updating the store to avoid UI flash.
127- // On first check, if subscription is already non-FREE (webhook arrived before page load),
128- // we update the store and skip polling immediately.
129126 setPendingPayment ( true ) ;
130- for ( let i = 0 ; i < 30 ; i ++ ) {
131- if ( cancelled ) break ;
132- const sub = await subscriptionStore . fetchSubscription ( false ) ;
133- if ( sub && sub . plan !== PlanType . FREE ) {
134- subscriptionStore . setSubscription ( sub ) ;
135- break ;
136- }
137- await new Promise ( ( r ) => setTimeout ( r , 2000 ) ) ;
127+ await subscriptionStore . pollSubscriptionUntil (
128+ ( sub ) => sub . plan !== PlanType . FREE ,
129+ { signal : controller . signal }
130+ ) ;
131+ if ( ! controller . signal . aborted ) {
132+ setPendingPayment ( false ) ;
138133 }
139- setPendingPayment ( false ) ;
140134 } catch ( e ) {
141135 console . error ( "failed to verify checkout session" , e ) ;
142136 }
@@ -145,7 +139,7 @@ export function PurchaseSection({ onRequireEnterprise }: PurchaseSectionProps) {
145139 } ) ( ) ;
146140
147141 return ( ) => {
148- cancelled = true ;
142+ controller . abort ( ) ;
149143 } ;
150144 } , [ ] ) ;
151145
@@ -306,16 +300,11 @@ export function PurchaseSection({ onRequireEnterprise }: PurchaseSectionProps) {
306300 if ( paymentUrl ) {
307301 window . location . href = paymentUrl ;
308302 } else {
309- // Direct update — poll without updating store to avoid UI flashing .
303+ // Direct update — wait for the webhook-driven reconciliation .
310304 setPendingPayment ( true ) ;
311- for ( let i = 0 ; i < 30 ; i ++ ) {
312- const sub = await subscriptionStore . fetchSubscription ( false ) ;
313- if ( sub && sub . plan !== PlanType . FREE && sub . seats === seats ) {
314- subscriptionStore . setSubscription ( sub ) ;
315- break ;
316- }
317- await new Promise ( ( r ) => setTimeout ( r , 2000 ) ) ;
318- }
305+ await subscriptionStore . pollSubscriptionUntil (
306+ ( sub ) => sub . plan !== PlanType . FREE && sub . seats === seats
307+ ) ;
319308 setPendingPayment ( false ) ;
320309 }
321310 } catch ( e ) {
@@ -330,6 +319,11 @@ export function PurchaseSection({ onRequireEnterprise }: PurchaseSectionProps) {
330319 setCanceling ( true ) ;
331320 try {
332321 await subscriptionStore . cancelPurchase ( ) ;
322+ // Wait for the Stripe webhook to reconcile before releasing the UI,
323+ // so the cached subscription/license reflects the new state.
324+ await subscriptionStore . pollSubscriptionUntil (
325+ ( sub ) => sub . plan === PlanType . FREE
326+ ) ;
333327 pushNotification ( {
334328 module : "bytebase" ,
335329 style : "SUCCESS" ,
0 commit comments