@@ -28,6 +28,8 @@ export default function TeamUsageBasedBilling() {
28
28
const [ pendingStripeSubscription , setPendingStripeSubscription ] = useState < PendingStripeSubscription | undefined > ( ) ;
29
29
const [ pollStripeSubscriptionTimeout , setPollStripeSubscriptionTimeout ] = useState < NodeJS . Timeout | undefined > ( ) ;
30
30
const [ stripePortalUrl , setStripePortalUrl ] = useState < string | undefined > ( ) ;
31
+ const [ showUpdateLimitModal , setShowUpdateLimitModal ] = useState < boolean > ( false ) ;
32
+ const [ spendingLimit , setSpendingLimit ] = useState < number | undefined > ( ) ;
31
33
32
34
useEffect ( ( ) => {
33
35
if ( ! team ) {
@@ -54,6 +56,8 @@ export default function TeamUsageBasedBilling() {
54
56
( async ( ) => {
55
57
const portalUrl = await getGitpodService ( ) . server . getStripePortalUrlForTeam ( team . id ) ;
56
58
setStripePortalUrl ( portalUrl ) ;
59
+ const spendingLimit = await getGitpodService ( ) . server . getSpendingLimitForTeam ( team . id ) ;
60
+ setSpendingLimit ( spendingLimit ) ;
57
61
} ) ( ) ;
58
62
} , [ team , stripeSubscriptionId ] ) ;
59
63
@@ -135,30 +139,50 @@ export default function TeamUsageBasedBilling() {
135
139
return < > </ > ;
136
140
}
137
141
142
+ const showSpinner = isLoading || pendingStripeSubscription ;
143
+ const showUpgradeBilling = ! showSpinner && ! stripeSubscriptionId ;
144
+ const showManageBilling = ! showSpinner && ! ! stripeSubscriptionId ;
145
+
146
+ const doUpdateLimit = async ( newLimit : number ) => {
147
+ if ( ! team ) {
148
+ return ;
149
+ }
150
+ const oldLimit = spendingLimit ;
151
+ setSpendingLimit ( newLimit ) ;
152
+ try {
153
+ await getGitpodService ( ) . server . setSpendingLimitForTeam ( team . id , newLimit ) ;
154
+ } catch ( error ) {
155
+ setSpendingLimit ( oldLimit ) ;
156
+ console . error ( error ) ;
157
+ alert ( error ?. message || "Failed to update spending limit. See console for error message." ) ;
158
+ }
159
+ setShowUpdateLimitModal ( false ) ;
160
+ } ;
161
+
138
162
return (
139
163
< div className = "mb-16" >
140
164
< h3 > Usage-Based Billing</ h3 >
141
165
< h2 className = "text-gray-500" > Manage usage-based billing, spending limit, and payment method.</ h2 >
142
- < div className = "max-w-xl" >
143
- < div className = "mt-4 h-32 p-4 flex flex-col rounded-xl bg-gray-100 dark:bg-gray-800" >
144
- < div className = "uppercase text-sm text- gray-400 dark:text -gray-500" > Billing </ div >
145
- { ( isLoading || pendingStripeSubscription ) && (
146
- < >
147
- < Spinner className = "m-2 h-5 w-5 animate-spin" / >
148
- </ >
149
- ) }
150
- { ! isLoading && ! pendingStripeSubscription && ! stripeSubscriptionId && (
151
- < >
152
- < div className = "text-xl font-semibold flex-grow text-gray-600 dark:text-gray-400" >
153
- Inactive
154
- </ div >
155
- < button className = "self-end" onClick = { ( ) => setShowBillingSetupModal ( true ) } >
156
- Upgrade Billing
157
- </ button >
158
- </ >
159
- ) }
160
- { ! isLoading && ! pendingStripeSubscription && ! ! stripeSubscriptionId && (
161
- < >
166
+ < div className = "max-w-xl flex flex-col " >
167
+ { showSpinner && (
168
+ < div className = "flex flex-col mt-4 h-32 p-4 rounded-xl bg- gray-100 dark:bg -gray-800" >
169
+ < div className = "uppercase text-sm text-gray-400 dark:text-gray-500" > Billing </ div >
170
+ < Spinner className = "m-2 h-5 w-5 animate-spin" / >
171
+ </ div >
172
+ ) }
173
+ { showUpgradeBilling && (
174
+ < div className = "flex flex-col mt-4 h-32 p-4 rounded-xl bg-gray-100 dark:bg-gray-800" >
175
+ < div className = "uppercase text-sm text-gray-400 dark:text-gray-500" > Billing </ div >
176
+ < div className = "text-xl font-semibold flex-grow text-gray-600 dark:text-gray-400" > Inactive </ div >
177
+ < button className = "self-end" onClick = { ( ) => setShowBillingSetupModal ( true ) } >
178
+ Upgrade Billing
179
+ </ button >
180
+ </ div >
181
+ ) }
182
+ { showManageBilling && (
183
+ < div className = "max-w-xl flex space-x-4" >
184
+ < div className = "flex flex-col w-72 mt-4 h-32 p-4 rounded-xl bg-gray-100 dark:bg-gray-800" >
185
+ < div className = "uppercase text-sm text-gray-400 dark:text-gray-500" > Billing </ div >
162
186
< div className = "text-xl font-semibold flex-grow text-gray-600 dark:text-gray-400" >
163
187
Active
164
188
</ div >
@@ -167,11 +191,27 @@ export default function TeamUsageBasedBilling() {
167
191
Manage Billing →
168
192
</ button >
169
193
</ a >
170
- </ >
171
- ) }
172
- </ div >
194
+ </ div >
195
+ < div className = "flex flex-col w-72 mt-4 h-32 p-4 rounded-xl bg-gray-100 dark:bg-gray-800" >
196
+ < div className = "uppercase text-sm text-gray-400 dark:text-gray-500" > Spending Limit</ div >
197
+ < div className = "text-xl font-semibold flex-grow text-gray-600 dark:text-gray-400" >
198
+ { spendingLimit || "–" }
199
+ </ div >
200
+ < button className = "self-end" onClick = { ( ) => setShowUpdateLimitModal ( true ) } >
201
+ Update Limit
202
+ </ button >
203
+ </ div >
204
+ </ div >
205
+ ) }
173
206
</ div >
174
207
{ showBillingSetupModal && < BillingSetupModal onClose = { ( ) => setShowBillingSetupModal ( false ) } /> }
208
+ { showUpdateLimitModal && (
209
+ < UpdateLimitModal
210
+ currentValue = { spendingLimit }
211
+ onClose = { ( ) => setShowUpdateLimitModal ( false ) }
212
+ onUpdate = { ( newLimit ) => doUpdateLimit ( newLimit ) }
213
+ />
214
+ ) }
175
215
</ div >
176
216
) ;
177
217
}
@@ -182,6 +222,49 @@ function getStripeAppearance(isDark?: boolean): Appearance {
182
222
} ;
183
223
}
184
224
225
+ function UpdateLimitModal ( props : {
226
+ currentValue : number | undefined ;
227
+ onClose : ( ) => void ;
228
+ onUpdate : ( newLimit : number ) => { } ;
229
+ } ) {
230
+ const [ newLimit , setNewLimit ] = useState < number | undefined > ( props . currentValue ) ;
231
+
232
+ return (
233
+ < Modal visible = { true } onClose = { props . onClose } >
234
+ < h3 className = "flex" > Update Limit</ h3 >
235
+ < div className = "border-t border-b border-gray-200 dark:border-gray-800 -mx-6 px-6 py-4 flex flex-col" >
236
+ < p className = "pb-4 text-gray-500 text-base" > Set up a spending limit on a monthly basis.</ p >
237
+
238
+ < label htmlFor = "newLimit" className = "font-medium" >
239
+ Limit
240
+ </ label >
241
+ < div className = "w-full" >
242
+ < input
243
+ name = "newLimit"
244
+ type = "number"
245
+ min = { 0 }
246
+ value = { newLimit }
247
+ className = "rounded-md w-full truncate overflow-x-scroll pr-8"
248
+ onChange = { ( e ) => setNewLimit ( parseInt ( e . target . value || "1" , 10 ) ) }
249
+ />
250
+ </ div >
251
+ </ div >
252
+ < div className = "flex justify-end mt-6 space-x-2" >
253
+ < button
254
+ className = "secondary"
255
+ onClick = { ( ) => {
256
+ if ( typeof newLimit === "number" ) {
257
+ props . onUpdate ( newLimit ) ;
258
+ }
259
+ } }
260
+ >
261
+ Update
262
+ </ button >
263
+ </ div >
264
+ </ Modal >
265
+ ) ;
266
+ }
267
+
185
268
function BillingSetupModal ( props : { onClose : ( ) => void } ) {
186
269
const { isDark } = useContext ( ThemeContext ) ;
187
270
const [ stripePromise , setStripePromise ] = useState < Promise < Stripe | null > | undefined > ( ) ;
@@ -243,7 +326,7 @@ function CreditCardInputForm() {
243
326
}
244
327
} catch ( error ) {
245
328
console . error ( error ) ;
246
- alert ( error ) ;
329
+ alert ( error ?. message || "Failed to submit form. See console for error message." ) ;
247
330
} finally {
248
331
setIsLoading ( false ) ;
249
332
}
0 commit comments