CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/billing/actions.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Billing actions.
8
9
These are mainly for interfacing with Stripe. They are
10
all async (no callbacks!).
11
*/
12
13
import { fromJS, Map } from "immutable";
14
import { redux, Actions, Store } from "../app-framework";
15
import { reuse_in_flight_methods } from "@cocalc/util/async-utils";
16
import {
17
server_minutes_ago,
18
server_time,
19
server_days_ago,
20
} from "@cocalc/util/misc";
21
import { webapp_client } from "../webapp-client";
22
import { StripeClient } from "../client/stripe";
23
import { getManagedLicenses } from "../account/licenses/util";
24
25
import { BillingStoreState } from "./store";
26
27
require("./store"); // ensure 'billing' store is created so can set this.store below.
28
29
export class BillingActions extends Actions<BillingStoreState> {
30
private store: Store<BillingStoreState>;
31
private last_subscription_attempt?: any;
32
private stripe: StripeClient;
33
34
constructor(name: string, redux: any) {
35
super(name, redux);
36
const store = redux.getStore("billing");
37
if (store == null) throw Error("bug -- billing store should be defined");
38
this.store = store;
39
this.stripe = webapp_client.stripe;
40
reuse_in_flight_methods(this, ["update_customer"]);
41
}
42
43
public clear_error(): void {
44
this.setState({ error: "" });
45
}
46
47
public async update_customer(): Promise<void> {
48
const is_commercial = redux
49
.getStore("customize")
50
.get("is_commercial", false);
51
if (!is_commercial) return;
52
this.setState({ action: "Updating billing information" });
53
try {
54
const resp = await this.stripe.get_customer();
55
if (!resp.stripe_publishable_key) {
56
this.setState({ no_stripe: true });
57
throw Error(
58
"WARNING: Stripe is not configured -- billing not available"
59
);
60
}
61
this.setState({
62
customer: resp.customer,
63
loaded: true,
64
stripe_publishable_key: resp.stripe_publishable_key,
65
});
66
if (resp.customer) {
67
// TODO: only call get_invoices if the customer already exists in the system!
68
// FUTURE: -- this {limit:100} will change when we use webhooks and our own database of info...
69
const invoices = await this.stripe.get_invoices({
70
limit: 100,
71
});
72
this.setState({ invoices });
73
}
74
} catch (err) {
75
this.setState({ error: err });
76
throw err;
77
} finally {
78
this.setState({ action: "" });
79
}
80
}
81
82
// Call a webapp_client.stripe. function with given opts, returning
83
// the result (which matters only for coupons?).
84
// This is wrapped as an async call, and also sets the action and error
85
// states of the Store so the UI can reflect what is happening.
86
// Also, after update_customer gets called, to update the UI.
87
// If there is an error, this also throws that error (so it is NOT just
88
// reflected in the UI).
89
private async stripe_action(
90
f: Function,
91
desc: string,
92
...args
93
): Promise<any> {
94
this.setState({ action: desc });
95
try {
96
return await f.bind(this.stripe)(...args);
97
} catch (err) {
98
this.setState({ error: `${err}` });
99
throw err;
100
} finally {
101
this.setState({ action: "" });
102
await this.update_customer();
103
}
104
}
105
106
public clear_action(): void {
107
this.setState({ action: "", error: "" });
108
}
109
110
public async delete_payment_method(card_id: string): Promise<void> {
111
await this.stripe_action(
112
this.stripe.delete_source,
113
"Deleting a payment method",
114
card_id
115
);
116
}
117
118
public async set_as_default_payment_method(card_id: string): Promise<void> {
119
await this.stripe_action(
120
this.stripe.set_default_source,
121
"Setting payment method as default",
122
card_id
123
);
124
}
125
126
public async submit_payment_method(token: string): Promise<void> {
127
await this.stripe_action(
128
this.stripe.create_source,
129
"Creating a new payment method (sending token)",
130
token
131
);
132
}
133
134
public async cancel_subscription(subscription_id: string): Promise<void> {
135
await this.stripe_action(
136
this.stripe.cancel_subscription,
137
"Cancel a subscription",
138
{ subscription_id }
139
);
140
}
141
142
public async create_subscription(plan: string): Promise<void> {
143
const lsa = this.last_subscription_attempt;
144
if (
145
lsa != null &&
146
lsa.plan == plan &&
147
lsa.timestamp > server_minutes_ago(2)
148
) {
149
this.setState({
150
action: "",
151
error:
152
"Too many subscription attempts in the last minute. Please **REFRESH YOUR BROWSER** THEN DOUBLE CHECK YOUR SUBSCRIPTION LIST.",
153
});
154
return;
155
}
156
let coupon: any;
157
this.setState({ error: "" });
158
const applied_coupons = this.store.get("applied_coupons");
159
if (applied_coupons != null && applied_coupons.size > 0) {
160
coupon = applied_coupons.first();
161
}
162
const opts = {
163
plan,
164
coupon_id: coupon?.id,
165
};
166
await this.stripe_action(
167
this.stripe.create_subscription,
168
"Create a subscription",
169
opts
170
);
171
this.last_subscription_attempt = { timestamp: server_time(), plan };
172
}
173
174
public async apply_coupon(coupon_id: string): Promise<any> {
175
try {
176
const coupon = await this.stripe_action(
177
this.stripe.get_coupon,
178
`Applying coupon: ${coupon_id}`,
179
coupon_id
180
);
181
const applied_coupons = this.store
182
.get("applied_coupons", Map<string, any>())
183
.set(coupon.id, coupon);
184
if (applied_coupons == null) throw Error("BUG -- can't happen");
185
this.setState({ applied_coupons, coupon_error: "" });
186
} catch (err) {
187
return this.setState({ coupon_error: `${err}` });
188
}
189
}
190
191
public clear_coupon_error(): void {
192
this.setState({ coupon_error: "" });
193
}
194
195
public remove_all_coupons(): void {
196
this.setState({ applied_coupons: Map<string, any>(), coupon_error: "" });
197
}
198
199
public remove_coupon(coupon_id: string): void {
200
this.setState({
201
applied_coupons: this.store
202
.get("applied_coupons", Map<string, any>())
203
.delete(coupon_id),
204
});
205
}
206
207
// Cancel all subscriptions, remove credit cards, etc. -- this is not a normal action,
208
// and is used only when deleting an account.
209
public async cancel_everything(): Promise<void> {
210
// update info about this customer
211
await this.update_customer();
212
// delete stuff
213
// delete payment methods
214
const payment_methods = this.store.getIn(["customer", "sources", "data"]);
215
if (payment_methods != null) {
216
for (const x of payment_methods.toJS() as any) {
217
await this.delete_payment_method(x.id);
218
}
219
}
220
const subscriptions = this.store.getIn([
221
"customer",
222
"subscriptions",
223
"data",
224
]);
225
if (subscriptions != null) {
226
for (const x of subscriptions.toJS() as any) {
227
await this.cancel_subscription(x.id);
228
}
229
}
230
}
231
232
// Set this while we are paying for the course.
233
public set_is_paying_for_course(
234
project_id: string,
235
is_paying: boolean
236
): void {
237
let course_pay = this.store.get("course_pay");
238
let continue_first_purchase = this.store.get("continue_first_purchase");
239
if (is_paying) {
240
course_pay = course_pay.add(project_id);
241
} else {
242
course_pay = course_pay.remove(project_id);
243
continue_first_purchase = false;
244
}
245
this.setState({ course_pay, continue_first_purchase });
246
}
247
248
public set_selected_plan(plan: string, period?: string): void {
249
if (period != null) {
250
if (period.slice(0, 4) == "year") {
251
plan += "-year";
252
} else if (period.slice(0, 4) == "week") {
253
plan += "-week";
254
}
255
}
256
this.setState({ selected_plan: plan });
257
}
258
259
public async update_managed_licenses(): Promise<void> {
260
// Update the license state in the frontend
261
const v = await getManagedLicenses();
262
const all_managed_license_ids = fromJS(v.map((x) => x.id)) as any;
263
264
const day_ago = server_days_ago(1);
265
const managed_license_ids = fromJS(
266
v
267
.filter((x) => x.expires == null || x.expires >= day_ago)
268
.map((x) => x.id)
269
) as any;
270
271
const x: { [license_id: string]: object } = {};
272
for (const license of v) {
273
x[license.id] = license;
274
}
275
const managed_licenses = fromJS(x) as any;
276
this.setState({
277
managed_licenses,
278
managed_license_ids,
279
all_managed_license_ids,
280
});
281
}
282
}
283
284
export const actions = redux.createActions("billing", BillingActions);
285
286