Path: blob/master/src/packages/next/components/store/usage-and-duration.tsx
1450 views
/*1* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { isAcademic } from "@cocalc/util/misc";6import { Subscription } from "@cocalc/util/licenses/purchase/types";7import { COSTS } from "@cocalc/util/licenses/purchase/consts";8import { Divider, Form, Input, Radio, Space } from "antd";9import DateRange from "components/misc/date-range";10import { ReactNode } from "react";11import useProfile from "lib/hooks/profile";1213interface Props {14showExplanations?: boolean;15form: any;16onChange: () => void;17disabled?: boolean;18showUsage?: boolean;19duration?: "all" | "subscriptions" | "monthly" | "yearly" | "range";20discount?: boolean;21extraDuration?: ReactNode;22}2324function getTimezoneFromDate(25date: Date,26format: "long" | "short" = "long",27): string {28return (29Intl.DateTimeFormat(undefined, {30timeZoneName: format,31})32.formatToParts(date)33.find((x) => x.type === "timeZoneName")?.value || ""34);35}3637export function UsageAndDuration(props: Props) {38const {39showExplanations = false,40form,41onChange,42disabled = false,43showUsage = true,44duration = "all",45discount = true,46extraDuration,47} = props;4849const profile = useProfile();5051function renderUsage() {52if (!showUsage) return;53return (54<Form.Item55name="user"56initialValue={57isAcademic(profile?.email_address) ? "academic" : "business"58}59label={"Usage"}60extra={61showExplanations ? (62<>63Will this license be used for academic or commercial purposes?64Academic users receive a 40% discount off the standard price.65</>66) : undefined67}68>69<Radio.Group disabled={disabled}>70<Space direction="vertical" style={{ margin: "5px 0" }}>71<Radio value={"business"}>Business - for commercial purposes</Radio>72<Radio value={"academic"}>73Academic - students, teachers, academic researchers, non-profit74organizations and hobbyists (40% discount)75</Radio>76</Space>{" "}77</Radio.Group>78</Form.Item>79);80}8182function renderRangeSelector(getFieldValue) {83const period = getFieldValue("period");84if (period !== "range") {85return;86}87let range = getFieldValue("range");88let invalidRange = range?.[0] == null || range?.[1] == null;89if (invalidRange) {90const start = new Date();91const end = new Date(start.valueOf() + 1000 * 60 * 60 * 24 * 30);92range = [start, end];93form.setFieldsValue({ range });94onChange();95}96let suffix;97try {98if (!invalidRange) {99// always make them actual dates. See100// https://github.com/sagemathinc/cocalc/issues/7173101// where this caused a crash when parsing the URL.102range[0] = new Date(range[0]);103range[1] = new Date(range[1]);104}105suffix =106range &&107range[0] &&108`(midnight to 11:59pm, ${getTimezoneFromDate(range[0], "long")})`;109} catch (err) {110invalidRange = true;111console.warn(`WARNING: issue parsing date ${range[0]}`);112suffix = undefined;113}114return (115<Form.Item116label="License Term"117name="range"118rules={[{ required: true }]}119help={invalidRange ? "Please enter a valid license range." : ""}120validateStatus={invalidRange ? "error" : "success"}121style={{ paddingBottom: "30px" }}122>123<DateRange124disabled={disabled}125noPast126maxDaysInFuture={365 * 4}127style={{ marginTop: "5px" }}128initialValues={range}129onChange={(range) => {130form.setFieldsValue({ range });131onChange();132}}133suffix={suffix}134/>135</Form.Item>136);137}138139function renderRange() {140return (141<Form.Item142noStyle143shouldUpdate={(prevValues, currentValues) =>144prevValues.period !== currentValues.period145}146>147{({ getFieldValue }) => renderRangeSelector(getFieldValue)}148</Form.Item>149);150}151152function renderSubsDiscount(duration: Subscription) {153if (!discount) return;154const pct = Math.round(100 * (1 - COSTS.sub_discount[duration]));155return <b> (discount {pct}%)</b>;156}157158function renderSubsOptions() {159if (duration === "all" || duration !== "range") {160return (161<>162{duration !== "yearly" && (163<Radio value={"monthly"}>164Monthly Subscription {renderSubsDiscount("monthly")}165</Radio>166)}167{duration !== "monthly" && (168<Radio value={"yearly"}>169Yearly Subscription {renderSubsDiscount("yearly")}170</Radio>171)}172</>173);174}175}176177function renderRangeOption() {178if (duration === "all" || duration === "range") {179return <Radio value={"range"}>Specific Start and End Dates</Radio>;180}181}182183function renderDurationExplanation() {184if (extraDuration) {185return extraDuration;186}187if (!showExplanations || !discount) return;188return (189<>190You can buy a license either via a subscription or a single purchase for191specific dates. Once you purchase a license,{" "}192<b>you can always edit it later, or cancel it for a prorated refund</b>{" "}193as credit that you can use to purchase something else. Subscriptions194will be canceled at the end of the paid for period.{" "}195{duration == "range" && (196<i>197Licenses start and end at the indicated times in your local198timezone.199</i>200)}201</>202);203}204205function renderDuration() {206const init = duration === "range" ? "range" : "monthly";207return (208<>209<Form.Item name="range" hidden={true}>210<Input />211</Form.Item>212<Form.Item213name="period"214initialValue={init}215label="Period"216extra={renderDurationExplanation()}217>218<Radio.Group disabled={disabled}>219<Space direction="vertical" style={{ margin: "5px 0" }}>220{renderSubsOptions()}221{renderRangeOption()}222</Space>223</Radio.Group>224</Form.Item>225226{renderRange()}227</>228);229}230231return (232<>233<Divider plain>{showUsage ? "Usage and " : ""}Duration</Divider>234{renderUsage()}235{renderDuration()}236</>237);238}239240241