mirror of
https://github.com/astuto/astuto.git
synced 2025-12-14 18:57:51 +01:00
186 lines
6.6 KiB
TypeScript
186 lines
6.6 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>
|
|
}
|
|
|
|
<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; |