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