first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import {useBillingUser} from '@common/billing/billing-page/use-billing-user';
import {FormattedDate} from '@common/i18n/formatted-date';
import {BillingPlanPanel} from '@common/billing/billing-page/billing-plan-panel';
import {Trans} from '@common/i18n/trans';
import {FormattedPrice} from '@common/i18n/formatted-price';
import {Button} from '@common/ui/buttons/button';
import {Link} from 'react-router-dom';
import {Fragment} from 'react';
import {SectionHelper} from '@common/ui/section-helper';
export function ActivePlanPanel() {
const {subscription} = useBillingUser();
if (!subscription?.price || !subscription?.product) return null;
const renewDate = (
<FormattedDate preset="long" date={subscription.renews_at} />
);
return (
<Fragment>
{subscription.past_due ? <PastDueMessage /> : null}
<BillingPlanPanel title={<Trans message="Current plan" />}>
<div className="mt-24 flex justify-between gap-20">
<div>
<div className="mb-2 text-xl font-bold">
{subscription.product.name}
</div>
<FormattedPrice
className="mb-2 text-xl"
price={subscription.price}
/>
<div className="text-base">
<Trans
message="Your plan renews on :date"
values={{date: renewDate}}
/>
</div>
</div>
<div className="w-[233px]">
<Button
variant="flat"
color="primary"
size="md"
className="mb-12 w-full"
elementType={Link}
to="/billing/change-plan"
disabled={subscription.gateway_name === 'none'}
>
<Trans message="Change plan" />
</Button>
<Button
variant="outline"
color="danger"
size="md"
className="w-full"
elementType={Link}
to="/billing/cancel"
>
<Trans message="Cancel plan" />
</Button>
</div>
</div>
</BillingPlanPanel>
</Fragment>
);
}
function PastDueMessage() {
return (
<SectionHelper
className="mb-24"
color="danger"
title="Payment is past due"
description="Your recent recurring payment has failed with the payment method we have on file. Please update your payment method to avoid any service interruptions."
/>
);
}

View File

@@ -0,0 +1,62 @@
import {useBillingUser} from '../use-billing-user';
import {FormattedDate} from '../../../i18n/formatted-date';
import {BillingPlanPanel} from '../billing-plan-panel';
import {Trans} from '../../../i18n/trans';
import {Chip} from '../../../ui/forms/input-field/chip-field/chip';
import {FormattedPrice} from '../../../i18n/formatted-price';
import {CalendarTodayIcon} from '../../../icons/material/CalendarToday';
import {Button} from '../../../ui/buttons/button';
import {Link} from 'react-router-dom';
export function CancelledPlanPanel() {
const {subscription} = useBillingUser();
if (!subscription?.price || !subscription?.product) return null;
const endingDate = (
<span className="whitespace-nowrap">
<FormattedDate preset="long" date={subscription.ends_at} />
</span>
);
return (
<BillingPlanPanel title={<Trans message="Current plan" />}>
<div className="mt-24 flex flex-col justify-between gap-20">
<div>
<Chip
className="mb-10 w-min"
size="xs"
radius="rounded"
color="danger"
>
<Trans message="Canceled" />
</Chip>
<div className="mb-2 text-xl font-bold">
{subscription.product.name}
</div>
<FormattedPrice className="mb-8 text-xl" price={subscription.price} />
<div className="flex items-center gap-8 text-base">
<CalendarTodayIcon size="sm" className="text-muted" />
<div className="flex-auto">
<Trans
message="Your plan will be canceled on :date"
values={{date: endingDate}}
/>
</div>
</div>
</div>
<div className="w-[233px]">
<Button
variant="flat"
color="primary"
size="md"
className="mb-12 w-full"
elementType={Link}
to="/billing/renew"
>
<Trans message="Renew plan" />
</Button>
</div>
</div>
</BillingPlanPanel>
);
}

View File

@@ -0,0 +1,99 @@
import {useBillingUser} from '../use-billing-user';
import {BillingPlanPanel} from '../billing-plan-panel';
import {Trans} from '../../../i18n/trans';
import {useInvoices} from '../requests/use-invoices';
import {FormattedDate} from '../../../i18n/formatted-date';
import {FormattedCurrency} from '../../../i18n/formatted-currency';
import {Chip} from '../../../ui/forms/input-field/chip-field/chip';
import {OpenInNewIcon} from '../../../icons/material/OpenInNew';
import {Skeleton} from '../../../ui/skeleton/skeleton';
import {AnimatePresence, m} from 'framer-motion';
import {Invoice} from '../../invoice';
import {opacityAnimation} from '../../../ui/animation/opacity-animation';
import {useSettings} from '../../../core/settings/use-settings';
export function InvoiceHistoryPanel() {
const {user} = useBillingUser();
const query = useInvoices(user?.id!);
if (!user) return null;
const invoices = query.data?.invoices;
return (
<BillingPlanPanel title={<Trans message="Payment history" />}>
<div className="max-w-[464px]">
<AnimatePresence initial={false} mode="wait">
{query.isLoading ? (
<LoadingSkeleton key="loading-skeleton" />
) : (
<InvoiceList key="invoices" invoices={invoices} />
)}
</AnimatePresence>
</div>
</BillingPlanPanel>
);
}
interface InvoiceListProps {
invoices?: Invoice[];
}
function InvoiceList({invoices}: InvoiceListProps) {
const {base_url} = useSettings();
return (
<m.div {...opacityAnimation}>
{!invoices?.length ? (
<div className="text-muted italic">
<Trans message="No invoices yet" />
</div>
) : undefined}
{invoices?.map(invoice => (
<div
className="whitespace-nowrap text-base flex items-center justify-between gap-10 mb-14"
key={invoice.id}
>
<a
href={`${base_url}/billing/invoices/${invoice.uuid}`}
target="_blank"
className="flex items-center gap-8 hover:underline"
rel="noreferrer"
>
<FormattedDate date={invoice.created_at} />
<OpenInNewIcon size="xs" />
</a>
{invoice.subscription.price && (
<div>
<FormattedCurrency
value={invoice.subscription.price.amount}
currency={invoice.subscription.price.currency}
/>
</div>
)}
<Chip
size="xs"
color={invoice.paid ? 'positive' : 'danger'}
radius="rounded"
>
{invoice.paid ? (
<Trans message="Paid" />
) : (
<Trans message="Unpaid" />
)}
</Chip>
<div>{invoice.subscription.product?.name}</div>
</div>
))}
</m.div>
);
}
function LoadingSkeleton() {
return (
<m.div {...opacityAnimation}>
<Skeleton className="mb-14" />
<Skeleton className="mb-14" />
<Skeleton className="mb-14" />
<Skeleton className="mb-14" />
<Skeleton />
</m.div>
);
}

View File

@@ -0,0 +1,77 @@
import {useBillingUser} from '../use-billing-user';
import {BillingPlanPanel} from '../billing-plan-panel';
import {Trans} from '../../../i18n/trans';
import {Link} from 'react-router-dom';
import {EditIcon} from '../../../icons/material/Edit';
import {Fragment} from 'react';
import paypalSvg from './paypal.svg';
import {SvgImage} from '../../../ui/images/svg-image/svg-image';
export function PaymentMethodPanel() {
const {user, subscription} = useBillingUser();
if (!user || !subscription) return null;
const isPaypal = subscription.gateway_name === 'paypal';
const PaymentMethod = isPaypal ? PaypalPaymentMethod : CardPaymentMethod;
return (
<BillingPlanPanel title={<Trans message="Payment method" />}>
<PaymentMethod
methodClassName="whitespace-nowrap text-base max-w-[464px] flex items-center gap-10"
linkClassName="flex items-center gap-4 text-muted mt-18 block hover:underline"
/>
</BillingPlanPanel>
);
}
interface PaymentMethodProps {
methodClassName: string;
linkClassName: string;
}
function CardPaymentMethod({
methodClassName,
linkClassName,
}: PaymentMethodProps) {
const {user} = useBillingUser();
if (!user) return null;
return (
<Fragment>
<div className={methodClassName}>
<span className="capitalize">{user.card_brand}</span>
{user.card_last_four}
{user.card_expires && (
<div className="ml-auto">
<Trans message="Expires :date" values={{date: user.card_expires}} />
</div>
)}
</div>
<Link className={linkClassName} to="/billing/change-payment-method">
<EditIcon size="sm" />
<Trans message="Change payment method" />
</Link>
</Fragment>
);
}
function PaypalPaymentMethod({
methodClassName,
linkClassName,
}: PaymentMethodProps) {
const {subscription} = useBillingUser();
return (
<Fragment>
<div className={methodClassName}>
<SvgImage src={paypalSvg} />
</div>
<a
className={linkClassName}
href={`https://www.sandbox.paypal.com/myaccount/autopay/connect/${subscription?.gateway_id}/funding`}
target="_blank"
rel="noreferrer"
>
<EditIcon size="sm" />
<Trans message="Change payment method" />
</a>
</Fragment>
);
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="101px" height="32" viewBox="0 0 101 32" preserveAspectRatio="xMinYMin meet"><path fill="#003087" d="M 12.237 2.8 L 4.437 2.8 C 3.937 2.8 3.437 3.2 3.337 3.7 L 0.237 23.7 C 0.137 24.1 0.437 24.4 0.837 24.4 L 4.537 24.4 C 5.037 24.4 5.537 24 5.637 23.5 L 6.437 18.1 C 6.537 17.6 6.937 17.2 7.537 17.2 L 10.037 17.2 C 15.137 17.2 18.137 14.7 18.937 9.8 C 19.237 7.7 18.937 6 17.937 4.8 C 16.837 3.5 14.837 2.8 12.237 2.8 Z M 13.137 10.1 C 12.737 12.9 10.537 12.9 8.537 12.9 L 7.337 12.9 L 8.137 7.7 C 8.137 7.4 8.437 7.2 8.737 7.2 L 9.237 7.2 C 10.637 7.2 11.937 7.2 12.637 8 C 13.137 8.4 13.337 9.1 13.137 10.1 Z"/><path fill="#003087" d="M 35.437 10 L 31.737 10 C 31.437 10 31.137 10.2 31.137 10.5 L 30.937 11.5 L 30.637 11.1 C 29.837 9.9 28.037 9.5 26.237 9.5 C 22.137 9.5 18.637 12.6 17.937 17 C 17.537 19.2 18.037 21.3 19.337 22.7 C 20.437 24 22.137 24.6 24.037 24.6 C 27.337 24.6 29.237 22.5 29.237 22.5 L 29.037 23.5 C 28.937 23.9 29.237 24.3 29.637 24.3 L 33.037 24.3 C 33.537 24.3 34.037 23.9 34.137 23.4 L 36.137 10.6 C 36.237 10.4 35.837 10 35.437 10 Z M 30.337 17.2 C 29.937 19.3 28.337 20.8 26.137 20.8 C 25.037 20.8 24.237 20.5 23.637 19.8 C 23.037 19.1 22.837 18.2 23.037 17.2 C 23.337 15.1 25.137 13.6 27.237 13.6 C 28.337 13.6 29.137 14 29.737 14.6 C 30.237 15.3 30.437 16.2 30.337 17.2 Z"/><path fill="#003087" d="M 55.337 10 L 51.637 10 C 51.237 10 50.937 10.2 50.737 10.5 L 45.537 18.1 L 43.337 10.8 C 43.237 10.3 42.737 10 42.337 10 L 38.637 10 C 38.237 10 37.837 10.4 38.037 10.9 L 42.137 23 L 38.237 28.4 C 37.937 28.8 38.237 29.4 38.737 29.4 L 42.437 29.4 C 42.837 29.4 43.137 29.2 43.337 28.9 L 55.837 10.9 C 56.137 10.6 55.837 10 55.337 10 Z"/><path fill="#009cde" d="M 67.737 2.8 L 59.937 2.8 C 59.437 2.8 58.937 3.2 58.837 3.7 L 55.737 23.6 C 55.637 24 55.937 24.3 56.337 24.3 L 60.337 24.3 C 60.737 24.3 61.037 24 61.037 23.7 L 61.937 18 C 62.037 17.5 62.437 17.1 63.037 17.1 L 65.537 17.1 C 70.637 17.1 73.637 14.6 74.437 9.7 C 74.737 7.6 74.437 5.9 73.437 4.7 C 72.237 3.5 70.337 2.8 67.737 2.8 Z M 68.637 10.1 C 68.237 12.9 66.037 12.9 64.037 12.9 L 62.837 12.9 L 63.637 7.7 C 63.637 7.4 63.937 7.2 64.237 7.2 L 64.737 7.2 C 66.137 7.2 67.437 7.2 68.137 8 C 68.637 8.4 68.737 9.1 68.637 10.1 Z"/><path fill="#009cde" d="M 90.937 10 L 87.237 10 C 86.937 10 86.637 10.2 86.637 10.5 L 86.437 11.5 L 86.137 11.1 C 85.337 9.9 83.537 9.5 81.737 9.5 C 77.637 9.5 74.137 12.6 73.437 17 C 73.037 19.2 73.537 21.3 74.837 22.7 C 75.937 24 77.637 24.6 79.537 24.6 C 82.837 24.6 84.737 22.5 84.737 22.5 L 84.537 23.5 C 84.437 23.9 84.737 24.3 85.137 24.3 L 88.537 24.3 C 89.037 24.3 89.537 23.9 89.637 23.4 L 91.637 10.6 C 91.637 10.4 91.337 10 90.937 10 Z M 85.737 17.2 C 85.337 19.3 83.737 20.8 81.537 20.8 C 80.437 20.8 79.637 20.5 79.037 19.8 C 78.437 19.1 78.237 18.2 78.437 17.2 C 78.737 15.1 80.537 13.6 82.637 13.6 C 83.737 13.6 84.537 14 85.137 14.6 C 85.737 15.3 85.937 16.2 85.737 17.2 Z"/><path fill="#009cde" d="M 95.337 3.3 L 92.137 23.6 C 92.037 24 92.337 24.3 92.737 24.3 L 95.937 24.3 C 96.437 24.3 96.937 23.9 97.037 23.4 L 100.237 3.5 C 100.337 3.1 100.037 2.8 99.637 2.8 L 96.037 2.8 C 95.637 2.8 95.437 3 95.337 3.3 Z"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB