WIP: Stripe integration

This commit is contained in:
binwiederhier 2023-01-14 06:43:44 -05:00
parent 7007c0a0bd
commit 01fd4754f9
20 changed files with 557 additions and 43 deletions

View file

@ -8,7 +8,7 @@ import {
accountTokenUrl,
accountUrl, maybeWithAuth, topicUrl,
withBasicAuth,
withBearerAuth
withBearerAuth, accountCheckoutUrl, accountBillingPortalUrl
} from "./utils";
import session from "./Session";
import subscriptionManager from "./SubscriptionManager";
@ -228,7 +228,7 @@ class AccountApi {
this.triggerChange(); // Dangle!
}
async upsertAccess(topic, everyone) {
async upsertReservation(topic, everyone) {
const url = accountReservationUrl(config.base_url);
console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`);
const response = await fetch(url, {
@ -249,7 +249,7 @@ class AccountApi {
this.triggerChange(); // Dangle!
}
async deleteAccess(topic) {
async deleteReservation(topic) {
const url = accountReservationSingleUrl(config.base_url, topic);
console.log(`[AccountApi] Removing topic reservation ${url}`);
const response = await fetch(url, {
@ -264,6 +264,39 @@ class AccountApi {
this.triggerChange(); // Dangle!
}
async createCheckoutSession(tier) {
const url = accountCheckoutUrl(config.base_url);
console.log(`[AccountApi] Creating checkout session`);
const response = await fetch(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
tier: tier
})
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
return await response.json();
}
async createBillingPortalSession() {
const url = accountBillingPortalUrl(config.base_url);
console.log(`[AccountApi] Creating billing portal session`);
const response = await fetch(url, {
method: "POST",
headers: withBearerAuth({}, session.token())
});
if (response.status === 401 || response.status === 403) {
throw new UnauthorizedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
return await response.json();
}
async sync() {
try {
if (!session.token()) {

View file

@ -26,6 +26,8 @@ export const accountSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/subscr
export const accountSubscriptionSingleUrl = (baseUrl, id) => `${baseUrl}/v1/account/subscription/${id}`;
export const accountReservationUrl = (baseUrl) => `${baseUrl}/v1/account/reservation`;
export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`;
export const accountCheckoutUrl = (baseUrl) => `${baseUrl}/v1/account/checkout`;
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
export const expandSecureUrl = (url) => `https://${url}`;

View file

@ -171,10 +171,28 @@ const Stats = () => {
const { t } = useTranslation();
const { account } = useContext(AccountContext);
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
if (!account) {
return <></>;
}
const normalize = (value, max) => Math.min(value / max * 100, 100);
const normalize = (value, max) => {
return Math.min(value / max * 100, 100);
};
const handleManageBilling = async () => {
try {
const response = await accountApi.createBillingPortalSession();
window.location.href = response.redirect_url;
} catch (e) {
console.log(`[Account] Error changing password`, e);
if ((e instanceof UnauthorizedError)) {
session.resetAndRedirect(routes.login);
}
// TODO show error
}
};
return (
<Card sx={{p: 3}} aria-label={t("account_usage_title")}>
<Typography variant="h5" sx={{marginBottom: 2}}>
@ -201,12 +219,20 @@ const Stats = () => {
>{t("account_usage_tier_upgrade_button")}</Button>
}
{config.enable_payments && account.role === "user" && account.tier?.paid &&
<Button
variant="outlined"
size="small"
onClick={() => setUpgradeDialogOpen(true)}
sx={{ml: 1}}
>{t("account_usage_tier_change_button")}</Button>
<>
<Button
variant="outlined"
size="small"
onClick={() => setUpgradeDialogOpen(true)}
sx={{ml: 1}}
>{t("account_usage_tier_change_button")}</Button>
<Button
variant="outlined"
size="small"
onClick={handleManageBilling}
sx={{ml: 1}}
>Manage billing</Button>
</>
}
<UpgradeDialog
open={upgradeDialogOpen}

View file

@ -501,7 +501,7 @@ const Reservations = () => {
const handleDialogSubmit = async (reservation) => {
setDialogOpen(false);
try {
await accountApi.upsertAccess(reservation.topic, reservation.everyone);
await accountApi.upsertReservation(reservation.topic, reservation.everyone);
await accountApi.sync();
console.debug(`[Preferences] Added topic reservation`, reservation);
} catch (e) {
@ -557,7 +557,7 @@ const ReservationsTable = (props) => {
const handleDialogSubmit = async (reservation) => {
setDialogOpen(false);
try {
await accountApi.upsertAccess(reservation.topic, reservation.everyone);
await accountApi.upsertReservation(reservation.topic, reservation.everyone);
await accountApi.sync();
console.debug(`[Preferences] Added topic reservation`, reservation);
} catch (e) {
@ -568,7 +568,7 @@ const ReservationsTable = (props) => {
const handleDeleteClick = async (reservation) => {
try {
await accountApi.deleteAccess(reservation.topic);
await accountApi.deleteReservation(reservation.topic);
await accountApi.sync();
console.debug(`[Preferences] Deleted topic reservation`, reservation);
} catch (e) {

View file

@ -110,7 +110,7 @@ const SubscribePage = (props) => {
if (session.exists() && baseUrl === config.base_url && reserveTopicVisible) {
console.log(`[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}`);
try {
await accountApi.upsertAccess(topic, everyone);
await accountApi.upsertReservation(topic, everyone);
// Account sync later after it was added
} catch (e) {
console.log(`[SubscribeDialog] Error reserving topic`, e);

View file

@ -37,9 +37,9 @@ const SubscriptionSettingsDialog = (props) => {
// Reservation
if (reserveTopicVisible) {
await accountApi.upsertAccess(subscription.topic, everyone);
await accountApi.upsertReservation(subscription.topic, everyone);
} else if (!reserveTopicVisible && subscription.reservation) { // Was removed
await accountApi.deleteAccess(subscription.topic);
await accountApi.deleteReservation(subscription.topic);
}
// Sync account

View file

@ -2,28 +2,83 @@ import * as React from 'react';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {useMediaQuery} from "@mui/material";
import {CardActionArea, CardContent, useMediaQuery} from "@mui/material";
import theme from "./theme";
import DialogFooter from "./DialogFooter";
import Button from "@mui/material/Button";
import accountApi, {TopicReservedError, UnauthorizedError} from "../app/AccountApi";
import session from "../app/Session";
import routes from "./routes";
import {useContext, useState} from "react";
import Card from "@mui/material/Card";
import Typography from "@mui/material/Typography";
import {AccountContext} from "./App";
const UpgradeDialog = (props) => {
const { account } = useContext(AccountContext);
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const [selected, setSelected] = useState(account?.tier?.code || null);
const [errorText, setErrorText] = useState("");
const handleSuccess = async () => {
// TODO
const handleCheckout = async () => {
try {
const response = await accountApi.createCheckoutSession(selected);
if (response.redirect_url) {
window.location.href = response.redirect_url;
} else {
await accountApi.sync();
}
} catch (e) {
console.log(`[UpgradeDialog] Error creating checkout session`, e);
if ((e instanceof UnauthorizedError)) {
session.resetAndRedirect(routes.login);
}
// FIXME show error
}
}
return (
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
<Dialog open={props.open} onClose={props.onCancel} maxWidth="md" fullScreen={fullScreen}>
<DialogTitle>Upgrade to Pro</DialogTitle>
<DialogContent>
Content
<div style={{
display: "flex",
flexDirection: "row"
}}>
<TierCard code={null} name={"Free"} selected={selected === null} onClick={() => setSelected(null)}/>
<TierCard code="starter" name={"Starter"} selected={selected === "starter"} onClick={() => setSelected("starter")}/>
<TierCard code="pro" name={"Pro"} selected={selected === "pro"} onClick={() => setSelected("pro")}/>
<TierCard code="business" name={"Business"} selected={selected === "business"} onClick={() => setSelected("business")}/>
</div>
</DialogContent>
<DialogFooter>
Footer
<DialogFooter status={errorText}>
<Button onClick={handleCheckout}>Checkout</Button>
</DialogFooter>
</Dialog>
);
};
const TierCard = (props) => {
const cardStyle = (props.selected) ? {
border: "1px solid red",
} : {};
return (
<Card sx={{ m: 1, maxWidth: 345 }}>
<CardActionArea>
<CardContent sx={{...cardStyle}} onClick={props.onClick}>
<Typography gutterBottom variant="h5" component="div">
{props.name}
</Typography>
<Typography variant="body2" color="text.secondary">
Lizards are a widespread group of squamate reptiles, with over 6,000
species, ranging across all continents except Antarctica
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
}
export default UpgradeDialog;