Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/account/account-page.tsx
1503 views
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
The account page. This is what you see when you
8
click "Account" in the upper right. It has tabs
9
for different account related information
10
and configuration.
11
*/
12
13
// cSpell:ignore payg
14
15
import { Flex, Menu, Space } from "antd";
16
import { useEffect } from "react";
17
import { useIntl } from "react-intl";
18
import { SignOut } from "@cocalc/frontend/account/sign-out";
19
import {
20
React,
21
redux,
22
useIsMountedRef,
23
useTypedRedux,
24
useWindowDimensions,
25
} from "@cocalc/frontend/app-framework";
26
import { Icon, Loading } from "@cocalc/frontend/components";
27
import { cloudFilesystemsEnabled } from "@cocalc/frontend/compute";
28
import CloudFilesystems from "@cocalc/frontend/compute/cloud-filesystem/cloud-filesystems";
29
import { Footer } from "@cocalc/frontend/customize";
30
import { appBasePath } from "@cocalc/frontend/customize/app-base-path";
31
import { labels } from "@cocalc/frontend/i18n";
32
import BalanceButton from "@cocalc/frontend/purchases/balance-button";
33
import PayAsYouGoPage from "@cocalc/frontend/purchases/payg-page";
34
import PaymentMethodsPage from "@cocalc/frontend/purchases/payment-methods-page";
35
import PaymentsPage from "@cocalc/frontend/purchases/payments-page";
36
import PurchasesPage from "@cocalc/frontend/purchases/purchases-page";
37
import StatementsPage from "@cocalc/frontend/purchases/statements-page";
38
import SubscriptionsPage from "@cocalc/frontend/purchases/subscriptions-page";
39
import { SupportTickets } from "@cocalc/frontend/support";
40
import {
41
KUCALC_COCALC_COM,
42
KUCALC_ON_PREMISES,
43
} from "@cocalc/util/db-schema/site-defaults";
44
import { AccountPreferences } from "./account-preferences";
45
import { I18NSelector } from "./i18n-selector";
46
import { LicensesPage } from "./licenses/licenses-page";
47
import { PublicPaths } from "./public-paths/public-paths";
48
import { UpgradesPage } from "./upgrades/upgrades-page";
49
50
// give up on trying to load account info and redirect to landing page.
51
// Do NOT make too short, since loading account info might takes ~10 seconds, e,g., due
52
// to slow network or some backend failure that times and retires involvin
53
// changefeeds.
54
const LOAD_ACCOUNT_INFO_TIMEOUT = 15_000;
55
56
export const AccountPage: React.FC = () => {
57
const intl = useIntl();
58
59
const { width: windowWidth } = useWindowDimensions();
60
const isWide = windowWidth > 800;
61
62
const active_page = useTypedRedux("account", "active_page") ?? "account";
63
const is_logged_in = useTypedRedux("account", "is_logged_in");
64
const account_id = useTypedRedux("account", "account_id");
65
const is_anonymous = useTypedRedux("account", "is_anonymous");
66
const kucalc = useTypedRedux("customize", "kucalc");
67
const is_commercial = useTypedRedux("customize", "is_commercial");
68
const get_api_key = useTypedRedux("page", "get_api_key");
69
70
function handle_select(key: string): void {
71
switch (key) {
72
case "billing":
73
redux.getActions("billing").update_customer();
74
break;
75
case "support":
76
break;
77
case "signout":
78
return;
79
}
80
redux.getActions("account").set_active_tab(key);
81
redux.getActions("account").push_state(`/${key}`);
82
}
83
84
function getTabs(): any[] {
85
const items: any[] = [
86
{
87
key: "account",
88
label: (
89
<span>
90
<Icon name="address-card" />{" "}
91
{intl.formatMessage(labels.preferences)}
92
</span>
93
),
94
children: (active_page == null || active_page === "account") && (
95
<AccountPreferences />
96
),
97
},
98
];
99
// adds a few conditional tabs
100
if (is_anonymous) {
101
// None of the rest make any sense for a temporary anonymous account.
102
return items;
103
}
104
items.push({ type: "divider" });
105
106
if (is_commercial) {
107
items.push({
108
key: "subscriptions",
109
label: (
110
<span>
111
<Icon name="calendar" /> {intl.formatMessage(labels.subscriptions)}
112
</span>
113
),
114
children: active_page === "subscriptions" && <SubscriptionsPage />,
115
});
116
items.push({
117
key: "licenses",
118
label: (
119
<span>
120
<Icon name="key" /> {intl.formatMessage(labels.licenses)}
121
</span>
122
),
123
children: active_page === "licenses" && <LicensesPage />,
124
});
125
items.push({
126
key: "payg",
127
label: (
128
<span>
129
<Icon name="line-chart" /> Pay As You Go
130
</span>
131
),
132
children: active_page === "payg" && <PayAsYouGoPage />,
133
});
134
if (is_commercial && kucalc === KUCALC_COCALC_COM) {
135
// these have been deprecated for ~ 5 years, but some customers still have them.
136
items.push({
137
key: "upgrades",
138
label: (
139
<span>
140
<Icon name="arrow-circle-up" />{" "}
141
{intl.formatMessage(labels.upgrades)}
142
</span>
143
),
144
children: active_page === "upgrades" && <UpgradesPage />,
145
});
146
}
147
items.push({ type: "divider" });
148
items.push({
149
key: "purchases",
150
label: (
151
<span>
152
<Icon name="money-check" /> {intl.formatMessage(labels.purchases)}
153
</span>
154
),
155
children: active_page === "purchases" && <PurchasesPage />,
156
});
157
items.push({
158
key: "payments",
159
label: (
160
<span>
161
<Icon name="credit-card" /> Payments
162
</span>
163
),
164
children: active_page === "payments" && <PaymentsPage />,
165
});
166
items.push({
167
key: "payment-methods",
168
label: (
169
<span>
170
<Icon name="credit-card" /> Payment Methods
171
</span>
172
),
173
children: active_page === "payment-methods" && <PaymentMethodsPage />,
174
});
175
items.push({
176
key: "statements",
177
label: (
178
<span>
179
<Icon name="calendar-week" />{" "}
180
{intl.formatMessage(labels.statements)}
181
</span>
182
),
183
children: active_page === "statements" && <StatementsPage />,
184
});
185
items.push({ type: "divider" });
186
}
187
188
items.push({
189
key: "public-files",
190
label: (
191
<span>
192
<Icon name="share-square" />{" "}
193
{intl.formatMessage(labels.published_files)}
194
</span>
195
),
196
children: active_page === "public-files" && <PublicPaths />,
197
});
198
if (cloudFilesystemsEnabled()) {
199
items.push({
200
key: "cloud-filesystems",
201
label: (
202
<>
203
<Icon name="server" style={{ marginRight: "5px" }} />
204
{intl.formatMessage(labels.cloud_file_system)}
205
</>
206
),
207
children: <CloudFilesystems noTitle />,
208
});
209
}
210
211
if (
212
kucalc === KUCALC_COCALC_COM ||
213
kucalc === KUCALC_ON_PREMISES ||
214
is_commercial
215
) {
216
}
217
218
if (is_commercial) {
219
items.push({ type: "divider" });
220
items.push({
221
key: "support",
222
label: (
223
<span>
224
<Icon name="medkit" /> {intl.formatMessage(labels.support)}
225
</span>
226
),
227
children: active_page === "support" && <SupportTickets />,
228
});
229
}
230
231
return items;
232
}
233
234
function renderExtraContent() {
235
return (
236
<Space>
237
{is_commercial ? <BalanceButton /> : undefined}
238
<I18NSelector isWide={isWide} />
239
<SignOut everywhere={false} highlight={true} narrow={!isWide} />
240
</Space>
241
);
242
}
243
244
function render_logged_in_view(): React.JSX.Element {
245
if (!account_id) {
246
return (
247
<div style={{ textAlign: "center", paddingTop: "15px" }}>
248
<Loading theme={"medium"} />
249
</div>
250
);
251
}
252
if (is_anonymous) {
253
return (
254
<div
255
style={{
256
margin: "15px 0 0 0",
257
padding: "0 10% 0 10%",
258
overflow: "auto",
259
}}
260
>
261
<AccountPreferences />
262
</div>
263
);
264
}
265
266
const tabs = getTabs();
267
268
// NOTE: This is a bit weird, since I rewrote it form antd Tabs
269
// to an antd Menu, but didn't change any of the above.
270
// Antd Menu has a notion of children and submenus, which we *could*
271
// use but aren't using yet.
272
const children = {};
273
const titles = {};
274
for (const tab of tabs) {
275
if (tab.type == "divider") {
276
continue;
277
}
278
children[tab.key] = tab.children;
279
titles[tab.key] = tab.label;
280
delete tab.children;
281
}
282
283
return (
284
<div className="smc-vfill" style={{ flexDirection: "row" }}>
285
<div
286
style={{
287
background: "#00000005",
288
borderRight: "1px solid rgba(5, 5, 5, 0.06)",
289
}}
290
>
291
<div
292
style={{
293
textAlign: "center",
294
margin: "15px 0",
295
fontSize: "11pt",
296
}}
297
>
298
<b>Account Configuration</b>
299
</div>
300
<Menu
301
onClick={(e) => {
302
handle_select(e.key);
303
}}
304
selectedKeys={[active_page]}
305
mode="vertical"
306
items={tabs}
307
style={{ width: 183, background: "#00000005", height: "100vh" }}
308
/>
309
</div>
310
<div
311
className="smc-vfill"
312
style={{
313
overflow: "auto",
314
paddingLeft: "15px",
315
paddingRight: "15px",
316
}}
317
>
318
<Flex style={{ marginTop: "5px" }}>
319
<h2>{titles[active_page]}</h2>
320
<div style={{ flex: 1 }} />
321
{renderExtraContent()}
322
</Flex>
323
{children[active_page]}
324
<Footer />
325
</div>
326
</div>
327
);
328
}
329
330
return (
331
<div className="smc-vfill">
332
{is_logged_in && !get_api_key ? (
333
render_logged_in_view()
334
) : (
335
<RedirectToNextApp />
336
)}
337
</div>
338
);
339
};
340
341
declare var DEBUG;
342
343
function RedirectToNextApp({}) {
344
const isMountedRef = useIsMountedRef();
345
346
useEffect(() => {
347
const f = () => {
348
if (isMountedRef.current && !DEBUG) {
349
// didn't get signed in so go to landing page
350
window.location.href = appBasePath;
351
}
352
};
353
setTimeout(f, LOAD_ACCOUNT_INFO_TIMEOUT);
354
}, []);
355
356
return <Loading theme="medium" />;
357
}
358
359