Path: blob/master/src/packages/next/components/store/usage-and-duration.tsx
5891 views
/*1* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Divider, Form, Input, Radio, Space } from "antd";6import { ReactNode } from "react";78import { COSTS } from "@cocalc/util/licenses/purchase/consts";9import { Subscription } from "@cocalc/util/licenses/purchase/types";10import { isAcademic, unreachable } from "@cocalc/util/misc";11import DateRange from "components/misc/date-range";12import useProfile from "lib/hooks/profile";1314import type { LicenseSource } from "@cocalc/util/upgrades/shopping";1516type Duration = "all" | "subscriptions" | "monthly" | "yearly" | "range";1718interface Props {19showExplanations?: boolean;20form: any;21onChange: () => void;22disabled?: boolean;23showUsage?: boolean;24duration?: Duration;25discount?: boolean;26extraDuration?: ReactNode;27source: LicenseSource;28}2930function getTimezoneFromDate(31date: Date,32format: "long" | "short" = "long",33): string {34return (35Intl.DateTimeFormat(undefined, {36timeZoneName: format,37})38.formatToParts(date)39.find((x) => x.type === "timeZoneName")?.value || ""40);41}4243export function UsageAndDuration({44showExplanations = false,45form,46onChange,47disabled = false,48showUsage = true,49discount = true,50extraDuration,51source,52duration = "all",53}: Props) {54const profile = useProfile();5556function renderUsageExplanation() {57if (!showExplanations) return;58const ac = (59<>Academic users receive a 40% discount off the standard price.</>60);61switch (source) {62case "site-license":63return (64<>65Will this license be used for academic or commercial purposes?66{ac}67</>68);69case "course":70return ac;71default:72unreachable(source);73}74}7576function renderUsageItem() {77switch (source) {78case "site-license":79return (80<Radio.Group disabled={disabled}>81<Space direction="vertical" style={{ margin: "5px 0" }}>82<Radio value={"business"}>83Business - for commercial purposes84</Radio>85<Radio value={"academic"}>86Academic - students, teachers, academic researchers, non-profit87organizations and hobbyists (40% discount)88</Radio>89</Space>{" "}90</Radio.Group>91);92case "course":93return <>Academic</>;9495default:96unreachable(source);97}98}99100function renderUsage() {101if (!showUsage) return;102103switch (source) {104case "course":105return (106<Form.Item107name="user"108initialValue="academic"109label={"Usage"}110extra={renderUsageExplanation()}111>112<Input type="hidden" value="academic" />113Academic114</Form.Item>115);116case "site-license":117return (118<Form.Item119name="user"120initialValue={121isAcademic(profile?.email_address) ? "academic" : "business"122}123label={"Usage"}124extra={renderUsageExplanation()}125>126{renderUsageItem()}127</Form.Item>128);129default:130unreachable(source);131}132}133134function renderRangeSelector(getFieldValue) {135const period = getFieldValue("period");136137// ensure range is valid even if we aren't going to render this range selector visibly:138// see https://github.com/sagemathinc/cocalc/issues/8461139let range = getFieldValue("range");140let invalidRange = range?.[0] == null || range?.[1] == null;141if (invalidRange) {142// Check if we're during initial load and URL has range parameters143// If so, don't override with default dates144const urlParams = new URLSearchParams(window.location.search);145const hasRangeInUrl = urlParams.has("range");146if (!hasRangeInUrl) {147const start = new Date();148const dayMs = 1000 * 60 * 60 * 24;149const daysDelta = source === "course" ? 4 * 30 : 30;150const end = new Date(start.valueOf() + dayMs * daysDelta);151range = [start, end];152form.setFieldsValue({ range });153onChange();154}155}156157if (period !== "range") {158return;159}160161let suffix;162try {163if (!invalidRange) {164// always make them actual dates. See165// https://github.com/sagemathinc/cocalc/issues/7173166// where this caused a crash when parsing the URL.167range[0] = new Date(range[0]);168range[1] = new Date(range[1]);169}170suffix =171range &&172range[0] &&173`(midnight to 11:59pm, ${getTimezoneFromDate(range[0], "long")})`;174} catch (err) {175invalidRange = true;176console.warn(`WARNING: issue parsing date ${range[0]}`);177suffix = undefined;178}179return (180<Form.Item181label={source === "course" ? "Course Dates" : "License Term"}182name="range"183rules={[{ required: true }]}184help={invalidRange ? "Please enter a valid license range." : ""}185validateStatus={invalidRange ? "error" : "success"}186style={{ paddingBottom: "30px" }}187extra={source === "course" ? renderDurationExplanation() : undefined}188>189<DateRange190key={191range ? `${range[0]?.getTime()}_${range[1]?.getTime()}` : "no-range"192}193disabled={disabled}194noPast195maxDaysInFuture={365 * 4}196style={{ marginTop: "5px" }}197initialValues={range}198onChange={(range) => {199form.setFieldsValue({ range });200onChange();201}}202suffix={suffix}203/>204</Form.Item>205);206}207208function renderRange() {209return (210<Form.Item211noStyle212shouldUpdate={(prevValues, currentValues) =>213prevValues.period !== currentValues.period214}215>216{({ getFieldValue }) => renderRangeSelector(getFieldValue)}217</Form.Item>218);219}220221function renderSubsDiscount(duration: Subscription) {222if (!discount) return;223const pct = Math.round(100 * (1 - COSTS.sub_discount[duration]));224return <b> (discount {pct}%)</b>;225}226227function renderSubsOptions() {228if (duration === "all" || duration !== "range") {229return (230<>231{duration !== "yearly" && (232<Radio value={"monthly"}>233Monthly Subscription {renderSubsDiscount("monthly")}234</Radio>235)}236{duration !== "monthly" && (237<Radio value={"yearly"}>238Yearly Subscription {renderSubsDiscount("yearly")}239</Radio>240)}241</>242);243}244}245246function renderRangeOption() {247if (duration === "all" || duration === "range") {248return <Radio value={"range"}>Specific Start and End Dates</Radio>;249}250}251252function renderDurationExplanation() {253if (extraDuration) {254return extraDuration;255}256if (!showExplanations || !discount) return;257258const tz = (259<i>260Licenses start and end at the indicated times in your local timezone.261</i>262);263264switch (source) {265case "course":266return <>{tz}</>;267268case "site-license":269return (270<>271You can buy a license either via a subscription or a single purchase272for specific dates. Once you purchase a license,{" "}273<b>274you can always edit it later, or cancel it for a prorated refund275</b>{" "}276as credit that you can use to purchase something else. Subscriptions277will be canceled at the end of the paid for period.{" "}278{duration == "range" && { tz }}279</>280);281default:282unreachable(source);283}284}285286function renderPeriod() {287const init =288source === "course"289? "range"290: duration === "range"291? "range"292: "monthly";293294switch (source) {295case "course":296return (297<Form.Item name="period" initialValue={init} hidden>298<Input type="hidden" value="range" />299</Form.Item>300);301302case "site-license":303return (304<Form.Item305name="period"306initialValue={init}307label="Period"308extra={renderDurationExplanation()}309>310<Radio.Group disabled={disabled}>311<Space direction="vertical" style={{ margin: "5px 0" }}>312{renderSubsOptions()}313{renderRangeOption()}314</Space>315</Radio.Group>316</Form.Item>317);318319default:320unreachable(source);321}322}323324function renderDuration() {325return (326<>327<Form.Item name="range" hidden={true}>328<Input />329</Form.Item>330{renderPeriod()}331{renderRange()}332</>333);334}335336return (337<>338<Divider plain>{showUsage ? "Usage and " : ""}Duration</Divider>339{renderUsage()}340{renderDuration()}341</>342);343}344345346