Skip to content

Commit d319d8c

Browse files
AlexTugarevgeropl
andauthored
[Switch to PAYG] Deduplicate calls to server.subscribeToStripe (#16822)
* simplify useLocalStorage hook * [switch2payg] evaluate isMigratedToTeamOnlyAttribution when rendering link to Billing * [switch2payg] Deduplicate calls to `server.subscribeToStripe` * [switch2payg] Deduplicate confetti --------- Co-authored-by: Gero Posmyk-Leinemann <[email protected]>
1 parent 0e2ec6c commit d319d8c

File tree

2 files changed

+138
-123
lines changed

2 files changed

+138
-123
lines changed

components/dashboard/src/SwitchToPAYG.tsx

Lines changed: 131 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import { Heading2, Subheading } from "./components/typography/headings";
2727
import { useCurrentOrg, useOrganizations } from "./data/organizations/orgs-query";
2828
import { PaymentContext } from "./payment-context";
2929

30+
// DEFAULTS
31+
const DEFAULT_USAGE_LIMIT = 1000;
32+
3033
/**
3134
* Keys of known page params
3235
*/
@@ -45,7 +48,7 @@ type PageParams = {
4548
setupIntentId?: string;
4649
};
4750
type PageState = {
48-
phase: "call-to-action" | "trigger-signup" | "wait-for-signup" | "cleanup" | "done";
51+
phase: "call-to-action" | "trigger-signup" | "cleanup" | "done";
4952
attributionId?: string;
5053
setupIntentId?: string;
5154
old?: {
@@ -68,23 +71,23 @@ function SwitchToPAYG() {
6871

6972
const currentOrg = useCurrentOrg().data;
7073
const orgs = useOrganizations().data;
71-
const [errorMessage, setErrorMessage] = useState<string | undefined>();
74+
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
7275
const [selectedOrganization, setSelectedOrganization] = useState<Team | undefined>(undefined);
7376
const [showBillingSetupModal, setShowBillingSetupModal] = useState<boolean>(false);
7477
const [pendingStripeSubscription, setPendingStripeSubscription] = useState<boolean>(false);
75-
const [droppedConfetti, setDroppedConfetti] = useState<boolean>(false);
7678
const { dropConfetti } = useConfetti();
7779

7880
useEffect(() => {
7981
setSelectedOrganization(currentOrg);
8082
}, [currentOrg, setSelectedOrganization]);
8183

8284
useEffect(() => {
83-
const { phase, attributionId, setupIntentId } = pageState;
85+
const phase = pageState.phase;
86+
const attributionId = pageState.attributionId;
87+
const setupIntentId = pageState.setupIntentId;
8488
if (phase !== "trigger-signup") {
8589
return;
8690
}
87-
console.log("phase: " + phase);
8891

8992
// We're back from the Stripe modal: (safely) trigger the signup
9093
if (!attributionId) {
@@ -97,53 +100,34 @@ function SwitchToPAYG() {
97100
return;
98101
}
99102

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 })}`);
132104

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 }));
134112
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]);
143120

144121
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+
147131
if (phase === "trigger-signup") {
148132
// Handled in separate effect
149133
return;
@@ -158,12 +142,28 @@ function SwitchToPAYG() {
158142
return;
159143
}
160144

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+
162154
switch (phase) {
163155
case "call-to-action": {
164156
// 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+
}));
167167
return;
168168
}
169169

@@ -190,7 +190,7 @@ function SwitchToPAYG() {
190190
if (Subscription.isCancelled(sub, now) || !Subscription.isActive(sub, now)) {
191191
// We're happy!
192192
if (!cancelled) {
193-
setPageState((s) => ({ ...s, phase: "done" }));
193+
setPageState((prev) => ({ ...prev, phase: "done" }));
194194
}
195195
return;
196196
}
@@ -215,7 +215,7 @@ function SwitchToPAYG() {
215215
if (TeamSubscription.isCancelled(ts, now) || !TeamSubscription.isActive(ts, now)) {
216216
// We're happy!
217217
if (!cancelled) {
218-
setPageState((s) => ({ ...s, phase: "done" }));
218+
setPageState((prev) => ({ ...prev, phase: "done" }));
219219
}
220220
return;
221221
}
@@ -245,7 +245,7 @@ function SwitchToPAYG() {
245245
if (TeamSubscription2.isCancelled(ts2, now) || !TeamSubscription2.isActive(ts2, now)) {
246246
// We're happy!
247247
if (!cancelled) {
248-
setPageState((s) => ({ ...s, phase: "done" }));
248+
setPageState((prev) => ({ ...prev, phase: "done" }));
249249
}
250250
return;
251251
}
@@ -259,55 +259,8 @@ function SwitchToPAYG() {
259259
}
260260
}
261261
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 }));
303263
}
304-
305-
setPendingStripeSubscription(false);
306-
if (!subscriptionId) {
307-
setErrorMessage(`Could not find the subscription.`);
308-
return;
309-
}
310-
setPageState((s) => ({ ...s, phase: "cleanup" }));
311264
})().catch(console.error);
312265

313266
return () => {
@@ -377,29 +330,31 @@ function SwitchToPAYG() {
377330
break;
378331
}
379332
}
380-
setPageState((s) => ({ ...s, phase: "done" }));
333+
setPageState((prev) => ({ ...prev, phase: "done" }));
381334
return;
382335
}
383336

384337
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();
389343
dropConfetti();
390344
}
391345
return;
392346
}
393347
}, [
394-
location.search,
395-
pageParams,
396-
pageState,
397-
setPageState,
398-
pendingStripeSubscription,
399-
setPendingStripeSubscription,
400348
selectedOrganization,
401349
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,
403358
]);
404359

405360
const onUpgradePlan = useCallback(async () => {
@@ -427,8 +382,15 @@ function SwitchToPAYG() {
427382

428383
const attributionId = pageState.attributionId || "";
429384
const parsed = AttributionId.parse(attributionId);
385+
let billingLink = "/billing";
430386
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+
}
432394

433395
return (
434396
<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: {
631593
>
632594
{props.headline}
633595
</p>
634-
<input className="opacity-0" type="radio" checked={props.selected} />
596+
<input className="opacity-0" type="radio" checked={props.selected} readOnly={true} />
635597
</div>
636598
<div className="pl-1 grid auto-rows-auto">
637599
<div
@@ -686,4 +648,57 @@ function parseSearchParams(search: string): PageParams | undefined {
686648
}
687649
}
688650

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+
689704
export default SwitchToPAYG;

0 commit comments

Comments
 (0)