Add dedicated mobile event add-ons page
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-07 15:00:14 +01:00
parent ddbfa38db1
commit 922da46331
10 changed files with 847 additions and 189 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Check, ChevronDown, Eye, EyeOff, Image as ImageIcon, RefreshCcw, Settings, Sparkles } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
@@ -21,7 +21,6 @@ import {
clearLiveShowPhoto,
ControlRoomSettings,
ControlRoomUploaderRule,
createEventAddonCheckout,
EventAddonCatalogItem,
EventAiEditingSettings,
EventLimitSummary,
@@ -60,8 +59,6 @@ import {
} from './lib/photoModerationQueue';
import { triggerHaptic } from './lib/haptics';
import { normalizeLiveStatus, resolveLiveShowApproveMode } from './lib/controlRoom';
import { LegalConsentSheet } from './components/LegalConsentSheet';
import { selectAddonKeyForScope } from './addons';
import { LimitWarnings } from './components/LimitWarnings';
type ModerationFilter = 'all' | 'featured' | 'hidden' | 'pending';
@@ -349,7 +346,6 @@ function isAiSettingsDirty(current: AiSettingsDraft, initial: AiSettingsDraft):
export default function MobileEventControlRoomPage() {
const { slug: slugParam } = useParams<{ slug?: string }>();
const navigate = useNavigate();
const location = useLocation();
const { t } = useTranslation('management');
const { activeEvent, selectEvent, refetch } = useEventContext();
const { user } = useAuth();
@@ -376,10 +372,6 @@ export default function MobileEventControlRoomPage() {
});
const [limits, setLimits] = React.useState<EventLimitSummary | null>(null);
const [catalogAddons, setCatalogAddons] = React.useState<EventAddonCatalogItem[]>([]);
const [busyScope, setBusyScope] = React.useState<string | null>(null);
const [consentOpen, setConsentOpen] = React.useState(false);
const [consentTarget, setConsentTarget] = React.useState<{ scope: 'photos' | 'gallery' | 'guests' | 'ai'; addonKey: string } | null>(null);
const [consentBusy, setConsentBusy] = React.useState(false);
const [livePhotos, setLivePhotos] = React.useState<TenantPhoto[]>([]);
const [liveStatusFilter, setLiveStatusFilter] = React.useState<LiveShowQueueStatus>('pending');
@@ -918,21 +910,6 @@ export default function MobileEventControlRoomPage() {
refreshCounts();
}, [refreshCounts]);
React.useEffect(() => {
if (!location.search || !slug) {
return;
}
const params = new URLSearchParams(location.search);
if (params.get('addon_success')) {
toast.success(t('mobileBilling.addonApplied', 'Add-on applied. Limits update shortly.'));
void refetch();
setModerationPage(1);
void loadModeration();
params.delete('addon_success');
navigate({ pathname: location.pathname, search: params.toString() ? `?${params.toString()}` : '' }, { replace: true });
}
}, [location.search, slug, loadModeration, navigate, t, location.pathname, refetch]);
const updateQueueState = React.useCallback((queue: PhotoModerationAction[]) => {
replacePhotoQueue(queue);
setQueuedActions(queue);
@@ -1134,7 +1111,7 @@ export default function MobileEventControlRoomPage() {
],
);
function resolveScopeAndAddonKey(scopeOrKey: 'photos' | 'gallery' | 'guests' | 'ai' | string) {
function resolveScope(scopeOrKey: 'photos' | 'gallery' | 'guests' | 'ai' | string): 'photos' | 'gallery' | 'guests' | 'ai' {
const scope =
scopeOrKey === 'photos' || scopeOrKey === 'gallery' || scopeOrKey === 'guests'
? scopeOrKey
@@ -1143,57 +1120,25 @@ export default function MobileEventControlRoomPage() {
: scopeOrKey.includes('gallery')
? 'gallery'
: scopeOrKey.includes('guest')
? 'guests'
? 'guests'
: 'photos';
const addonKey =
scopeOrKey === 'photos' || scopeOrKey === 'gallery' || scopeOrKey === 'guests'
? selectAddonKeyForScope(catalogAddons, scope as 'photos' | 'gallery' | 'guests')
: scopeOrKey === 'ai'
? aiStylingAddon?.key ?? 'ai_styling_unlock'
: scopeOrKey;
return { scope, addonKey };
return scope as 'photos' | 'gallery' | 'guests' | 'ai';
}
function startAddonCheckout(scopeOrKey: 'photos' | 'gallery' | 'guests' | 'ai' | string) {
if (!slug) return;
const { scope, addonKey } = resolveScopeAndAddonKey(scopeOrKey);
setConsentTarget({ scope: scope as 'photos' | 'gallery' | 'guests' | 'ai', addonKey });
setConsentOpen(true);
}
async function confirmAddonCheckout(consents: { acceptedTerms: boolean; acceptedWaiver: boolean }) {
if (!slug || !consentTarget) return;
const currentUrl = typeof window !== 'undefined'
? `${window.location.origin}${adminPath(`/mobile/events/${slug}/control-room`)}`
: '';
const successUrl = `${currentUrl}?addon_success=1`;
setBusyScope(consentTarget.scope);
setConsentBusy(true);
try {
const checkout = await createEventAddonCheckout(slug, {
addon_key: consentTarget.addonKey,
quantity: 1,
success_url: successUrl,
cancel_url: currentUrl,
accepted_terms: consents.acceptedTerms,
accepted_waiver: consents.acceptedWaiver,
});
if (checkout.checkout_url) {
window.location.href = checkout.checkout_url;
} else {
toast.error(t('events.errors.checkoutMissing', 'Checkout could not be started.'));
}
} catch (err) {
toast.error(getApiErrorMessage(err, t('events.errors.checkoutFailed', 'Add-on checkout failed.')));
} finally {
setConsentBusy(false);
setConsentOpen(false);
setConsentTarget(null);
setBusyScope(null);
function openAddonShop(scopeOrKey: 'photos' | 'gallery' | 'guests' | 'ai' | string) {
if (!slug) {
return;
}
const scope = resolveScope(scopeOrKey);
const query = new URLSearchParams({ scope });
if (scopeOrKey !== 'photos' && scopeOrKey !== 'gallery' && scopeOrKey !== 'guests' && scopeOrKey !== 'ai') {
query.set('addon', scopeOrKey);
}
navigate(adminPath(`/mobile/events/${slug}/addons?${query.toString()}`));
}
async function handleApprove(photo: TenantPhoto) {
@@ -1867,9 +1812,7 @@ export default function MobileEventControlRoomPage() {
</Text>
<CTAButton
label={aiStylingAddonCta}
onPress={() => startAddonCheckout('ai')}
loading={busyScope === 'ai'}
disabled={consentBusy}
onPress={() => openAddonShop('ai')}
/>
</MobileCard>
) : null
@@ -1879,8 +1822,8 @@ export default function MobileEventControlRoomPage() {
<LimitWarnings
limits={limits}
addons={catalogAddons}
onCheckout={startAddonCheckout}
busyScope={busyScope}
onCheckout={openAddonShop}
busyScope={null}
translate={translateLimits(t as any)}
textColor={text}
borderColor={border}
@@ -2165,17 +2108,6 @@ export default function MobileEventControlRoomPage() {
]}
/>
<LegalConsentSheet
open={consentOpen}
onClose={() => {
if (consentBusy) return;
setConsentOpen(false);
setConsentTarget(null);
}}
onConfirm={confirmAddonCheckout}
busy={consentBusy}
t={t}
/>
</MobileShell>
);
}