Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/components/billing/payment-methods.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/* Show payment methods.67TODO: we are only showing the credit card payment sources at present.8There are other types of sources, e.g., "ACH credit transfer".910In the *near* future we will support more payment methods!11*/1213import { Alert, Button, Divider, Popconfirm, Table } from "antd";14import { useMemo, useState } from "react";15import { Icon } from "@cocalc/frontend/components/icon";16import { cmp } from "@cocalc/util/misc";17import { Title } from "components/misc";18import A from "components/misc/A";19import Loading from "components/share/loading";20import apiPost from "lib/api/post";21import useAPI from "lib/hooks/api";22import useIsMounted from "lib/hooks/mounted";23import SiteName from "components/share/site-name";2425function PaymentSourceActions({ onChange, default_source, brand, last4, id }) {26const isMounted = useIsMounted();27const [error, setError] = useState<string>("");28return (29<div>30{error && (31<Alert type="error" message={error} style={{ marginBottom: "5px" }} />32)}33{default_source ? (34<Popconfirm35placement="topLeft"36showCancel={false}37title={38<div style={{ width: "400px" }}>39The default payment method is the{" "}40<b>41{brand} card ending in ...42{last4}43</b>44. It will be used by default for subscriptions and new purchases.45</div>46}47okText="OK"48>49<Button50disabled51type={"primary"}52style={{ marginRight: "5px", marginBottom: "5px" }}53>54Default55</Button>56</Popconfirm>57) : (58<Popconfirm59placement="topLeft"60title={61<div style={{ width: "400px" }}>62Do you want to set the{" "}63<b>64{brand} card ending in ...{last4}65</b>{" "}66to be the default for subscriptions and new purchases?67</div>68}69onConfirm={async () => {70try {71setError("");72await apiPost("/billing/set-default-source", {73default_source: id,74});75if (!isMounted.current) return;76onChange?.();77} catch (err) {78if (!isMounted.current) return;79setError(err.message);80}81}}82okText="Yes"83cancelText="No"84>85<Button86disabled87type={"dashed"}88style={{ marginRight: "5px", marginBottom: "5px" }}89>90Default91</Button>92</Popconfirm>93)}94<Popconfirm95placement="topLeft"96title={97<div style={{ width: "400px" }}>98Do you want to delete the{" "}99<b>100{brand} card ending in ...{last4}101</b>102? It will no longer be used for subscriptions and you will have to103enter it again to use it to make a purchase.104</div>105}106onConfirm={async () => {107try {108setError("");109await apiPost("/billing/delete-payment-method", { id });110onChange?.();111} catch (err) {112setError(err.message);113}114}}115okText="Yes, delete this card"116cancelText="Cancel"117>118<Button type="dashed">119<Icon name="trash" /> Delete120</Button>121</Popconfirm>122</div>123);124}125126const columns = (onChange) => [127{128responsive: ["xs"],129title: "Card",130render: (_, card) => (131<div>132<CreditCard {...card} />133<PaymentSourceActions {...card} onChange={onChange} />134</div>135),136},137{138responsive: ["sm"],139title: "Type",140dataIndex: "brand",141render: (_, card) => <Brand {...card} />,142},143{144responsive: ["sm"],145title: "Number",146dataIndex: "last4",147render: (_, card) => <Number {...card} />,148},149{150responsive: ["sm"],151title: "Expiration Date",152align: "center" as "center",153render: (_, card) => <ExpirationDate {...card} />,154},155{156responsive: ["sm"],157title: "Country",158dataIndex: "country",159align: "center" as "center",160},161{162responsive: ["sm"],163title: "Postal Code",164dataIndex: "address_zip",165align: "center" as "center",166},167{168responsive: ["sm"],169title: "",170render: (_, card) => <PaymentSourceActions {...card} onChange={onChange} />,171},172];173174export default function PaymentMethods() {175const { result, error, call } = useAPI("billing/get-customer");176177const cols: any = useMemo(() => {178return columns(call);179}, [call]);180181const cards = useMemo(() => {182if (result?.sources == null) return [];183// set default so can use in table184const { default_source } = result;185const cards: (CardProps & { id: string; default_source: boolean })[] = [];186for (const row of result.sources.data) {187if (row.id == default_source) {188row.default_source = true;189}190if (row.id.startsWith("card_")) {191cards.push(row);192}193}194// sort by data rather than what comes back, so changing195// default stays stable (since moving is confusing).196cards.sort((x, y) => cmp(x.id, y.id));197198return cards;199}, [result?.sources]);200201if (error) {202return <Alert type="error" message={error} />;203}204if (!result) {205return <Loading center />;206}207208return (209<div>210<Title level={2}>Credit Cards ({cards.length})</Title>211<SiteName /> used to use a credit card on file for automatic subscription212payments. We now use a new more flexible and powerful automatic payments213system that works with far more payment providers. To configure it,{" "}214<A href="/settings/subscriptions" external>215click on <Button size="small">Enable Automatic Payments...</Button> in216subscription settings...217</A>218{cards.length > 0 && (219<div>220<Divider>Legacy Cards</Divider>221<>222These are the credit cards that you have currently setup.{" "}223<b>Don't worry -- your default card will continue to be used</b> for224now if you don't configure automatic payments as explained above.225</>226<Table227columns={cols}228dataSource={cards}229rowKey={"id"}230style={{ marginTop: "15px", overflowX: "auto" }}231pagination={{ hideOnSinglePage: true, pageSize: 100 }}232/>233</div>234)}235</div>236);237}238239function Brand({ brand }) {240return (241<>242{brand?.includes(" ") ? (243""244) : (245<Icon name={`cc-${brand?.toLowerCase()}` as any} />246)}{" "}247{brand}248</>249);250}251252function Number({ last4 }) {253return <>{`**** **** **** ${last4}`}</>;254}255256function ExpirationDate({ exp_month, exp_year }) {257return <>{`${exp_month}/${exp_year}`}</>;258}259260interface CardProps {261brand;262last4;263exp_month;264exp_year;265country;266address_zip;267}268269export function CreditCard(props: CardProps) {270const { brand, last4, exp_month, exp_year, country, address_zip } = props;271return (272<div273style={{274backgroundColor: "#f0f0ff",275border: "1px solid lightgrey",276margin: "15px 0",277padding: "10px",278borderRadius: "5px",279maxWidth: "300px",280}}281>282<Brand brand={brand} />283<br />284<Number last4={last4} />285<br />286<ExpirationDate exp_month={exp_month} exp_year={exp_year} />287<br />288{country} {address_zip}289<br />290</div>291);292}293294295