Skip to content

Commit c64826f

Browse files
committed
handle invite URLs for teams
1 parent da162f1 commit c64826f

File tree

2 files changed

+113
-23
lines changed

2 files changed

+113
-23
lines changed

components/dashboard/src/settings/Plans.tsx

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7-
import { useState, useEffect, useContext } from "react";
7+
import React, { useState, useEffect, useContext } from "react";
88
import { countries } from 'countries-list';
99
import { AccountStatement, Subscription, UserPaidSubscription, AssignedTeamSubscription } from "@gitpod/gitpod-protocol/lib/accounting-protocol";
1010
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";
1212
import { ChargebeeClient } from "../chargebee/chargebee-client";
1313
import Modal from "../components/Modal";
1414
import SelectableCard from "../components/SelectableCard";
@@ -22,6 +22,16 @@ import settingsMenu from "./settings-menu";
2222
type PlanWithOriginalPrice = Plan & { originalPrice?: number };
2323
type PendingPlan = PlanWithOriginalPrice & { pendingSince: number };
2424

25+
type TeamClaimModal = {
26+
errorText: string;
27+
mode: "error";
28+
} | {
29+
text: string;
30+
teamId: string;
31+
slotId: string;
32+
mode: "confirmation";
33+
}
34+
2535
export default function () {
2636
const { user } = useContext(UserContext);
2737
const { server } = getGitpodService();
@@ -34,6 +44,8 @@ export default function () {
3444
const [ gitHubUpgradeUrls, setGitHubUpgradeUrls ] = useState<GithubUpgradeURL[]>();
3545
const [ privateRepoTrialEndDate, setPrivateRepoTrialEndDate ] = useState<string>();
3646

47+
const [ teamClaimModal, setTeamClaimModal ] = useState<TeamClaimModal | undefined>(undefined);
48+
3749
let pollAccountStatementTimeout: NodeJS.Timeout | undefined;
3850

3951
useEffect(() => {
@@ -49,12 +61,46 @@ export default function () {
4961
server.getAppliedCoupons().then(v => () => setAppliedCoupons(v)),
5062
server.getGithubUpgradeUrls().then(v => () => setGitHubUpgradeUrls(v)),
5163
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+
5370
return function cleanup() {
5471
clearTimeout(pollAccountStatementTimeout!);
5572
}
5673
}, []);
5774

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+
58104
console.log('privateRepoTrialEndDate', privateRepoTrialEndDate);
59105

60106
const activeSubscriptions = (accountStatement?.subscriptions || []).filter(s => Subscription.isActive(s, new Date().toISOString()));
@@ -64,8 +110,17 @@ export default function () {
64110
const freePlan = freeSubscription && Plans.getById(freeSubscription.planId) || Plans.getFreePlan(user?.creationDate || new Date().toISOString());
65111
const paidSubscription = activeSubscriptions.find(s => UserPaidSubscription.is(s));
66112
const paidPlan = paidSubscription && Plans.getById(paidSubscription.planId);
113+
67114
const assignedTeamSubscriptions = activeSubscriptions.filter(s => AssignedTeamSubscription.is(s));
68115
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;
69124

70125
const [ pendingUpgradePlan, setPendingUpgradePlan ] = useState<PendingPlan | undefined>(getLocalStorageObject('pendingUpgradePlan'));
71126
const setPendingUpgrade = (to: PendingPlan) => {
@@ -95,7 +150,7 @@ export default function () {
95150
}
96151

97152
// 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;
99154

100155
// If the user has a paid plan with a different currency, force that currency.
101156
if (currency !== currentPlan.currency && !Plans.isFreePlan(currentPlan.chargebeeId)) {
@@ -235,7 +290,7 @@ export default function () {
235290
<p className="truncate" title="30 min Timeout">✓ 30 min Timeout</p>
236291
</>;
237292
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>);
239294
} else {
240295
const targetPlan = freePlan;
241296
let bottomLabel;
@@ -248,7 +303,7 @@ export default function () {
248303
switch (Plans.subscriptionChange(currentPlan.type, targetPlan.type)) {
249304
case 'downgrade': onDowngrade = () => confirmDowngrade(targetPlan); break;
250305
}
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>);
252307
}
253308

254309
// Plan card: Personal
@@ -258,7 +313,7 @@ export default function () {
258313
</>;
259314
if (currentPlan.chargebeeId === personalPlan.chargebeeId) {
260315
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>);
262317
} else {
263318
const targetPlan = applyCoupons(personalPlan, availableCoupons);
264319
let bottomLabel;
@@ -272,7 +327,7 @@ export default function () {
272327
case 'upgrade': onUpgrade = () => confirmUpgrade(targetPlan); break;
273328
case 'downgrade': onDowngrade = () => confirmDowngrade(targetPlan); break;
274329
}
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>);
276331
}
277332

278333
// Plan card: Professional
@@ -281,9 +336,10 @@ export default function () {
281336
<p className="truncate" title="8 Parallel Workspaces">✓ 8 Parallel Workspaces</p>
282337
<p className="truncate" title="Teams">✓ Teams</p>
283338
</>;
339+
284340
if (currentPlan.chargebeeId === professionalPlan.chargebeeId) {
285341
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>);
287343
} else {
288344
const targetPlan = applyCoupons(professionalPlan, availableCoupons);
289345
let bottomLabel;
@@ -297,7 +353,7 @@ export default function () {
297353
case 'upgrade': onUpgrade = () => confirmUpgrade(targetPlan); break;
298354
case 'downgrade': onDowngrade = () => confirmDowngrade(targetPlan); break;
299355
}
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>);
301357
}
302358

303359
// Plan card: Unleashed (or Student Unleashed)
@@ -307,26 +363,29 @@ export default function () {
307363
<p className="truncate" title="1h Timeout">✓ 1h Timeout</p>
308364
<p className="truncate" title="3h Timeout Boost">✓ 3h Timeout Boost</p>
309365
</>;
366+
const isUnleashedTsAssigned = !!assignedStudentUnleashedTs || !!assignedUnleashedTs;
310367
if (currentPlan.chargebeeId === studentUnleashedPlan.chargebeeId) {
311368
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>);
313370
} else if (currentPlan.chargebeeId === unleashedPlan.chargebeeId) {
314371
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>);
316373
} else {
317374
const targetPlan = applyCoupons(isStudent ? studentUnleashedPlan : unleashedPlan, availableCoupons);
318375
let onUpgrade;
319376
switch (Plans.subscriptionChange(currentPlan.type, targetPlan.type)) {
320377
case 'upgrade': onUpgrade = () => confirmUpgrade(targetPlan); break;
321378
}
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>);
323380
}
324381

325382
return <div>
326383
<PageWithSubMenu subMenu={settingsMenu} title='Plans' subtitle='Manage account usage and billing.'>
327384
<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+
)}
330389
<p className="mt-2 font-semibold text-gray-500">Remaining hours: {typeof(accountStatement?.remainingHours) === 'number'
331390
? Math.floor(accountStatement.remainingHours * 10) / 10
332391
: accountStatement?.remainingHours}</p>
@@ -378,6 +437,38 @@ export default function () {
378437
<button className="bg-red-600 border-red-800" onClick={doDowngrade}>Downgrade Plan</button>
379438
</div>
380439
</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>)}
381472
</PageWithSubMenu>
382473
</div>;
383474
}
@@ -389,6 +480,8 @@ interface PlanCardProps {
389480
onUpgrade?: () => void;
390481
onDowngrade?: () => void;
391482
bottomLabel?: React.ReactNode;
483+
isDisabled?: boolean;
484+
isTsAssigned?: boolean;
392485
}
393486

394487
function PlanCard(p: PlanCardProps) {
@@ -405,11 +498,14 @@ function PlanCard(p: PlanCardProps) {
405498
}</p>
406499
{p.isCurrent
407500
? <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>)
410503
|| <button className="w-full secondary" disabled={true}>&nbsp;</button>)}
411504
</div>
412505
<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+
)}
413509
<div className="absolute w-full mt-5 text-center font-semibold">{p.bottomLabel}</div>
414510
</div>
415511
</SelectableCard>;

components/dashboard/src/settings/Teams.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,11 @@ function AllTeams() {
5151
const [addMembersModal, setAddMembersModal] = useState<{ sub: TeamSubscription } | undefined>(undefined);
5252

5353
const restorePendingPlanPurchase = () => {
54-
if (pendingPlanPurchase) {
55-
return;
56-
}
5754
const pendingState = restorePendingState("pendingPlanPurchase") as { planId: string } | undefined;
5855
return pendingState;
5956
}
6057

6158
const restorePendingSlotsPurchase = () => {
62-
if (pendingSlotsPurchase) {
63-
return;
64-
}
6559
const pendingState = restorePendingState("pendingSlotsPurchase") as { tsId: string } | undefined;
6660
return pendingState;
6761
}

0 commit comments

Comments
 (0)