feat: implement AI styling foundation and billing scope rework
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-06 20:01:58 +01:00
parent df00deb0df
commit 36bed12ff9
80 changed files with 8944 additions and 49 deletions

View File

@@ -6,6 +6,7 @@ import { Camera, ChevronLeft, ChevronRight, Download, Heart, Loader2, Share2, Sp
import AppShell from '../components/AppShell';
import PhotoFrameTile from '../components/PhotoFrameTile';
import ShareSheet from '../components/ShareSheet';
import AiMagicEditSheet from '../components/AiMagicEditSheet';
import { useEventData } from '../context/EventDataContext';
import { createPhotoShareLink, deletePhoto, fetchGallery, fetchPhoto, likePhoto, unlikePhoto } from '../services/photosApi';
import { usePollGalleryDelta } from '../hooks/usePollGalleryDelta';
@@ -17,6 +18,7 @@ import { buildEventPath } from '../lib/routes';
import { getBentoSurfaceTokens } from '../lib/bento';
import { usePollStats } from '../hooks/usePollStats';
import { pushGuestToast } from '../lib/toast';
import { GUEST_AI_MAGIC_EDITS_ENABLED } from '../lib/featureFlags';
type GalleryFilter = 'latest' | 'popular' | 'mine' | 'photobooth';
@@ -111,6 +113,7 @@ export default function GalleryScreen() {
url: null,
loading: false,
});
const [aiMagicEditOpen, setAiMagicEditOpen] = React.useState(false);
const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false);
const [deleteBusy, setDeleteBusy] = React.useState(false);
const [deleteConfirmMounted, setDeleteConfirmMounted] = React.useState(false);
@@ -294,6 +297,7 @@ export default function GalleryScreen() {
const lightboxSelected = lightboxIndex >= 0 ? displayPhotos[lightboxIndex] : null;
const lightboxOpen = Boolean(selectedPhotoId);
const canDelete = Boolean(lightboxPhoto && (lightboxPhoto.isMine || myPhotoIds.has(lightboxPhoto.id)));
const hasAiStylingAccess = GUEST_AI_MAGIC_EDITS_ENABLED && Boolean(event?.capabilities?.ai_styling);
React.useEffect(() => {
if (filter === 'photobooth' && !photos.some((photo) => photo.ingestSource === 'photobooth')) {
@@ -340,6 +344,7 @@ export default function GalleryScreen() {
[searchParams, setSearchParams, token]
);
const closeLightbox = React.useCallback(() => {
setAiMagicEditOpen(false);
const next = new URLSearchParams(searchParams);
next.delete('photo');
setSearchParams(next, { replace: true });
@@ -769,6 +774,14 @@ export default function GalleryScreen() {
document.body.removeChild(link);
}, []);
const openAiMagicEdit = React.useCallback(() => {
if (!lightboxPhoto || !hasAiStylingAccess) {
return;
}
setAiMagicEditOpen(true);
}, [hasAiStylingAccess, lightboxPhoto]);
const handleTouchStart = (event: React.TouchEvent) => {
touchStartX.current = event.touches[0]?.clientX ?? null;
};
@@ -1313,6 +1326,17 @@ export default function GalleryScreen() {
<Download size={14} color={isDark ? '#F8FAFF' : '#0F172A'} />
</Button>
) : null}
{lightboxPhoto && hasAiStylingAccess ? (
<Button
unstyled
paddingHorizontal="$2"
paddingVertical="$1.5"
onPress={openAiMagicEdit}
aria-label={t('galleryPage.lightbox.aiMagicEditAria', 'AI Magic Edit')}
>
<Sparkles size={14} color={isDark ? '#F8FAFF' : '#0F172A'} />
</Button>
) : null}
{lightboxPhoto && canDelete ? (
<Button
unstyled
@@ -1454,6 +1478,15 @@ export default function GalleryScreen() {
onCopyLink={() => copyLink(shareSheet.url)}
variant="inline"
/>
{hasAiStylingAccess ? (
<AiMagicEditSheet
open={aiMagicEditOpen}
onOpenChange={setAiMagicEditOpen}
eventToken={token ?? null}
photoId={lightboxPhoto?.id ?? null}
originalImageUrl={lightboxPhoto?.imageUrl ?? null}
/>
) : null}
</YStack>
</YStack>
</YStack>