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/next/components/store/usage-and-duration.tsx
Views: 687
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 { isAcademic } from "@cocalc/util/misc";
7
import { Subscription } from "@cocalc/util/licenses/purchase/types";
8
import { COSTS } from "@cocalc/util/licenses/purchase/consts";
9
import { Divider, Form, Input, Radio, Space } from "antd";
10
import DateRange from "components/misc/date-range";
11
import { ReactNode } from "react";
12
import useProfile from "lib/hooks/profile";
13
14
interface Props {
15
showExplanations?: boolean;
16
form: any;
17
onChange: () => void;
18
disabled?: boolean;
19
showUsage?: boolean;
20
duration?: "all" | "subscriptions" | "monthly" | "yearly" | "range";
21
discount?: boolean;
22
extraDuration?: ReactNode;
23
}
24
25
function getTimezoneFromDate(
26
date: Date,
27
format: "long" | "short" = "long",
28
): string {
29
return (
30
Intl.DateTimeFormat(undefined, {
31
timeZoneName: format,
32
})
33
.formatToParts(date)
34
.find((x) => x.type === "timeZoneName")?.value || ""
35
);
36
}
37
38
export function UsageAndDuration(props: Props) {
39
const {
40
showExplanations = false,
41
form,
42
onChange,
43
disabled = false,
44
showUsage = true,
45
duration = "all",
46
discount = true,
47
extraDuration,
48
} = props;
49
50
const profile = useProfile();
51
52
function renderUsage() {
53
if (!showUsage) return;
54
return (
55
<Form.Item
56
name="user"
57
initialValue={
58
isAcademic(profile?.email_address) ? "academic" : "business"
59
}
60
label={"Usage"}
61
extra={
62
showExplanations ? (
63
<>
64
Will this license be used for academic or commercial purposes?
65
Academic users receive a 40% discount off the standard price.
66
</>
67
) : undefined
68
}
69
>
70
<Radio.Group disabled={disabled}>
71
<Space direction="vertical" style={{ margin: "5px 0" }}>
72
<Radio value={"business"}>Business - for commercial purposes</Radio>
73
<Radio value={"academic"}>
74
Academic - students, teachers, academic researchers, non-profit
75
organizations and hobbyists (40% discount)
76
</Radio>
77
</Space>{" "}
78
</Radio.Group>
79
</Form.Item>
80
);
81
}
82
83
function renderRangeSelector(getFieldValue) {
84
const period = getFieldValue("period");
85
if (period !== "range") return;
86
const range = getFieldValue("range");
87
let invalidRange = range?.[0] == null || range?.[1] == null;
88
let suffix;
89
try {
90
if (!invalidRange) {
91
// always make them actual dates. See
92
// https://github.com/sagemathinc/cocalc/issues/7173
93
// where this caused a crash when parsing the URL.
94
range[0] = new Date(range[0]);
95
range[1] = new Date(range[1]);
96
}
97
suffix =
98
range && range[0] && `(midnight to 11:59pm, ${getTimezoneFromDate(range[0], "long")})`;
99
} catch (err) {
100
invalidRange = true;
101
console.warn(`WARNING: issue parsing date ${range[0]}`);
102
suffix = undefined;
103
}
104
return (
105
<Form.Item
106
label="License Term"
107
name="range"
108
rules={[{ required: true }]}
109
help={invalidRange ? "Please enter a valid license range." : ""}
110
validateStatus={invalidRange ? "error" : "success"}
111
style={{ paddingBottom: "30px" }}
112
>
113
<DateRange
114
disabled={disabled}
115
noPast
116
maxDaysInFuture={365 * 4}
117
style={{ marginTop: "5px" }}
118
initialValues={range}
119
onChange={(range) => {
120
form.setFieldsValue({ range });
121
onChange();
122
}}
123
suffix={suffix}
124
/>
125
</Form.Item>
126
);
127
}
128
129
function renderRange() {
130
return (
131
<Form.Item
132
noStyle
133
shouldUpdate={(prevValues, currentValues) =>
134
prevValues.period !== currentValues.period
135
}
136
>
137
{({ getFieldValue }) => renderRangeSelector(getFieldValue)}
138
</Form.Item>
139
);
140
}
141
142
function renderSubsDiscount(duration: Subscription) {
143
if (!discount) return;
144
const pct = Math.round(100 * (1 - COSTS.sub_discount[duration]));
145
return <b> (discount {pct}%)</b>;
146
}
147
148
function renderSubsOptions() {
149
if (duration === "all" || duration !== "range") {
150
return (
151
<>
152
{duration !== "yearly" && (
153
<Radio value={"monthly"}>
154
Monthly Subscription {renderSubsDiscount("monthly")}
155
</Radio>
156
)}
157
{duration !== "monthly" && (
158
<Radio value={"yearly"}>
159
Yearly Subscription {renderSubsDiscount("yearly")}
160
</Radio>
161
)}
162
</>
163
);
164
}
165
}
166
167
function renderRangeOption() {
168
if (duration === "all" || duration === "range") {
169
return <Radio value={"range"}>Specific Start and End Dates</Radio>;
170
}
171
}
172
173
function renderDurationExplanation() {
174
if (extraDuration) {
175
return extraDuration;
176
}
177
if (!showExplanations || !discount) return;
178
return (
179
<>
180
You can buy a license either via a subscription or a single purchase for
181
specific dates. Once you purchase a license,{" "}
182
<b>you can always edit it later, or cancel it for a prorated refund</b>{" "}
183
as credit that you can use to purchase something else. Subscriptions
184
will be cancelled at the end of the paid for period.{" "}
185
{duration == "range" && (
186
<i>
187
Licenses start and end at the indicated times in your local
188
timezone.
189
</i>
190
)}
191
</>
192
);
193
}
194
195
function renderDuration() {
196
const init = duration === "range" ? "range" : "monthly";
197
return (
198
<>
199
<Form.Item name="range" hidden={true}>
200
<Input />
201
</Form.Item>
202
<Form.Item
203
name="period"
204
initialValue={init}
205
label="Period"
206
extra={renderDurationExplanation()}
207
>
208
<Radio.Group disabled={disabled}>
209
<Space direction="vertical" style={{ margin: "5px 0" }}>
210
{renderSubsOptions()}
211
{renderRangeOption()}
212
</Space>
213
</Radio.Group>
214
</Form.Item>
215
216
{renderRange()}
217
</>
218
);
219
}
220
221
return (
222
<>
223
<Divider plain>{showUsage ? "Usage and " : ""}Duration</Divider>
224
{renderUsage()}
225
{renderDuration()}
226
</>
227
);
228
}
229
230