Add dedicated mobile event add-ons page
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user