Migrate billing from Paddle to Lemon Squeezy
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.',
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user