Addon-Kauf im Event admin korrigiert.

This commit is contained in:
Codex Agent
2025-12-29 19:31:26 +01:00
parent aaf418a917
commit 902e78cae9
8 changed files with 295 additions and 29 deletions

View File

@@ -1381,6 +1381,8 @@ function translateLimits(t: (key: string, defaultValue?: string, options?: Recor
guestsBlocked: 'Guest limit reached.',
guestsWarning: '{{remaining}} of {{limit}} guests remaining.',
galleryExpired: 'Gallery expired. Extend to keep it online.',
galleryWarningHour: 'Gallery expires in {{hours}} hour.',
galleryWarningHours: 'Gallery expires in {{hours}} hours.',
galleryWarningDay: 'Gallery expires in {{days}} day.',
galleryWarningDays: 'Gallery expires in {{days}} days.',
buyMorePhotos: 'Buy more photos',
@@ -1390,7 +1392,7 @@ function translateLimits(t: (key: string, defaultValue?: string, options?: Recor
return (key, options) => t(`limits.${key}`, defaults[key] ?? key, options);
}
function LimitWarnings({
export function LimitWarnings({
limits,
addons,
onCheckout,
@@ -1420,32 +1422,40 @@ function LimitWarnings({
<Text fontSize="$sm" color={textColor} fontWeight="700">
{warning.message}
</Text>
{(warning.scope === 'photos' || warning.scope === 'gallery' || warning.scope === 'guests') && addons.length ? (
<MobileAddonsPicker
scope={warning.scope}
addons={addons}
busy={busyScope === warning.scope}
onCheckout={onCheckout}
translate={translate}
/>
) : null}
<CTAButton
label={
warning.scope === 'photos'
? translate('buyMorePhotos')
: warning.scope === 'gallery'
? translate('extendGallery')
: translate('buyMoreGuests')
}
onPress={() => onCheckout(warning.scope)}
loading={busyScope === warning.scope}
/>
{(warning.scope === 'photos' || warning.scope === 'gallery' || warning.scope === 'guests')
&& resolveAddonOptions(addons, warning.scope).length ? (
<MobileAddonsPicker
scope={warning.scope}
addons={addons}
busy={busyScope === warning.scope}
onCheckout={onCheckout}
translate={translate}
/>
) : (
<CTAButton
label={
warning.scope === 'photos'
? translate('buyMorePhotos')
: warning.scope === 'gallery'
? translate('extendGallery')
: translate('buyMoreGuests')
}
onPress={() => onCheckout(warning.scope)}
loading={busyScope === warning.scope}
/>
)}
</MobileCard>
))}
</YStack>
);
}
function resolveAddonOptions(addons: EventAddonCatalogItem[], scope: 'photos' | 'gallery' | 'guests'): EventAddonCatalogItem[] {
const whitelist = scopeDefaults[scope];
const filtered = addons.filter((addon) => addon.price_id && whitelist.includes(addon.key));
return filtered.length ? filtered : addons.filter((addon) => addon.price_id);
}
function MobileAddonsPicker({
scope,
addons,
@@ -1459,11 +1469,7 @@ function MobileAddonsPicker({
onCheckout: (addonKey: string) => void;
translate: LimitTranslator;
}) {
const options = React.useMemo(() => {
const whitelist = scopeDefaults[scope];
const filtered = addons.filter((addon) => addon.price_id && whitelist.includes(addon.key));
return filtered.length ? filtered : addons.filter((addon) => addon.price_id);
}, [addons, scope]);
const options = React.useMemo(() => resolveAddonOptions(addons, scope), [addons, scope]);
const [selected, setSelected] = React.useState<string>(() => options[0]?.key ?? selectAddonKeyForScope(addons, scope));

View File

@@ -0,0 +1,107 @@
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { render } from '@testing-library/react';
import { LimitWarnings } from '../EventPhotosPage';
vi.mock('@tamagui/stacks', () => ({
YStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
XStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/text', () => ({
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
}));
vi.mock('@tamagui/core', () => ({
useTheme: () => ({
color: { val: '#111827' },
gray: { val: '#4b5563' },
borderColor: { val: '#e5e7eb' },
blue3: { val: '#e8f1ff' },
blue6: { val: '#bfdbfe' },
red10: { val: '#b91c1c' },
surface: { val: '#ffffff' },
gray12: { val: '#0f172a' },
}),
}));
vi.mock('@tamagui/react-native-web-lite', () => ({
Pressable: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
}));
vi.mock('../components/Primitives', () => ({
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
PillBadge: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
CTAButton: ({
label,
onPress,
disabled,
loading,
}: {
label: string;
onPress?: () => void;
disabled?: boolean;
loading?: boolean;
}) => (
<button type="button" onClick={onPress} disabled={disabled || loading}>
{label}
</button>
),
SkeletonCard: () => <div />,
}));
vi.mock('../components/FormControls', () => ({
MobileField: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
MobileInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
MobileSelect: ({
children,
compact,
...props
}: {
children: React.ReactNode;
compact?: boolean;
}) => <select {...props}>{children}</select>,
}));
describe('LimitWarnings', () => {
it('renders a single checkout button when add-on selection is available', () => {
const limits = {
photos: {
limit: 100,
used: 100,
remaining: 0,
percentage: 100,
state: 'limit_reached',
threshold_reached: null,
next_threshold: null,
thresholds: [],
},
guests: null,
gallery: null,
can_upload_photos: false,
can_add_guests: true,
};
const addons = [
{
key: 'extra_photos_500',
label: 'Extra photos',
price_id: 'pri_addon_photos',
},
];
const { getAllByRole } = render(
<LimitWarnings
limits={limits}
addons={addons}
onCheckout={vi.fn()}
busyScope={null}
translate={(key) => key}
textColor="#111827"
borderColor="#e5e7eb"
/>,
);
expect(getAllByRole('button')).toHaveLength(1);
});
});