Skip to content

Commit 84e5640

Browse files
authored
[dashboard] Guard subscribeToStripe against multiple calls (#16890)
1 parent 60413be commit 84e5640

File tree

1 file changed

+78
-44
lines changed

1 file changed

+78
-44
lines changed

components/dashboard/src/components/UsageBasedBillingConfig.tsx

Lines changed: 78 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ interface Props {
3232
hideSubheading?: boolean;
3333
}
3434

35+
// Guard against multiple calls to subscripe (per page load)
36+
let didAlreadyCallSubscribe = false;
37+
3538
export default function UsageBasedBillingConfig({ attributionId, hideSubheading = false }: Props) {
36-
const location = useLocation();
3739
const currentOrg = useCurrentOrg().data;
3840
const attrId = attributionId ? AttributionId.parse(attributionId) : undefined;
3941
const [showUpdateLimitModal, setShowUpdateLimitModal] = useState<boolean>(false);
@@ -49,6 +51,9 @@ export default function UsageBasedBillingConfig({ attributionId, hideSubheading
4951
undefined,
5052
);
5153

54+
// Stripe-controlled parameters
55+
const location = useLocation();
56+
5257
const now = useMemo(() => dayjs().utc(true), []);
5358
const [billingCycleFrom, setBillingCycleFrom] = useState<dayjs.Dayjs>(now.startOf("month"));
5459
const [billingCycleTo, setBillingCycleTo] = useState<dayjs.Dayjs>(now.endOf("month"));
@@ -90,53 +95,82 @@ export default function UsageBasedBillingConfig({ attributionId, hideSubheading
9095
}, [attributionId, refreshSubscriptionDetails]);
9196

9297
useEffect(() => {
93-
if (!attributionId) {
94-
return;
95-
}
9698
const params = new URLSearchParams(location.search);
97-
if (!params.get("setup_intent") || params.get("redirect_status") !== "succeeded") {
98-
return;
99+
const setupIntentId = params.get("setup_intent");
100+
const redirectStatus = params.get("redirect_status");
101+
if (setupIntentId && redirectStatus) {
102+
subscribeToStripe({
103+
setupIntentId,
104+
redirectStatus,
105+
});
99106
}
100-
const setupIntentId = params.get("setup_intent")!;
101-
window.history.replaceState({}, "", location.pathname);
102-
(async () => {
103-
const pendingSubscription = { pendingSince: Date.now() };
104-
try {
105-
setPendingStripeSubscription(pendingSubscription);
106-
// Pick a good initial value for the Stripe usage limit (base_limit * team_size)
107-
// FIXME: Should we ask the customer to confirm or edit this default limit?
108-
let limit = BASE_USAGE_LIMIT_FOR_STRIPE_USERS;
109-
if (attrId?.kind === "team" && currentOrg) {
110-
limit = BASE_USAGE_LIMIT_FOR_STRIPE_USERS * currentOrg.members.length;
111-
}
112-
const newLimit = await getGitpodService().server.subscribeToStripe(attributionId, setupIntentId, limit);
113-
if (newLimit) {
114-
setUsageLimit(newLimit);
115-
}
107+
// eslint-disable-next-line react-hooks/exhaustive-deps
108+
}, []);
116109

117-
//refresh every 5 secs until we get a subscriptionId
118-
const interval = setInterval(async () => {
119-
try {
120-
const subscriptionId = await refreshSubscriptionDetails(attributionId);
121-
if (subscriptionId) {
122-
setPendingStripeSubscription(undefined);
123-
clearInterval(interval);
124-
}
125-
} catch (error) {
126-
console.error(error);
127-
}
128-
}, 1000);
129-
} catch (error) {
130-
console.error("Could not subscribe to Stripe", error);
131-
setPendingStripeSubscription(undefined);
132-
setErrorMessage(
133-
`Could not subscribe: ${
134-
error?.message || String(error)
135-
} Contact [email protected] if you believe this is a system error.`,
136-
);
110+
const subscribeToStripe = useCallback(
111+
(stripeParams: { setupIntentId: string; redirectStatus: string }) => {
112+
if (!attributionId) {
113+
return;
137114
}
138-
})();
139-
}, [attrId?.kind, attributionId, currentOrg, location.pathname, location.search, refreshSubscriptionDetails]);
115+
const { setupIntentId, redirectStatus } = stripeParams;
116+
if (redirectStatus !== "succeeded") {
117+
// TODO(gpl) We have to handle external validation errors (3DS, e.g.) here
118+
return;
119+
}
120+
121+
// Guard against multiple execution following the pattern here: https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application
122+
if (didAlreadyCallSubscribe) {
123+
console.log("didAlreadyCallSubscribe, skipping this time.");
124+
return;
125+
}
126+
didAlreadyCallSubscribe = true;
127+
console.log("didAlreadyCallSubscribe false, first run.");
128+
129+
window.history.replaceState({}, "", location.pathname);
130+
(async () => {
131+
const pendingSubscription = { pendingSince: Date.now() };
132+
try {
133+
setPendingStripeSubscription(pendingSubscription);
134+
// Pick a good initial value for the Stripe usage limit (base_limit * team_size)
135+
// FIXME: Should we ask the customer to confirm or edit this default limit?
136+
let limit = BASE_USAGE_LIMIT_FOR_STRIPE_USERS;
137+
if (attrId?.kind === "team" && currentOrg) {
138+
limit = BASE_USAGE_LIMIT_FOR_STRIPE_USERS * currentOrg.members.length;
139+
}
140+
const newLimit = await getGitpodService().server.subscribeToStripe(
141+
attributionId,
142+
setupIntentId,
143+
limit,
144+
);
145+
if (newLimit) {
146+
setUsageLimit(newLimit);
147+
}
148+
149+
//refresh every 5 secs until we get a subscriptionId
150+
const interval = setInterval(async () => {
151+
try {
152+
const subscriptionId = await refreshSubscriptionDetails(attributionId);
153+
if (subscriptionId) {
154+
setPendingStripeSubscription(undefined);
155+
clearInterval(interval);
156+
}
157+
} catch (error) {
158+
console.error(error);
159+
}
160+
}, 1000);
161+
} catch (error) {
162+
console.error("Could not subscribe to Stripe", error);
163+
setPendingStripeSubscription(undefined);
164+
setErrorMessage(
165+
`Could not subscribe: ${
166+
error?.message || String(error)
167+
} Contact [email protected] if you believe this is a system error.`,
168+
);
169+
}
170+
})();
171+
},
172+
[attrId?.kind, attributionId, currentOrg, location.pathname, refreshSubscriptionDetails],
173+
);
140174

141175
const showSpinner = !attributionId || isLoadingStripeSubscription || !!pendingStripeSubscription;
142176
const showBalance = !showSpinner;

0 commit comments

Comments
 (0)