Files
astuto/app/javascript/components/Billing/index.tsx
Riccardo Graziosi 9176b96167 Update README and remove old links (#481)
- Remove links to demo and docs website from README
- Remove most links across the codebase referring to docs.astuto.io and astuto.io

Goodbye, Astuto 🥲
2025-10-31 15:20:05 +01:00

190 lines
6.7 KiB
TypeScript

import * as React from 'react';
import I18n from 'i18n-js';
import { loadStripe } from '@stripe/stripe-js';
import ITenantBilling, { TENANT_BILLING_STATUS_ACTIVE, TENANT_BILLING_STATUS_CANCELED, TENANT_BILLING_STATUS_TRIAL } from '../../interfaces/ITenantBilling';
import Box from '../common/Box';
import { EmbeddedCheckout, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js';
import buildRequestHeaders from '../../helpers/buildRequestHeaders';
import { SmallMutedText } from '../common/CustomTexts';
import ActionLink from '../common/ActionLink';
import { BackIcon, LearnMoreIcon } from '../common/Icons';
import PricingTable from './PricingTable';
interface Props {
tenantBilling: ITenantBilling;
prices: Array<any>;
createCheckoutSessionUrl: string;
billingUrl: string;
manageSubscriptionUrl: string;
stripeMonthlyLookupKey: string;
stripeYearlyLookupKey: string;
stripePublicKey: string;
authenticityToken: string;
}
const Billing = ({
tenantBilling,
prices,
createCheckoutSessionUrl,
billingUrl,
manageSubscriptionUrl,
stripeMonthlyLookupKey,
stripeYearlyLookupKey,
stripePublicKey,
authenticityToken,
}: Props) => {
const [stripePromise, setStripePromise] = React.useState(null);
const [currentPrice, setCurrentPrice] = React.useState(null);
const [chosenPrice, setChosenPrice] = React.useState(null);
const [showBackLink, setShowBackLink] = React.useState(false);
React.useEffect(() => {
setStripePromise(loadStripe(stripePublicKey));
}, []);
React.useEffect(() => {
if (prices && prices.length > 0) {
setCurrentPrice(prices[0].id);
}
}, [prices]);
const fetchClientSecret = React.useCallback(() => {
// Create a Checkout Session
return fetch(`${createCheckoutSessionUrl}?price_id=${chosenPrice}&tenant_id=${tenantBilling.slug}`, {
method: "POST",
headers: buildRequestHeaders(authenticityToken),
})
.then((res) => res.json())
.then((data) => data.clientSecret);
}, [chosenPrice]);
React.useEffect(() => {
if (chosenPrice) {
// scroll to checkout
const checkoutElement = document.getElementById('checkout');
setTimeout(() => {
checkoutElement.scrollIntoView({behavior: 'smooth'});
}, 300);
// show back link after 5 seconds
const timer = setTimeout(() => {
setShowBackLink(true);
}, 5000);
return () => clearTimeout(timer); // cleanup on unmount or when chosenPrice changes
} else {
setShowBackLink(false); // reset state when chosenPrice becomes null
}
}, [chosenPrice]);
const options = {fetchClientSecret};
const currentTime = new Date();
const trialEndsAt = new Date(tenantBilling.trial_ends_at);
const trialEndsAtFormatted = trialEndsAt.toLocaleString(undefined, {year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit'});
const subscriptionEndsAt = new Date(tenantBilling.subscription_ends_at);
const subscriptionEndsAtFormatted = subscriptionEndsAt.toLocaleString(undefined, {year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit'});
const isExpired = (
(tenantBilling.status === TENANT_BILLING_STATUS_TRIAL && trialEndsAt < currentTime) ||
((tenantBilling.status === TENANT_BILLING_STATUS_ACTIVE || tenantBilling.status === TENANT_BILLING_STATUS_CANCELED) && subscriptionEndsAt < currentTime)
);
return (
<Box customClass="billingContainer">
<h2>{ I18n.t('billing.title') }</h2>
<p>
<span className="billingStatusBadge">{tenantBilling.status}</span>
{ isExpired && <span className="billingStatusBadge billingStatusBadgeExpired">Expired</span> }
</p>
{
tenantBilling.status === TENANT_BILLING_STATUS_TRIAL &&
<p>Trial {isExpired ? 'expired' : 'expires'} on <b>{trialEndsAtFormatted}</b></p>
}
{
tenantBilling.status === TENANT_BILLING_STATUS_TRIAL && isExpired &&
<p>Your trial has expired. Please choose a subscription plan to continue using the service.</p>
}
{
tenantBilling.status === TENANT_BILLING_STATUS_ACTIVE &&
<p>Subscription {isExpired ? 'expired' : 'renews'} on {subscriptionEndsAtFormatted}</p>
}
{
tenantBilling.status === TENANT_BILLING_STATUS_ACTIVE && isExpired &&
<p>Your subscription has expired because automatic renewal failed. Please update your payment details by managing your subscription.</p>
}
{
tenantBilling.status === TENANT_BILLING_STATUS_CANCELED &&
<p>Subscription {isExpired ? 'expired' : 'expires'} on {subscriptionEndsAtFormatted}</p>
}
{/* {
(tenantBilling.status === TENANT_BILLING_STATUS_TRIAL) && chosenPrice === null &&
<PricingTable
prices={prices}
currentPrice={currentPrice}
setCurrentPrice={setCurrentPrice}
setChosenPrice={setChosenPrice}
stripeMonthlyLookupKey={stripeMonthlyLookupKey}
stripeYearlyLookupKey={stripeYearlyLookupKey}
/>
}
{
chosenPrice &&
<div className="checkoutContainer">
{ showBackLink ?
<ActionLink onClick={() => window.location.href = billingUrl} icon={<BackIcon />}>
Choose another plan
</ActionLink>
:
<br />
}
<div id="checkout">
<EmbeddedCheckoutProvider
stripe={stripePromise}
options={options}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
</div>
</div>
}
{
(tenantBilling.status === TENANT_BILLING_STATUS_ACTIVE || tenantBilling.status === TENANT_BILLING_STATUS_CANCELED) &&
<div className="billingAction">
<button className="btnPrimary" onClick={() => window.open(manageSubscriptionUrl, '_blank')}>
{tenantBilling.status === TENANT_BILLING_STATUS_ACTIVE ? 'Manage subscription' : 'Renew subscription'}
</button>
<SmallMutedText>
You will be redirected to Stripe, our billing partner.
</SmallMutedText>
</div>
} */}
<p>
We do not accept new subscriptions right now.
</p>
{/* <div className="billingUsefulLinks">
<ActionLink onClick={() => window.open('https://astuto.io/terms-of-service', '_blank')} icon={<LearnMoreIcon />}>
Terms of Service
</ActionLink>
<ActionLink onClick={() => window.open('https://astuto.io/privacy-policy', '_blank')} icon={<LearnMoreIcon />}>
Privacy Policy
</ActionLink>
</div> */}
</Box>
);
}
export default Billing;