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
1450 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 { 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") {
86
return;
87
}
88
let range = getFieldValue("range");
89
let invalidRange = range?.[0] == null || range?.[1] == null;
90
if (invalidRange) {
91
const start = new Date();
92
const end = new Date(start.valueOf() + 1000 * 60 * 60 * 24 * 30);
93
range = [start, end];
94
form.setFieldsValue({ range });
95
onChange();
96
}
97
let suffix;
98
try {
99
if (!invalidRange) {
100
// always make them actual dates. See
101
// https://github.com/sagemathinc/cocalc/issues/7173
102
// where this caused a crash when parsing the URL.
103
range[0] = new Date(range[0]);
104
range[1] = new Date(range[1]);
105
}
106
suffix =
107
range &&
108
range[0] &&
109
`(midnight to 11:59pm, ${getTimezoneFromDate(range[0], "long")})`;
110
} catch (err) {
111
invalidRange = true;
112
console.warn(`WARNING: issue parsing date ${range[0]}`);
113
suffix = undefined;
114
}
115
return (
116
<Form.Item
117
label="License Term"
118
name="range"
119
rules={[{ required: true }]}
120
help={invalidRange ? "Please enter a valid license range." : ""}
121
validateStatus={invalidRange ? "error" : "success"}
122
style={{ paddingBottom: "30px" }}
123
>
124
<DateRange
125
disabled={disabled}
126
noPast
127
maxDaysInFuture={365 * 4}
128
style={{ marginTop: "5px" }}
129
initialValues={range}
130
onChange={(range) => {
131
form.setFieldsValue({ range });
132
onChange();
133
}}
134
suffix={suffix}
135
/>
136
</Form.Item>
137
);
138
}
139
140
function renderRange() {
141
return (
142
<Form.Item
143
noStyle
144
shouldUpdate={(prevValues, currentValues) =>
145
prevValues.period !== currentValues.period
146
}
147
>
148
{({ getFieldValue }) => renderRangeSelector(getFieldValue)}
149
</Form.Item>
150
);
151
}
152
153
function renderSubsDiscount(duration: Subscription) {
154
if (!discount) return;
155
const pct = Math.round(100 * (1 - COSTS.sub_discount[duration]));
156
return <b> (discount {pct}%)</b>;
157
}
158
159
function renderSubsOptions() {
160
if (duration === "all" || duration !== "range") {
161
return (
162
<>
163
{duration !== "yearly" && (
164
<Radio value={"monthly"}>
165
Monthly Subscription {renderSubsDiscount("monthly")}
166
</Radio>
167
)}
168
{duration !== "monthly" && (
169
<Radio value={"yearly"}>
170
Yearly Subscription {renderSubsDiscount("yearly")}
171
</Radio>
172
)}
173
</>
174
);
175
}
176
}
177
178
function renderRangeOption() {
179
if (duration === "all" || duration === "range") {
180
return <Radio value={"range"}>Specific Start and End Dates</Radio>;
181
}
182
}
183
184
function renderDurationExplanation() {
185
if (extraDuration) {
186
return extraDuration;
187
}
188
if (!showExplanations || !discount) return;
189
return (
190
<>
191
You can buy a license either via a subscription or a single purchase for
192
specific dates. Once you purchase a license,{" "}
193
<b>you can always edit it later, or cancel it for a prorated refund</b>{" "}
194
as credit that you can use to purchase something else. Subscriptions
195
will be canceled at the end of the paid for period.{" "}
196
{duration == "range" && (
197
<i>
198
Licenses start and end at the indicated times in your local
199
timezone.
200
</i>
201
)}
202
</>
203
);
204
}
205
206
function renderDuration() {
207
const init = duration === "range" ? "range" : "monthly";
208
return (
209
<>
210
<Form.Item name="range" hidden={true}>
211
<Input />
212
</Form.Item>
213
<Form.Item
214
name="period"
215
initialValue={init}
216
label="Period"
217
extra={renderDurationExplanation()}
218
>
219
<Radio.Group disabled={disabled}>
220
<Space direction="vertical" style={{ margin: "5px 0" }}>
221
{renderSubsOptions()}
222
{renderRangeOption()}
223
</Space>
224
</Radio.Group>
225
</Form.Item>
226
227
{renderRange()}
228
</>
229
);
230
}
231
232
return (
233
<>
234
<Divider plain>{showUsage ? "Usage and " : ""}Duration</Divider>
235
{renderUsage()}
236
{renderDuration()}
237
</>
238
);
239
}
240
241