@@ -27,6 +27,9 @@ import { Heading2, Subheading } from "./components/typography/headings";
27
27
import { useCurrentOrg , useOrganizations } from "./data/organizations/orgs-query" ;
28
28
import { PaymentContext } from "./payment-context" ;
29
29
30
+ // DEFAULTS
31
+ const DEFAULT_USAGE_LIMIT = 1000 ;
32
+
30
33
/**
31
34
* Keys of known page params
32
35
*/
@@ -45,7 +48,7 @@ type PageParams = {
45
48
setupIntentId ?: string ;
46
49
} ;
47
50
type PageState = {
48
- phase : "call-to-action" | "trigger-signup" | "wait-for-signup" | " cleanup" | "done" ;
51
+ phase : "call-to-action" | "trigger-signup" | "cleanup" | "done" ;
49
52
attributionId ?: string ;
50
53
setupIntentId ?: string ;
51
54
old ?: {
@@ -68,23 +71,23 @@ function SwitchToPAYG() {
68
71
69
72
const currentOrg = useCurrentOrg ( ) . data ;
70
73
const orgs = useOrganizations ( ) . data ;
71
- const [ errorMessage , setErrorMessage ] = useState < string | undefined > ( ) ;
74
+ const [ errorMessage , setErrorMessage ] = useState < string | undefined > ( undefined ) ;
72
75
const [ selectedOrganization , setSelectedOrganization ] = useState < Team | undefined > ( undefined ) ;
73
76
const [ showBillingSetupModal , setShowBillingSetupModal ] = useState < boolean > ( false ) ;
74
77
const [ pendingStripeSubscription , setPendingStripeSubscription ] = useState < boolean > ( false ) ;
75
- const [ droppedConfetti , setDroppedConfetti ] = useState < boolean > ( false ) ;
76
78
const { dropConfetti } = useConfetti ( ) ;
77
79
78
80
useEffect ( ( ) => {
79
81
setSelectedOrganization ( currentOrg ) ;
80
82
} , [ currentOrg , setSelectedOrganization ] ) ;
81
83
82
84
useEffect ( ( ) => {
83
- const { phase, attributionId, setupIntentId } = pageState ;
85
+ const phase = pageState . phase ;
86
+ const attributionId = pageState . attributionId ;
87
+ const setupIntentId = pageState . setupIntentId ;
84
88
if ( phase !== "trigger-signup" ) {
85
89
return ;
86
90
}
87
- console . log ( "phase: " + phase ) ;
88
91
89
92
// We're back from the Stripe modal: (safely) trigger the signup
90
93
if ( ! attributionId ) {
@@ -97,53 +100,34 @@ function SwitchToPAYG() {
97
100
return ;
98
101
}
99
102
100
- let cancelled = false ;
101
- ( async ( ) => {
102
- // At this point we're coming back from the Stripe modal, and have the intent to setup a new subscription.
103
- // Technically, we have to guard against:
104
- // - reloads
105
- // - unmounts (for whatever reason)
106
-
107
- // Do we already have a subscription (co-owner, me in another tab, reload, etc.)?
108
- let subscriptionId = await getGitpodService ( ) . server . findStripeSubscriptionId ( attributionId ) ;
109
- if ( subscriptionId ) {
110
- console . log ( `${ attributionId } already has a subscription! Moving to cleanup` ) ;
111
- // We're happy!
112
- if ( ! cancelled ) {
113
- setPageState ( ( s ) => ( { ...s , phase : "cleanup" } ) ) ;
114
- }
115
- return ;
116
- }
117
-
118
- // Now we want to signup for sure
119
- setPendingStripeSubscription ( true ) ;
120
- try {
121
- if ( cancelled ) return ;
122
-
123
- const limit = 1000 ;
124
- console . log ( "SUBSCRIBE TO STRIPE" ) ;
125
- await getGitpodService ( ) . server . subscribeToStripe ( attributionId , setupIntentId , limit ) ;
126
- // Here we go off the effect handler due to the await
127
- if ( ! cancelled ) {
128
- setPageState ( ( s ) => ( { ...s , phase : "wait-for-signup" } ) ) ;
129
- }
130
- } catch ( error ) {
131
- if ( cancelled ) return ;
103
+ console . log ( `trigger-signup: ${ JSON . stringify ( { phase, attributionId, setupIntentId } ) } ` ) ;
132
104
133
- setErrorMessage ( `Could not subscribe to Stripe. ${ error ?. message || String ( error ) } ` ) ;
105
+ // do not await here, it might get called several times during rendering, but only first call has any effect.
106
+ setPendingStripeSubscription ( true ) ;
107
+ subscribeToStripe (
108
+ setupIntentId ,
109
+ attributionId ,
110
+ ( update ) => {
111
+ setPageState ( ( prev ) => ( { ...prev , ...update } ) ) ;
134
112
setPendingStripeSubscription ( false ) ;
135
- return ;
136
- }
137
- } ) ( ) . catch ( console . error ) ;
138
-
139
- return ( ) => {
140
- cancelled = true ;
141
- } ;
142
- } , [ pageState , setPageState ] ) ;
113
+ } ,
114
+ ( errorMessage ) => {
115
+ setErrorMessage ( errorMessage ) ;
116
+ setPendingStripeSubscription ( false ) ;
117
+ } ,
118
+ ) . catch ( console . error ) ;
119
+ } , [ pageState . attributionId , pageState . phase , pageState . setupIntentId , setPageState ] ) ;
143
120
144
121
useEffect ( ( ) => {
145
- const { phase, attributionId, old } = pageState ;
146
- const { setupIntentId, type, oldSubscriptionOrTeamId } = pageParams || { } ;
122
+ if ( ! pageParams ?. type ) {
123
+ return ;
124
+ }
125
+ const phase = pageState . phase ;
126
+ const attributionId = pageState . attributionId ;
127
+ const old = pageState . old ;
128
+ const type = pageParams . type ;
129
+ const oldSubscriptionOrTeamId = pageParams . oldSubscriptionOrTeamId ;
130
+
147
131
if ( phase === "trigger-signup" ) {
148
132
// Handled in separate effect
149
133
return ;
@@ -158,12 +142,28 @@ function SwitchToPAYG() {
158
142
return ;
159
143
}
160
144
161
- console . log ( "phase: " + phase ) ;
145
+ console . log (
146
+ `context: ${ JSON . stringify ( {
147
+ state : { phase, attributionId, old } ,
148
+ params : { type, oldSubscriptionOrTeamId } ,
149
+ oldSubscriptionOrTeamId,
150
+ type,
151
+ } ) } `,
152
+ ) ;
153
+
162
154
switch ( phase ) {
163
155
case "call-to-action" : {
164
156
// Check: Can we progress?
165
- if ( setupIntentId ) {
166
- setPageState ( ( s ) => ( { ...s , setupIntentId, phase : "trigger-signup" } ) ) ;
157
+ if ( pageParams . setupIntentId ) {
158
+ if ( pageState . setupIntentId === pageParams . setupIntentId ) {
159
+ // we've been here already
160
+ return ;
161
+ }
162
+ setPageState ( ( prev ) => ( {
163
+ ...prev ,
164
+ setupIntentId : pageParams . setupIntentId ,
165
+ phase : "trigger-signup" ,
166
+ } ) ) ;
167
167
return ;
168
168
}
169
169
@@ -190,7 +190,7 @@ function SwitchToPAYG() {
190
190
if ( Subscription . isCancelled ( sub , now ) || ! Subscription . isActive ( sub , now ) ) {
191
191
// We're happy!
192
192
if ( ! cancelled ) {
193
- setPageState ( ( s ) => ( { ...s , phase : "done" } ) ) ;
193
+ setPageState ( ( prev ) => ( { ...prev , phase : "done" } ) ) ;
194
194
}
195
195
return ;
196
196
}
@@ -215,7 +215,7 @@ function SwitchToPAYG() {
215
215
if ( TeamSubscription . isCancelled ( ts , now ) || ! TeamSubscription . isActive ( ts , now ) ) {
216
216
// We're happy!
217
217
if ( ! cancelled ) {
218
- setPageState ( ( s ) => ( { ...s , phase : "done" } ) ) ;
218
+ setPageState ( ( prev ) => ( { ...prev , phase : "done" } ) ) ;
219
219
}
220
220
return ;
221
221
}
@@ -245,7 +245,7 @@ function SwitchToPAYG() {
245
245
if ( TeamSubscription2 . isCancelled ( ts2 , now ) || ! TeamSubscription2 . isActive ( ts2 , now ) ) {
246
246
// We're happy!
247
247
if ( ! cancelled ) {
248
- setPageState ( ( s ) => ( { ...s , phase : "done" } ) ) ;
248
+ setPageState ( ( prev ) => ( { ...prev , phase : "done" } ) ) ;
249
249
}
250
250
return ;
251
251
}
@@ -259,55 +259,8 @@ function SwitchToPAYG() {
259
259
}
260
260
}
261
261
if ( ! cancelled && ! attributionId ) {
262
- setPageState ( ( s ) => {
263
- const attributionId = s . attributionId || derivedAttributionId ;
264
- return { ...s , attributionId, old } ;
265
- } ) ;
266
- }
267
- } ) ( ) . catch ( console . error ) ;
268
-
269
- return ( ) => {
270
- cancelled = true ;
271
- } ;
272
- }
273
-
274
- case "wait-for-signup" : {
275
- // Wait for the singup to be completed
276
- if ( ! attributionId ) {
277
- console . error ( "Signup, but attributionId not set!" ) ;
278
- return ;
279
- }
280
- setPendingStripeSubscription ( true ) ;
281
-
282
- let cancelled = false ;
283
- ( async ( ) => {
284
- // We need to poll for the subscription to appear
285
- let subscriptionId : string | undefined ;
286
- for ( let i = 1 ; i <= 10 ; i ++ ) {
287
- if ( cancelled ) {
288
- break ;
289
- }
290
-
291
- try {
292
- subscriptionId = await getGitpodService ( ) . server . findStripeSubscriptionId ( attributionId ) ;
293
- if ( subscriptionId ) {
294
- break ;
295
- }
296
- } catch ( error ) {
297
- console . error ( "Search for subscription failed." , error ) ;
298
- }
299
- await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ;
300
- }
301
- if ( cancelled ) {
302
- return ;
262
+ setPageState ( ( prev ) => ( { ...prev , old, attributionId : derivedAttributionId } ) ) ;
303
263
}
304
-
305
- setPendingStripeSubscription ( false ) ;
306
- if ( ! subscriptionId ) {
307
- setErrorMessage ( `Could not find the subscription.` ) ;
308
- return ;
309
- }
310
- setPageState ( ( s ) => ( { ...s , phase : "cleanup" } ) ) ;
311
264
} ) ( ) . catch ( console . error ) ;
312
265
313
266
return ( ) => {
@@ -377,29 +330,31 @@ function SwitchToPAYG() {
377
330
break ;
378
331
}
379
332
}
380
- setPageState ( ( s ) => ( { ...s , phase : "done" } ) ) ;
333
+ setPageState ( ( prev ) => ( { ...prev , phase : "done" } ) ) ;
381
334
return ;
382
335
}
383
336
384
337
case "done" :
385
- // Hooray and confetti!
386
- resetAllNotifications ( ) ;
387
- if ( ! droppedConfetti ) {
388
- setDroppedConfetti ( true ) ;
338
+ if ( ! confettiDropped ) {
339
+ confettiDropped = true ;
340
+
341
+ // Hooray and confetti!
342
+ resetAllNotifications ( ) ;
389
343
dropConfetti ( ) ;
390
344
}
391
345
return ;
392
346
}
393
347
} , [
394
- location . search ,
395
- pageParams ,
396
- pageState ,
397
- setPageState ,
398
- pendingStripeSubscription ,
399
- setPendingStripeSubscription ,
400
348
selectedOrganization ,
401
349
dropConfetti ,
402
- droppedConfetti ,
350
+ setPageState ,
351
+ pageParams ?. type ,
352
+ pageParams ?. oldSubscriptionOrTeamId ,
353
+ pageParams ?. setupIntentId ,
354
+ pageState . phase ,
355
+ pageState . attributionId ,
356
+ pageState . old ,
357
+ pageState . setupIntentId ,
403
358
] ) ;
404
359
405
360
const onUpgradePlan = useCallback ( async ( ) => {
@@ -427,8 +382,15 @@ function SwitchToPAYG() {
427
382
428
383
const attributionId = pageState . attributionId || "" ;
429
384
const parsed = AttributionId . parse ( attributionId ) ;
385
+ let billingLink = "/billing" ;
430
386
const orgId = parsed ?. kind === "team" ? parsed . teamId : undefined ;
431
- const billingLink = orgId ? `/billing?org=${ orgId } ` : "/user/billing" ;
387
+ if ( orgId ) {
388
+ billingLink = `/billing?org=${ orgId } ` ;
389
+ } else {
390
+ if ( ! user . additionalData ?. isMigratedToTeamOnlyAttribution ) {
391
+ billingLink = "/user/billing" ;
392
+ }
393
+ }
432
394
433
395
return (
434
396
< div className = "flex flex-col max-h-screen max-w-2xl mx-auto items-center w-full mt-24" >
@@ -631,7 +593,7 @@ function renderCard(props: {
631
593
>
632
594
{ props . headline }
633
595
</ p >
634
- < input className = "opacity-0" type = "radio" checked = { props . selected } />
596
+ < input className = "opacity-0" type = "radio" checked = { props . selected } readOnly = { true } />
635
597
</ div >
636
598
< div className = "pl-1 grid auto-rows-auto" >
637
599
< div
@@ -686,4 +648,57 @@ function parseSearchParams(search: string): PageParams | undefined {
686
648
}
687
649
}
688
650
651
+ let confettiDropped = false ;
652
+
653
+ let subscribeToStripe_called = false ;
654
+ async function subscribeToStripe (
655
+ setupIntentId : string ,
656
+ attributionId : string ,
657
+ updateState : ( u : Partial < PageState > ) => void ,
658
+ onError : ( e : string ) => void ,
659
+ ) {
660
+ if ( subscribeToStripe_called ) {
661
+ return ;
662
+ }
663
+ subscribeToStripe_called = true ;
664
+
665
+ // Do we already have a subscription (co-owner, me in another tab, reload, etc.)?
666
+ let subscriptionId = await getGitpodService ( ) . server . findStripeSubscriptionId ( attributionId ) ;
667
+ if ( subscriptionId ) {
668
+ console . log ( `${ attributionId } already has a subscription! Moving to cleanup` ) ;
669
+ // We're happy!
670
+ updateState ( { phase : "cleanup" } ) ;
671
+ return ;
672
+ }
673
+
674
+ // Now we want to signup for sure
675
+ try {
676
+ await getGitpodService ( ) . server . subscribeToStripe ( attributionId , setupIntentId , DEFAULT_USAGE_LIMIT ) ;
677
+
678
+ // We need to poll for the subscription to appear
679
+ let subscriptionId : string | undefined ;
680
+ for ( let i = 1 ; i <= 10 ; i ++ ) {
681
+ try {
682
+ subscriptionId = await getGitpodService ( ) . server . findStripeSubscriptionId ( attributionId ) ;
683
+ if ( subscriptionId ) {
684
+ break ;
685
+ }
686
+ } catch ( error ) {
687
+ console . error ( "Search for subscription failed." , error ) ;
688
+ }
689
+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ;
690
+ }
691
+
692
+ if ( ! subscriptionId ) {
693
+ onError ( `Could not find the subscription.` ) ;
694
+ return ;
695
+ }
696
+
697
+ updateState ( { phase : "cleanup" } ) ;
698
+ } catch ( error ) {
699
+ onError ( `Could not subscribe to Stripe. ${ error ?. message || String ( error ) } ` ) ;
700
+ return ;
701
+ }
702
+ }
703
+
689
704
export default SwitchToPAYG ;
0 commit comments