Migrate billing from Paddle to Lemon Squeezy

This commit is contained in:
Codex Agent
2026-02-03 10:59:54 +01:00
parent 2f4ebfefd4
commit a0ef90e13a
228 changed files with 4369 additions and 4067 deletions

View File

@@ -12,10 +12,10 @@ import { ContextHelpLink } from './components/ContextHelpLink';
import {
createTenantBillingPortalSession,
getTenantPackagesOverview,
getTenantPaddleTransactions,
getTenantLemonSqueezyTransactions,
getTenantPackageCheckoutStatus,
TenantPackageSummary,
PaddleTransactionSummary,
LemonSqueezyOrderSummary,
} from '../api';
import { TenantAddonHistoryEntry, getTenantAddonHistory } from '../api';
import { getApiErrorMessage } from '../lib/apiError';
@@ -52,7 +52,7 @@ export default function MobileBillingPage() {
const { textStrong, text, muted, subtle, danger, border, primary, accentSoft } = useAdminTheme();
const [packages, setPackages] = React.useState<TenantPackageSummary[]>([]);
const [activePackage, setActivePackage] = React.useState<TenantPackageSummary | null>(null);
const [transactions, setTransactions] = React.useState<PaddleTransactionSummary[]>([]);
const [transactions, setTransactions] = React.useState<LemonSqueezyOrderSummary[]>([]);
const [addons, setAddons] = React.useState<TenantAddonHistoryEntry[]>([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
@@ -78,7 +78,7 @@ export default function MobileBillingPage() {
try {
const [pkg, trx, addonHistory] = await Promise.all([
getTenantPackagesOverview({ force: true }),
getTenantPaddleTransactions().catch(() => ({ data: [] as PaddleTransactionSummary[] })),
getTenantLemonSqueezyTransactions().catch(() => ({ data: [] as LemonSqueezyOrderSummary[] })),
getTenantAddonHistory().catch(() => ({ data: [] as TenantAddonHistoryEntry[] })),
]);
setPackages(pkg.packages ?? []);
@@ -116,7 +116,7 @@ export default function MobileBillingPage() {
window.open(url, '_blank', 'noopener');
}
} catch (err) {
const message = getApiErrorMessage(err, t('billing.errors.portal', 'Konnte das Paddle-Portal nicht öffnen.'));
const message = getApiErrorMessage(err, t('billing.errors.portal', 'Konnte das Lemon Squeezy-Portal nicht öffnen.'));
toast.error(message);
} finally {
setPortalBusy(false);
@@ -388,7 +388,7 @@ export default function MobileBillingPage() {
{t('billing.sections.packages.hint', 'Active package, limits, and history at a glance.')}
</Text>
<CTAButton
label={portalBusy ? t('billing.actions.portalBusy', 'Öffne Portal...') : t('billing.actions.portal', 'Manage in Paddle')}
label={portalBusy ? t('billing.actions.portalBusy', 'Öffne Portal...') : t('billing.actions.portal', 'Manage in Lemon Squeezy')}
onPress={openPortal}
disabled={portalBusy}
/>

View File

@@ -1465,7 +1465,7 @@ function WatermarkPreview({
background: ADMIN_GRADIENTS.softCard,
}}
>
<div style={{ position: 'absolute', inset: 0, background: `linear-gradient(180deg, ${overlay}, transparent)` }} />
<div style={{ position: 'absolute', inset: 0, background: `linear-gradient(180deg, rgba(0,0,0,0.4), transparent)` }} />
<div
style={{
position: 'absolute',

View File

@@ -222,7 +222,12 @@ export default function MobileDashboardPage() {
return (
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
<DashboardCard padding="$0">
<YStack padding="$3" gap="$2">
<YStack
padding="$3"
gap="$2"
animation="bouncy"
enterStyle={{ opacity: 0, scale: 0.9 }}
>
<SectionHeader
title={t('dashboard:overview.title', 'At a glance')}
showSeparator={false}

View File

@@ -224,7 +224,7 @@ function PackageShopCard({
const isResellerCatalog = catalogType === 'reseller';
const statusLabel = getPackageStatusLabel({ t, isActive, owned });
const isSubdued = Boolean(!isResellerCatalog && (isDowngrade || !isUpgrade) && !isActive);
const canSelect = isResellerCatalog ? Boolean(pkg.paddle_price_id) : canSelectPackage(isUpgrade, isActive);
const canSelect = isResellerCatalog ? Boolean(pkg.lemonsqueezy_variant_id) : canSelectPackage(isUpgrade, isActive);
const hasManageAction = Boolean(isActive && onManage);
const includedTierLabel = resolveIncludedTierLabel(t, pkg.included_package_slug ?? null);
const handlePress = isActive ? onManage : canSelect ? onSelect : undefined;
@@ -524,7 +524,7 @@ function PackageShopCompareView({
<YStack width={labelWidth} />
{entries.map((entry) => {
const isResellerCatalog = catalogType === 'reseller';
const canSelect = isResellerCatalog ? Boolean(entry.pkg.paddle_price_id) : canSelectPackage(entry.isUpgrade, entry.isActive);
const canSelect = isResellerCatalog ? Boolean(entry.pkg.lemonsqueezy_variant_id) : canSelectPackage(entry.isUpgrade, entry.isActive);
const label = isResellerCatalog
? canSelect
? t('shop.partner.buy', 'Kaufen')

View File

@@ -1,10 +1,8 @@
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { ChevronLeft, Bell, QrCode, ChevronsUpDown, Search } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { YStack, XStack, SizableText as Text, Image } from 'tamagui';
import { Pressable } from '@tamagui/react-native-web-lite';
import { Image } from '@tamagui/image';
import { useTranslation } from 'react-i18next';
import { useEventContext } from '../../context/EventContext';
import { BottomNav, NavKey } from './BottomNav';

View File

@@ -1,9 +1,6 @@
import React from 'react';
import { Card } from '@tamagui/card';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Card, YStack, XStack, SizableText as Text, Tabs, Separator } from 'tamagui';
import { Pressable } from '@tamagui/react-native-web-lite';
import { Tabs, Separator } from 'tamagui';
import { useAdminTheme } from '../theme';
import { withAlpha } from './colors';
@@ -16,7 +13,9 @@ export function MobileCard({
const { surface, border, shadow, glassSurface, glassBorder, glassShadow } = useAdminTheme();
return (
<YStack
className={['admin-fade-up', className].filter(Boolean).join(' ')}
className={className}
animation="bouncy"
enterStyle={{ opacity: 0, y: 10, scale: 0.9 }}
backgroundColor={glassSurface ?? surface}
borderRadius={20}
borderWidth={2}
@@ -131,6 +130,8 @@ export function CTAButton({
}}
>
<XStack
animation="quick"
pressStyle={{ scale: 0.97, opacity: 0.9 }}
height={52}
borderRadius={18}
alignItems="center"
@@ -335,7 +336,8 @@ export function ActionTile({
disabled={disabled}
>
<YStack
className="admin-fade-up"
animation="lazy"
pressStyle={{ scale: 0.96, opacity: 0.8 }}
style={tileStyle}
borderRadius={isCluster ? 14 : 16}
padding="$3"

View File

@@ -36,9 +36,6 @@ export function MobileSheet({
const { surface, textStrong, muted, overlay, shadow, border } = useAdminTheme();
const bottomOffset = `max(env(safe-area-inset-bottom, 0px), ${bottomOffsetPx}px)`;
if (!open) {
return null;
}
return (
<Sheet
modal
@@ -53,29 +50,33 @@ export function MobileSheet({
dismissOnOverlayPress
dismissOnSnapToBottom
zIndex={100000}
animation="lazy"
>
<Sheet.Overlay {...({ backgroundColor: `${overlay}66` } as any)} />
<Sheet.Overlay
animation="lazy"
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
backgroundColor={overlay}
/>
<Sheet.Frame
{...({
width: '100%',
maxWidth: 520,
alignSelf: 'center',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
backgroundColor: surface,
padding,
paddingBottom,
shadowColor: shadow,
shadowOpacity: 0.12,
shadowRadius: 18,
shadowOffset: { width: 0, height: -8 },
} as any)}
width="100%"
maxWidth={520}
alignSelf="center"
borderTopLeftRadius={24}
borderTopRightRadius={24}
backgroundColor={surface}
padding={padding}
paddingBottom={paddingBottom}
shadowColor={shadow}
shadowOpacity={0.12}
shadowRadius={18}
shadowOffset={{ width: 0, height: -8 }}
style={{ marginBottom: bottomOffset }}
>
<Sheet.Handle height={5} width={48} backgroundColor={border} borderRadius={999} marginBottom="$3" />
<Sheet.ScrollView
showsVerticalScrollIndicator={false}
{...({ contentContainerStyle: { paddingBottom: 6 } } as any)}
contentContainerStyle={{ paddingBottom: 6 }}
>
<YStack gap={contentSpacing}>
<XStack alignItems="center" justifyContent="space-between">

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import toast from 'react-hot-toast';
import { createTenantPaddleCheckout } from '../../api';
import { createTenantLemonSqueezyCheckout } from '../../api';
import { adminPath } from '../../constants';
import { getApiErrorMessage } from '../../lib/apiError';
import { storePendingCheckout } from '../lib/billingCheckout';
@@ -33,7 +33,7 @@ export function usePackageCheckout(): {
cancelUrl.searchParams.set('checkout', 'cancel');
cancelUrl.searchParams.set('package_id', String(packageId));
const { checkout_url, checkout_session_id } = await createTenantPaddleCheckout(packageId, {
const { checkout_url, checkout_session_id } = await createTenantLemonSqueezyCheckout(packageId, {
success_url: successUrl.toString(),
return_url: cancelUrl.toString(),
});

View File

@@ -161,7 +161,7 @@ export function useAdminTheme() {
infoText: String(theme.blue10?.val ?? ADMIN_COLORS.primaryStrong),
danger: String(theme.danger?.val ?? ADMIN_COLORS.danger),
backdrop: String(theme.backgroundStrong?.val ?? ADMIN_COLORS.backdrop),
overlay: withAlpha(String(theme.backgroundStrong?.val ?? ADMIN_COLORS.backdrop), 0.6),
overlay: withAlpha(ADMIN_COLORS.backdrop, 0.7),
shadow: String(theme.shadowColor?.val ?? 'rgba(15, 23, 42, 0.08)'),
glassSurface,
glassSurfaceStrong,

View File

@@ -166,7 +166,7 @@ export default function WelcomeSummaryPage() {
{(t('summary.nextSteps', {
returnObjects: true,
defaultValue: [
'Optional: finish billing via Paddle inside the billing area.',
'Optional: finish billing via Lemon Squeezy inside the billing area.',
'Complete the event setup and configure tasks, team, and gallery.',
'Check your event slots before go-live and share your guest link.',
],