4
4
* See License-AGPL.txt in the project root for license information.
5
5
*/
6
6
7
- import { useState , useEffect , useContext } from "react" ;
7
+ import React , { useState , useEffect , useContext } from "react" ;
8
8
import { countries } from 'countries-list' ;
9
9
import { AccountStatement , Subscription , UserPaidSubscription , AssignedTeamSubscription } from "@gitpod/gitpod-protocol/lib/accounting-protocol" ;
10
10
import { PlanCoupon , GithubUpgradeURL } from "@gitpod/gitpod-protocol/lib/payment-protocol" ;
11
- import { Plans , Plan , Currency } from "@gitpod/gitpod-protocol/lib/plans" ;
11
+ import { Plans , Plan , Currency , PlanType } from "@gitpod/gitpod-protocol/lib/plans" ;
12
12
import { ChargebeeClient } from "../chargebee/chargebee-client" ;
13
13
import Modal from "../components/Modal" ;
14
14
import SelectableCard from "../components/SelectableCard" ;
@@ -22,6 +22,16 @@ import settingsMenu from "./settings-menu";
22
22
type PlanWithOriginalPrice = Plan & { originalPrice ?: number } ;
23
23
type PendingPlan = PlanWithOriginalPrice & { pendingSince : number } ;
24
24
25
+ type TeamClaimModal = {
26
+ errorText : string ;
27
+ mode : "error" ;
28
+ } | {
29
+ text : string ;
30
+ teamId : string ;
31
+ slotId : string ;
32
+ mode : "confirmation" ;
33
+ }
34
+
25
35
export default function ( ) {
26
36
const { user } = useContext ( UserContext ) ;
27
37
const { server } = getGitpodService ( ) ;
@@ -34,6 +44,8 @@ export default function () {
34
44
const [ gitHubUpgradeUrls , setGitHubUpgradeUrls ] = useState < GithubUpgradeURL [ ] > ( ) ;
35
45
const [ privateRepoTrialEndDate , setPrivateRepoTrialEndDate ] = useState < string > ( ) ;
36
46
47
+ const [ teamClaimModal , setTeamClaimModal ] = useState < TeamClaimModal | undefined > ( undefined ) ;
48
+
37
49
let pollAccountStatementTimeout : NodeJS . Timeout | undefined ;
38
50
39
51
useEffect ( ( ) => {
@@ -49,12 +61,46 @@ export default function () {
49
61
server . getAppliedCoupons ( ) . then ( v => ( ) => setAppliedCoupons ( v ) ) ,
50
62
server . getGithubUpgradeUrls ( ) . then ( v => ( ) => setGitHubUpgradeUrls ( v ) ) ,
51
63
server . getPrivateRepoTrialEndDate ( ) . then ( v => ( ) => setPrivateRepoTrialEndDate ( v ) ) ,
52
- ] ) . then ( setters => setters . forEach ( s => s ( ) ) ) ;
64
+ ] ) . then ( setters => setters . forEach ( s => s ( ) ) )
65
+ . then ( ( ) => {
66
+ handleTeamClaim ( ) ;
67
+ } ) ;
68
+
69
+
53
70
return function cleanup ( ) {
54
71
clearTimeout ( pollAccountStatementTimeout ! ) ;
55
72
}
56
73
} , [ ] ) ;
57
74
75
+ const handleTeamClaim = async ( ) => {
76
+ const teamId = new URL ( window . location . href ) . searchParams . get ( 'teamid' ) ;
77
+ if ( ! teamId ) {
78
+ return ;
79
+ }
80
+ const currentlyActiveSubscriptions = ( accountStatement ?. subscriptions || [ ] ) . filter ( s => Subscription . isActive ( s , new Date ( ) . toISOString ( ) ) ) ;
81
+ const assignedSubscriptions = currentlyActiveSubscriptions . filter ( s => AssignedTeamSubscription . is ( s ) ) ;
82
+ if ( assignedSubscriptions . some ( s => ! ! s . teamSubscriptionSlotId ) ) {
83
+ return ;
84
+ }
85
+
86
+ const freeSlot = await getGitpodService ( ) . server . tsGetUnassignedSlot ( teamId ) ;
87
+ if ( ! freeSlot ) {
88
+ setTeamClaimModal ( {
89
+ mode : "error" ,
90
+ errorText : "This invitation is no longer valid. Please contact the team owner." ,
91
+ } ) ;
92
+ return ;
93
+ }
94
+
95
+ setTeamClaimModal ( {
96
+ mode : "confirmation" ,
97
+ teamId,
98
+ slotId : freeSlot . id ,
99
+ text : "You are about to claim a seat in a team." ,
100
+ } )
101
+
102
+ } ;
103
+
58
104
console . log ( 'privateRepoTrialEndDate' , privateRepoTrialEndDate ) ;
59
105
60
106
const activeSubscriptions = ( accountStatement ?. subscriptions || [ ] ) . filter ( s => Subscription . isActive ( s , new Date ( ) . toISOString ( ) ) ) ;
@@ -64,8 +110,17 @@ export default function () {
64
110
const freePlan = freeSubscription && Plans . getById ( freeSubscription . planId ) || Plans . getFreePlan ( user ?. creationDate || new Date ( ) . toISOString ( ) ) ;
65
111
const paidSubscription = activeSubscriptions . find ( s => UserPaidSubscription . is ( s ) ) ;
66
112
const paidPlan = paidSubscription && Plans . getById ( paidSubscription . planId ) ;
113
+
67
114
const assignedTeamSubscriptions = activeSubscriptions . filter ( s => AssignedTeamSubscription . is ( s ) ) ;
68
115
console . log ( 'assignedTeamSubscriptions' , assignedTeamSubscriptions ) ;
116
+ const getAssignedTs = ( type : PlanType ) => assignedTeamSubscriptions . find ( s => {
117
+ const p = Plans . getById ( s . planId ) ;
118
+ return ! ! p && p . type === type
119
+ } ) ;
120
+ const assignedUnleashedTs = getAssignedTs ( 'professional' ) ;
121
+ const assignedProfessionalTs = getAssignedTs ( 'professional-new' ) ;
122
+ const assignedStudentUnleashedTs = getAssignedTs ( 'student' ) ;
123
+ const assignedTs = assignedUnleashedTs || assignedProfessionalTs || assignedStudentUnleashedTs ;
69
124
70
125
const [ pendingUpgradePlan , setPendingUpgradePlan ] = useState < PendingPlan | undefined > ( getLocalStorageObject ( 'pendingUpgradePlan' ) ) ;
71
126
const setPendingUpgrade = ( to : PendingPlan ) => {
@@ -95,7 +150,7 @@ export default function () {
95
150
}
96
151
97
152
// Optimistically select a new paid plan even if the transaction is still in progress (i.e. waiting for Chargebee callback)
98
- const currentPlan = pendingUpgradePlan || paidPlan || freePlan ;
153
+ const currentPlan = pendingUpgradePlan || paidPlan || Plans . getById ( assignedTs ?. planId ) || freePlan ;
99
154
100
155
// If the user has a paid plan with a different currency, force that currency.
101
156
if ( currency !== currentPlan . currency && ! Plans . isFreePlan ( currentPlan . chargebeeId ) ) {
@@ -235,7 +290,7 @@ export default function () {
235
290
< p className = "truncate" title = "30 min Timeout" > ✓ 30 min Timeout</ p >
236
291
</ > ;
237
292
if ( currentPlan . chargebeeId === freePlan . chargebeeId ) {
238
- planCards . push ( < PlanCard plan = { freePlan } isCurrent = { ! ! accountStatement } > { openSourceFeatures } </ PlanCard > ) ;
293
+ planCards . push ( < PlanCard isDisabled = { ! ! assignedTs } plan = { freePlan } isCurrent = { ! ! accountStatement } > { openSourceFeatures } </ PlanCard > ) ;
239
294
} else {
240
295
const targetPlan = freePlan ;
241
296
let bottomLabel ;
@@ -248,7 +303,7 @@ export default function () {
248
303
switch ( Plans . subscriptionChange ( currentPlan . type , targetPlan . type ) ) {
249
304
case 'downgrade' : onDowngrade = ( ) => confirmDowngrade ( targetPlan ) ; break ;
250
305
}
251
- planCards . push ( < PlanCard plan = { targetPlan } isCurrent = { false } onDowngrade = { onDowngrade } bottomLabel = { bottomLabel } > { openSourceFeatures } </ PlanCard > ) ;
306
+ planCards . push ( < PlanCard isDisabled = { ! ! assignedTs } plan = { targetPlan } isCurrent = { false } onDowngrade = { onDowngrade } bottomLabel = { bottomLabel } > { openSourceFeatures } </ PlanCard > ) ;
252
307
}
253
308
254
309
// Plan card: Personal
@@ -258,7 +313,7 @@ export default function () {
258
313
</ > ;
259
314
if ( currentPlan . chargebeeId === personalPlan . chargebeeId ) {
260
315
const bottomLabel = ( 'pendingSince' in currentPlan ) ? < p className = "text-green-600 animate-pulse" > Upgrade in progress</ p > : undefined ;
261
- planCards . push ( < PlanCard plan = { applyCoupons ( personalPlan , appliedCoupons ) } isCurrent = { true } bottomLabel = { bottomLabel } > { personalFeatures } </ PlanCard > ) ;
316
+ planCards . push ( < PlanCard isDisabled = { ! ! assignedTs } plan = { applyCoupons ( personalPlan , appliedCoupons ) } isCurrent = { true } bottomLabel = { bottomLabel } > { personalFeatures } </ PlanCard > ) ;
262
317
} else {
263
318
const targetPlan = applyCoupons ( personalPlan , availableCoupons ) ;
264
319
let bottomLabel ;
@@ -272,7 +327,7 @@ export default function () {
272
327
case 'upgrade' : onUpgrade = ( ) => confirmUpgrade ( targetPlan ) ; break ;
273
328
case 'downgrade' : onDowngrade = ( ) => confirmDowngrade ( targetPlan ) ; break ;
274
329
}
275
- planCards . push ( < PlanCard plan = { targetPlan } isCurrent = { false } onUpgrade = { onUpgrade } onDowngrade = { onDowngrade } bottomLabel = { bottomLabel } > { personalFeatures } </ PlanCard > ) ;
330
+ planCards . push ( < PlanCard isDisabled = { ! ! assignedTs } plan = { targetPlan } isCurrent = { false } onUpgrade = { onUpgrade } onDowngrade = { onDowngrade } bottomLabel = { bottomLabel } > { personalFeatures } </ PlanCard > ) ;
276
331
}
277
332
278
333
// Plan card: Professional
@@ -281,9 +336,10 @@ export default function () {
281
336
< p className = "truncate" title = "8 Parallel Workspaces" > ✓ 8 Parallel Workspaces</ p >
282
337
< p className = "truncate" title = "Teams" > ✓ Teams</ p >
283
338
</ > ;
339
+
284
340
if ( currentPlan . chargebeeId === professionalPlan . chargebeeId ) {
285
341
const bottomLabel = ( 'pendingSince' in currentPlan ) ? < p className = "text-green-600 animate-pulse" > Upgrade in progress</ p > : undefined ;
286
- planCards . push ( < PlanCard plan = { applyCoupons ( professionalPlan , appliedCoupons ) } isCurrent = { true } bottomLabel = { bottomLabel } > { professionalFeatures } </ PlanCard > ) ;
342
+ planCards . push ( < PlanCard isDisabled = { ! ! assignedTs } plan = { applyCoupons ( professionalPlan , appliedCoupons ) } isCurrent = { true } bottomLabel = { bottomLabel } > { professionalFeatures } </ PlanCard > ) ;
287
343
} else {
288
344
const targetPlan = applyCoupons ( professionalPlan , availableCoupons ) ;
289
345
let bottomLabel ;
@@ -297,7 +353,7 @@ export default function () {
297
353
case 'upgrade' : onUpgrade = ( ) => confirmUpgrade ( targetPlan ) ; break ;
298
354
case 'downgrade' : onDowngrade = ( ) => confirmDowngrade ( targetPlan ) ; break ;
299
355
}
300
- planCards . push ( < PlanCard plan = { targetPlan } isCurrent = { false } onUpgrade = { onUpgrade } onDowngrade = { onDowngrade } bottomLabel = { bottomLabel } > { professionalFeatures } </ PlanCard > ) ;
356
+ planCards . push ( < PlanCard isDisabled = { ! ! assignedTs } plan = { targetPlan } isCurrent = { ! ! assignedProfessionalTs } onUpgrade = { onUpgrade } onDowngrade = { onDowngrade } bottomLabel = { bottomLabel } isTsAssigned = { ! ! assignedProfessionalTs } > { professionalFeatures } </ PlanCard > ) ;
301
357
}
302
358
303
359
// Plan card: Unleashed (or Student Unleashed)
@@ -307,26 +363,29 @@ export default function () {
307
363
< p className = "truncate" title = "1h Timeout" > ✓ 1h Timeout</ p >
308
364
< p className = "truncate" title = "3h Timeout Boost" > ✓ 3h Timeout Boost</ p >
309
365
</ > ;
366
+ const isUnleashedTsAssigned = ! ! assignedStudentUnleashedTs || ! ! assignedUnleashedTs ;
310
367
if ( currentPlan . chargebeeId === studentUnleashedPlan . chargebeeId ) {
311
368
const bottomLabel = ( 'pendingSince' in currentPlan ) ? < p className = "text-green-600 animate-pulse" > Upgrade in progress</ p > : undefined ;
312
- planCards . push ( < PlanCard plan = { applyCoupons ( studentUnleashedPlan , appliedCoupons ) } isCurrent = { true } bottomLabel = { bottomLabel } > { unleashedFeatures } </ PlanCard > ) ;
369
+ planCards . push ( < PlanCard isDisabled = { ! ! assignedTs } plan = { applyCoupons ( studentUnleashedPlan , appliedCoupons ) } isCurrent = { true } bottomLabel = { bottomLabel } isTsAssigned = { isUnleashedTsAssigned } > { unleashedFeatures } </ PlanCard > ) ;
313
370
} else if ( currentPlan . chargebeeId === unleashedPlan . chargebeeId ) {
314
371
const bottomLabel = ( 'pendingSince' in currentPlan ) ? < p className = "text-green-600 animate-pulse" > Upgrade in progress</ p > : undefined ;
315
- planCards . push ( < PlanCard plan = { applyCoupons ( unleashedPlan , appliedCoupons ) } isCurrent = { true } bottomLabel = { bottomLabel } > { unleashedFeatures } </ PlanCard > ) ;
372
+ planCards . push ( < PlanCard isDisabled = { ! ! assignedTs } plan = { applyCoupons ( unleashedPlan , appliedCoupons ) } isCurrent = { true } bottomLabel = { bottomLabel } isTsAssigned = { isUnleashedTsAssigned } > { unleashedFeatures } </ PlanCard > ) ;
316
373
} else {
317
374
const targetPlan = applyCoupons ( isStudent ? studentUnleashedPlan : unleashedPlan , availableCoupons ) ;
318
375
let onUpgrade ;
319
376
switch ( Plans . subscriptionChange ( currentPlan . type , targetPlan . type ) ) {
320
377
case 'upgrade' : onUpgrade = ( ) => confirmUpgrade ( targetPlan ) ; break ;
321
378
}
322
- planCards . push ( < PlanCard plan = { targetPlan } isCurrent = { false } onUpgrade = { onUpgrade } > { unleashedFeatures } </ PlanCard > ) ;
379
+ planCards . push ( < PlanCard isDisabled = { ! ! assignedTs } plan = { targetPlan } isCurrent = { ! ! isUnleashedTsAssigned } onUpgrade = { onUpgrade } isTsAssigned = { isUnleashedTsAssigned } > { unleashedFeatures } </ PlanCard > ) ;
323
380
}
324
381
325
382
return < div >
326
383
< PageWithSubMenu subMenu = { settingsMenu } title = 'Plans' subtitle = 'Manage account usage and billing.' >
327
384
< div className = "w-full text-center" >
328
- < p className = "text-xl text-gray-500" > You are currently using the < span className = "font-bold" > { currentPlan . name } </ span > plan.</ p >
329
- < p className = "text-base w-96 m-auto" > Upgrade your plan to get access to private repositories or more parallel workspaces.</ p >
385
+ < p className = "text-xl text-gray-500" > You are currently using the < span className = "font-bold" > { Plans . getById ( assignedTs ?. planId ) ?. name || currentPlan . name } </ span > plan.</ p >
386
+ { ! assignedTs && (
387
+ < p className = "text-base w-96 m-auto" > Upgrade your plan to get access to private repositories or more parallel workspaces.</ p >
388
+ ) }
330
389
< p className = "mt-2 font-semibold text-gray-500" > Remaining hours: { typeof ( accountStatement ?. remainingHours ) === 'number'
331
390
? Math . floor ( accountStatement . remainingHours * 10 ) / 10
332
391
: accountStatement ?. remainingHours } </ p >
@@ -378,6 +437,38 @@ export default function () {
378
437
< button className = "bg-red-600 border-red-800" onClick = { doDowngrade } > Downgrade Plan</ button >
379
438
</ div >
380
439
</ Modal > }
440
+ { ! ! teamClaimModal && ( < Modal visible = { true } onClose = { ( ) => setTeamClaimModal ( undefined ) } >
441
+ < h3 className = "pb-2" > Team Invitation</ h3 >
442
+ < div className = "border-t border-b border-gray-200 mt-2 -mx-6 px-6 py-4" >
443
+ < p className = "pb-4 text-gray-500 text-base" > { teamClaimModal . mode === "error" ? teamClaimModal . errorText : teamClaimModal . text } </ p >
444
+ </ div >
445
+ < div className = "flex justify-end mt-6" >
446
+ { teamClaimModal . mode === "confirmation" && (
447
+ < React . Fragment >
448
+ < button className = "secondary" onClick = { ( ) => setTeamClaimModal ( undefined ) } > Cancel</ button >
449
+ < button className = { "ml-2" } onClick = { async ( ) => {
450
+ try {
451
+ await getGitpodService ( ) . server . tsAssignSlot ( teamClaimModal . teamId , teamClaimModal . slotId , undefined ) ;
452
+ window . history . replaceState ( { } , window . document . title , window . location . href . replace ( window . location . search , '' ) ) ;
453
+
454
+ setTeamClaimModal ( undefined ) ;
455
+
456
+ const statement = await server . getAccountStatement ( { } ) ;
457
+ setAccountStatement ( statement ) ;
458
+ } catch ( error ) {
459
+ setTeamClaimModal ( {
460
+ mode : "error" ,
461
+ errorText : `Error: ${ error . message } ` ,
462
+ } )
463
+ }
464
+ } } > Accept Invitation</ button >
465
+ </ React . Fragment >
466
+ ) }
467
+ { teamClaimModal . mode === "error" && (
468
+ < button className = "secondary" onClick = { ( ) => setTeamClaimModal ( undefined ) } > Close</ button >
469
+ ) }
470
+ </ div >
471
+ </ Modal > ) }
381
472
</ PageWithSubMenu >
382
473
</ div > ;
383
474
}
@@ -389,6 +480,8 @@ interface PlanCardProps {
389
480
onUpgrade ?: ( ) => void ;
390
481
onDowngrade ?: ( ) => void ;
391
482
bottomLabel ?: React . ReactNode ;
483
+ isDisabled ?: boolean ;
484
+ isTsAssigned ?: boolean ;
392
485
}
393
486
394
487
function PlanCard ( p : PlanCardProps ) {
@@ -405,11 +498,14 @@ function PlanCard(p: PlanCardProps) {
405
498
} </ p >
406
499
{ p . isCurrent
407
500
? < button className = "w-full" disabled = { true } > Current Plan</ button >
408
- : ( ( p . onUpgrade && < button className = "w-full secondary group-hover:bg-green-600 group-hover:text-gray-100" onClick = { p . onUpgrade } > Upgrade</ button > )
409
- || ( p . onDowngrade && < button className = "w-full secondary group-hover:bg-green-600 group-hover:text-gray-100" onClick = { p . onDowngrade } > Downgrade</ button > )
501
+ : ( ( p . onUpgrade && < button disabled = { p . isDisabled } className = "w-full secondary group-hover:bg-green-600 group-hover:text-gray-100" onClick = { p . onUpgrade } > Upgrade</ button > )
502
+ || ( p . onDowngrade && < button disabled = { p . isDisabled } className = "w-full secondary group-hover:bg-green-600 group-hover:text-gray-100" onClick = { p . onDowngrade } > Downgrade</ button > )
410
503
|| < button className = "w-full secondary" disabled = { true } > </ button > ) }
411
504
</ div >
412
505
< div className = "relative w-full" >
506
+ { p . isTsAssigned && (
507
+ < div className = "absolute w-full mt-5 text-center font-semibold" > Team seat assigned</ div >
508
+ ) }
413
509
< div className = "absolute w-full mt-5 text-center font-semibold" > { p . bottomLabel } </ div >
414
510
</ div >
415
511
</ SelectableCard > ;
0 commit comments