Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/store/usage-and-duration.tsx
5891 views
1
/*
2
* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Divider, Form, Input, Radio, Space } from "antd";
7
import { ReactNode } from "react";
8
9
import { COSTS } from "@cocalc/util/licenses/purchase/consts";
10
import { Subscription } from "@cocalc/util/licenses/purchase/types";
11
import { isAcademic, unreachable } from "@cocalc/util/misc";
12
import DateRange from "components/misc/date-range";
13
import useProfile from "lib/hooks/profile";
14
15
import type { LicenseSource } from "@cocalc/util/upgrades/shopping";
16
17
type Duration = "all" | "subscriptions" | "monthly" | "yearly" | "range";
18
19
interface Props {
20
showExplanations?: boolean;
21
form: any;
22
onChange: () => void;
23
disabled?: boolean;
24
showUsage?: boolean;
25
duration?: Duration;
26
discount?: boolean;
27
extraDuration?: ReactNode;
28
source: LicenseSource;
29
}
30
31
function getTimezoneFromDate(
32
date: Date,
33
format: "long" | "short" = "long",
34
): string {
35
return (
36
Intl.DateTimeFormat(undefined, {
37
timeZoneName: format,
38
})
39
.formatToParts(date)
40
.find((x) => x.type === "timeZoneName")?.value || ""
41
);
42
}
43
44
export function UsageAndDuration({
45
showExplanations = false,
46
form,
47
onChange,
48
disabled = false,
49
showUsage = true,
50
discount = true,
51
extraDuration,
52
source,
53
duration = "all",
54
}: Props) {
55
const profile = useProfile();
56
57
function renderUsageExplanation() {
58
if (!showExplanations) return;
59
const ac = (
60
<>Academic users receive a 40% discount off the standard price.</>
61
);
62
switch (source) {
63
case "site-license":
64
return (
65
<>
66
Will this license be used for academic or commercial purposes?
67
{ac}
68
</>
69
);
70
case "course":
71
return ac;
72
default:
73
unreachable(source);
74
}
75
}
76
77
function renderUsageItem() {
78
switch (source) {
79
case "site-license":
80
return (
81
<Radio.Group disabled={disabled}>
82
<Space direction="vertical" style={{ margin: "5px 0" }}>
83
<Radio value={"business"}>
84
Business - for commercial purposes
85
</Radio>
86
<Radio value={"academic"}>
87
Academic - students, teachers, academic researchers, non-profit
88
organizations and hobbyists (40% discount)
89
</Radio>
90
</Space>{" "}
91
</Radio.Group>
92
);
93
case "course":
94
return <>Academic</>;
95
96
default:
97
unreachable(source);
98
}
99
}
100
101
function renderUsage() {
102
if (!showUsage) return;
103
104
switch (source) {
105
case "course":
106
return (
107
<Form.Item
108
name="user"
109
initialValue="academic"
110
label={"Usage"}
111
extra={renderUsageExplanation()}
112
>
113
<Input type="hidden" value="academic" />
114
Academic
115
</Form.Item>
116
);
117
case "site-license":
118
return (
119
<Form.Item
120
name="user"
121
initialValue={
122
isAcademic(profile?.email_address) ? "academic" : "business"
123
}
124
label={"Usage"}
125
extra={renderUsageExplanation()}
126
>
127
{renderUsageItem()}
128
</Form.Item>
129
);
130
default:
131
unreachable(source);
132
}
133
}
134
135
function renderRangeSelector(getFieldValue) {
136
const period = getFieldValue("period");
137
138
// ensure range is valid even if we aren't going to render this range selector visibly:
139
// see https://github.com/sagemathinc/cocalc/issues/8461
140
let range = getFieldValue("range");
141
let invalidRange = range?.[0] == null || range?.[1] == null;
142
if (invalidRange) {
143
// Check if we're during initial load and URL has range parameters
144
// If so, don't override with default dates
145
const urlParams = new URLSearchParams(window.location.search);
146
const hasRangeInUrl = urlParams.has("range");
147
if (!hasRangeInUrl) {
148
const start = new Date();
149
const dayMs = 1000 * 60 * 60 * 24;
150
const daysDelta = source === "course" ? 4 * 30 : 30;
151
const end = new Date(start.valueOf() + dayMs * daysDelta);
152
range = [start, end];
153
form.setFieldsValue({ range });
154
onChange();
155
}
156
}
157
158
if (period !== "range") {
159
return;
160
}
161
162
let suffix;
163
try {
164
if (!invalidRange) {
165
// always make them actual dates. See
166
// https://github.com/sagemathinc/cocalc/issues/7173
167
// where this caused a crash when parsing the URL.
168
range[0] = new Date(range[0]);
169
range[1] = new Date(range[1]);
170
}
171
suffix =
172
range &&
173
range[0] &&
174
`(midnight to 11:59pm, ${getTimezoneFromDate(range[0], "long")})`;
175
} catch (err) {
176
invalidRange = true;
177
console.warn(`WARNING: issue parsing date ${range[0]}`);
178
suffix = undefined;
179
}
180
return (
181
<Form.Item
182
label={source === "course" ? "Course Dates" : "License Term"}
183
name="range"
184
rules={[{ required: true }]}
185
help={invalidRange ? "Please enter a valid license range." : ""}
186
validateStatus={invalidRange ? "error" : "success"}
187
style={{ paddingBottom: "30px" }}
188
extra={source === "course" ? renderDurationExplanation() : undefined}
189
>
190
<DateRange
191
key={
192
range ? `${range[0]?.getTime()}_${range[1]?.getTime()}` : "no-range"
193
}
194
disabled={disabled}
195
noPast
196
maxDaysInFuture={365 * 4}
197
style={{ marginTop: "5px" }}
198
initialValues={range}
199
onChange={(range) => {
200
form.setFieldsValue({ range });
201
onChange();
202
}}
203
suffix={suffix}
204
/>
205
</Form.Item>
206
);
207
}
208
209
function renderRange() {
210
return (
211
<Form.Item
212
noStyle
213
shouldUpdate={(prevValues, currentValues) =>
214
prevValues.period !== currentValues.period
215
}
216
>
217
{({ getFieldValue }) => renderRangeSelector(getFieldValue)}
218
</Form.Item>
219
);
220
}
221
222
function renderSubsDiscount(duration: Subscription) {
223
if (!discount) return;
224
const pct = Math.round(100 * (1 - COSTS.sub_discount[duration]));
225
return <b> (discount {pct}%)</b>;
226
}
227
228
function renderSubsOptions() {
229
if (duration === "all" || duration !== "range") {
230
return (
231
<>
232
{duration !== "yearly" && (
233
<Radio value={"monthly"}>
234
Monthly Subscription {renderSubsDiscount("monthly")}
235
</Radio>
236
)}
237
{duration !== "monthly" && (
238
<Radio value={"yearly"}>
239
Yearly Subscription {renderSubsDiscount("yearly")}
240
</Radio>
241
)}
242
</>
243
);
244
}
245
}
246
247
function renderRangeOption() {
248
if (duration === "all" || duration === "range") {
249
return <Radio value={"range"}>Specific Start and End Dates</Radio>;
250
}
251
}
252
253
function renderDurationExplanation() {
254
if (extraDuration) {
255
return extraDuration;
256
}
257
if (!showExplanations || !discount) return;
258
259
const tz = (
260
<i>
261
Licenses start and end at the indicated times in your local timezone.
262
</i>
263
);
264
265
switch (source) {
266
case "course":
267
return <>{tz}</>;
268
269
case "site-license":
270
return (
271
<>
272
You can buy a license either via a subscription or a single purchase
273
for specific dates. Once you purchase a license,{" "}
274
<b>
275
you can always edit it later, or cancel it for a prorated refund
276
</b>{" "}
277
as credit that you can use to purchase something else. Subscriptions
278
will be canceled at the end of the paid for period.{" "}
279
{duration == "range" && { tz }}
280
</>
281
);
282
default:
283
unreachable(source);
284
}
285
}
286
287
function renderPeriod() {
288
const init =
289
source === "course"
290
? "range"
291
: duration === "range"
292
? "range"
293
: "monthly";
294
295
switch (source) {
296
case "course":
297
return (
298
<Form.Item name="period" initialValue={init} hidden>
299
<Input type="hidden" value="range" />
300
</Form.Item>
301
);
302
303
case "site-license":
304
return (
305
<Form.Item
306
name="period"
307
initialValue={init}
308
label="Period"
309
extra={renderDurationExplanation()}
310
>
311
<Radio.Group disabled={disabled}>
312
<Space direction="vertical" style={{ margin: "5px 0" }}>
313
{renderSubsOptions()}
314
{renderRangeOption()}
315
</Space>
316
</Radio.Group>
317
</Form.Item>
318
);
319
320
default:
321
unreachable(source);
322
}
323
}
324
325
function renderDuration() {
326
return (
327
<>
328
<Form.Item name="range" hidden={true}>
329
<Input />
330
</Form.Item>
331
{renderPeriod()}
332
{renderRange()}
333
</>
334
);
335
}
336
337
return (
338
<>
339
<Divider plain>{showUsage ? "Usage and " : ""}Duration</Divider>
340
{renderUsage()}
341
{renderDuration()}
342
</>
343
);
344
}
345
346