Next.js 16, React 19.2, Identities page, Invitations identities step, PNPM Catalogs (#381)

* Upgraded to Next.js 16
* Refactored code to comply with React 19.2 ESLint rules
* Refactored some useEffect usages with the new useEffectEvent
* Added Identities page and added second step to set up an identity after accepting an invitation
* Updated all dependencies
* Introduced PNPM catalogs for some frequently updated dependencies
* Bugs fixing and improvements
This commit is contained in:
Giancarlo Buomprisco
2025-10-22 11:47:47 +09:00
committed by GitHub
parent ea0c1dde80
commit 2c0d0bf7a1
98 changed files with 4812 additions and 4394 deletions

View File

@@ -1,11 +1,27 @@
import { Suspense, forwardRef, lazy, memo, useMemo } from 'react';
import { Suspense, lazy } from 'react';
import { Enums } from '@kit/supabase/database';
import { LoadingOverlay } from '@kit/ui/loading-overlay';
type BillingProvider = Enums<'billing_provider'>;
const Fallback = <LoadingOverlay fullPage={false} />;
// Create lazy components at module level (not during render)
const StripeCheckoutLazy = lazy(async () => {
const { StripeCheckout } = await import('@kit/stripe/components');
return { default: StripeCheckout };
});
const LemonSqueezyCheckoutLazy = lazy(async () => {
const { LemonSqueezyEmbeddedCheckout } = await import(
'@kit/lemon-squeezy/components'
);
return { default: LemonSqueezyEmbeddedCheckout };
});
type CheckoutProps = {
onClose: (() => unknown) | undefined;
checkoutToken: string;
};
export function EmbeddedCheckout(
props: React.PropsWithChildren<{
@@ -14,100 +30,54 @@ export function EmbeddedCheckout(
onClose?: () => void;
}>,
) {
const CheckoutComponent = useMemo(
() => loadCheckoutComponent(props.provider),
[props.provider],
);
return (
<>
<CheckoutComponent
onClose={props.onClose}
checkoutToken={props.checkoutToken}
/>
<Suspense fallback={<LoadingOverlay fullPage={false} />}>
<CheckoutSelector
provider={props.provider}
onClose={props.onClose}
checkoutToken={props.checkoutToken}
/>
</Suspense>
<BlurryBackdrop />
</>
);
}
function loadCheckoutComponent(provider: BillingProvider) {
switch (provider) {
case 'stripe': {
return buildLazyComponent(() => {
return import('@kit/stripe/components').then(({ StripeCheckout }) => {
return {
default: StripeCheckout,
};
});
});
}
case 'lemon-squeezy': {
return buildLazyComponent(() => {
return import('@kit/lemon-squeezy/components').then(
({ LemonSqueezyEmbeddedCheckout }) => {
return {
default: LemonSqueezyEmbeddedCheckout,
};
},
);
});
}
case 'paddle': {
throw new Error('Paddle is not yet supported');
}
default:
throw new Error(`Unsupported provider: ${provider as string}`);
}
}
function buildLazyComponent<
Component extends React.ComponentType<{
onClose: (() => unknown) | undefined;
checkoutToken: string;
}>,
>(
load: () => Promise<{
default: Component;
}>,
fallback = Fallback,
function CheckoutSelector(
props: CheckoutProps & { provider: BillingProvider },
) {
let LoadedComponent: ReturnType<typeof lazy<Component>> | null = null;
const LazyComponent = forwardRef<
React.ElementRef<'div'>,
{
onClose: (() => unknown) | undefined;
checkoutToken: string;
}
>(function LazyDynamicComponent(props, ref) {
if (!LoadedComponent) {
LoadedComponent = lazy(load);
}
return (
<Suspense fallback={fallback}>
{/* @ts-expect-error: weird TS */}
<LoadedComponent
switch (props.provider) {
case 'stripe':
return (
<StripeCheckoutLazy
onClose={props.onClose}
checkoutToken={props.checkoutToken}
ref={ref}
/>
</Suspense>
);
});
);
return memo(LazyComponent);
case 'lemon-squeezy':
return (
<LemonSqueezyCheckoutLazy
onClose={props.onClose}
checkoutToken={props.checkoutToken}
/>
);
case 'paddle':
throw new Error('Paddle is not yet supported');
default:
throw new Error(`Unsupported provider: ${props.provider as string}`);
}
}
function BlurryBackdrop() {
return (
<div
className={
'bg-background/30 fixed left-0 top-0 w-full backdrop-blur-sm' +
'bg-background/30 fixed top-0 left-0 w-full backdrop-blur-sm' +
' !m-0 h-full'
}
/>