feat(slideshow): gate behind a feature flag + move globals to a Settings tab
- New `slideshow` feature flag (backend KNOWN_FLAGS/DEFAULT_FLAGS, frontend FeatureKey + context default, a toggle card under Settings -> Features -> Core). Default off; strictly opt-in. - Move the global watermark defaults off the Event Types page into a dedicated Settings -> Slideshow tab (new SlideshowSettingsPage), shown only when the flag is on. - Gate the per-event Live Slideshow card and the per-event-type preset section behind the flag too (and stop writing a type preset when it's off). - en/de strings for the feature card + settings tab.
This commit is contained in:
@@ -84,6 +84,10 @@ const KNOWN_FLAGS = [
|
|||||||
// opt-in — operators must register a Meta-approved template before turning
|
// opt-in — operators must register a Meta-approved template before turning
|
||||||
// it on. Independent of email; both can fire on the same event.
|
// it on. Independent of email; both can fire on the same event.
|
||||||
'whatsapp',
|
'whatsapp',
|
||||||
|
// Live Slideshow ("Diashow") — the per-event fullscreen kiosk link + its
|
||||||
|
// per-event-type presets and global watermark defaults tab. Strictly opt-in;
|
||||||
|
// gates all slideshow admin UI (per-event card, type preset, settings tab).
|
||||||
|
'slideshow',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Spec defaults for any flag missing from the DB (e.g. a row added by a
|
// Spec defaults for any flag missing from the DB (e.g. a row added by a
|
||||||
@@ -112,6 +116,7 @@ const DEFAULT_FLAGS = {
|
|||||||
expenses: false,
|
expenses: false,
|
||||||
projects: false,
|
projects: false,
|
||||||
whatsapp: false,
|
whatsapp: false,
|
||||||
|
slideshow: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function readAllFlags() {
|
async function readAllFlags() {
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ export const DEFAULT_FLAGS: FeatureFlags = {
|
|||||||
projects: false,
|
projects: false,
|
||||||
// WhatsApp Business API delivery channel (migration 136, #640D).
|
// WhatsApp Business API delivery channel (migration 136, #640D).
|
||||||
whatsapp: false,
|
whatsapp: false,
|
||||||
|
// Live Slideshow ("Diashow") — opt-in; gates all slideshow admin UI.
|
||||||
|
slideshow: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FEATURE_FLAGS_QUERY_KEY = ['feature-flags'] as const;
|
export const FEATURE_FLAGS_QUERY_KEY = ['feature-flags'] as const;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
ScanLine,
|
ScanLine,
|
||||||
Wallet,
|
Wallet,
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
|
MonitorPlay,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Card } from '../../../components/common';
|
import { Button, Card } from '../../../components/common';
|
||||||
@@ -116,6 +117,21 @@ export const FeaturesTab: React.FC = () => {
|
|||||||
"Galleries are the foundation of PicPeak and can't be turned off.",
|
"Galleries are the foundation of PicPeak and can't be turned off.",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FeatureCard
|
||||||
|
icon={MonitorPlay}
|
||||||
|
title={t('settings.features.slideshow.title', 'Live Slideshow')}
|
||||||
|
description={t(
|
||||||
|
'settings.features.slideshow.description',
|
||||||
|
'A separate fullscreen "Diashow" link per event for projectors at live events — auto-picks-up new uploads, with per-event-type presets and global watermark defaults under Settings → Slideshow.',
|
||||||
|
)}
|
||||||
|
status="new"
|
||||||
|
statusLabel={statusLabel('new')}
|
||||||
|
sidebarHidden
|
||||||
|
sidebarHiddenLabel={sidebarHiddenLabel}
|
||||||
|
enabled={staged.slideshow}
|
||||||
|
onToggle={(next) => setFlag('slideshow', next)}
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Clients (#354 follow-up). Visual grouping for the CRM-area
|
{/* Clients (#354 follow-up). Visual grouping for the CRM-area
|
||||||
|
|||||||
@@ -1768,6 +1768,10 @@
|
|||||||
"whatsapp": {
|
"whatsapp": {
|
||||||
"title": "WhatsApp",
|
"title": "WhatsApp",
|
||||||
"description": "Liefert die Gallerie-Bereit-Benachrichtigung zusätzlich zur E-Mail über die WhatsApp Business API. Voraussetzung: Meta-Business-Konto, genehmigte Nachrichtenvorlage und eine Kunden-Telefonnummer am Event. Zugangsdaten unter Einstellungen → WhatsApp konfigurieren."
|
"description": "Liefert die Gallerie-Bereit-Benachrichtigung zusätzlich zur E-Mail über die WhatsApp Business API. Voraussetzung: Meta-Business-Konto, genehmigte Nachrichtenvorlage und eine Kunden-Telefonnummer am Event. Zugangsdaten unter Einstellungen → WhatsApp konfigurieren."
|
||||||
|
},
|
||||||
|
"slideshow": {
|
||||||
|
"title": "Live-Diashow",
|
||||||
|
"description": "Ein separater Vollbild-„Diashow“-Link pro Event für Beamer bei Live-Events – übernimmt neue Uploads automatisch, mit Voreinstellungen je Event-Typ und globalen Wasserzeichen-Vorgaben unter Einstellungen → Diashow."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"customerSurface": {
|
"customerSurface": {
|
||||||
@@ -1815,6 +1819,10 @@
|
|||||||
"hourlyRateHint": "Verrechnungs-Fallback, wenn ein Kunde keinen eigenen Satz hat (Stundenerfassung). In {{currency}}, in Hauptwährungseinheiten. Leer lassen, um einen Satz pro Kunde oder pro Eintrag zu verlangen."
|
"hourlyRateHint": "Verrechnungs-Fallback, wenn ein Kunde keinen eigenen Satz hat (Stundenerfassung). In {{currency}}, in Hauptwährungseinheiten. Leer lassen, um einen Satz pro Kunde oder pro Eintrag zu verlangen."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"slideshow": {
|
||||||
|
"title": "Diashow",
|
||||||
|
"subtitle": "Globale Vorgaben für die Live-Diashow. Events und Event-Typen können diese überschreiben."
|
||||||
|
},
|
||||||
"whatsapp": {
|
"whatsapp": {
|
||||||
"title": "WhatsApp",
|
"title": "WhatsApp",
|
||||||
"subtitle": "Meta-Business-Zugangsdaten konfigurieren, um die Gallerie-Bereit-Benachrichtigung zusätzlich zur E-Mail per WhatsApp zu senden.",
|
"subtitle": "Meta-Business-Zugangsdaten konfigurieren, um die Gallerie-Bereit-Benachrichtigung zusätzlich zur E-Mail per WhatsApp zu senden.",
|
||||||
|
|||||||
@@ -1324,6 +1324,10 @@
|
|||||||
"whatsapp": {
|
"whatsapp": {
|
||||||
"title": "WhatsApp",
|
"title": "WhatsApp",
|
||||||
"description": "Deliver the gallery-ready notification via WhatsApp Business API in addition to email. Requires a Meta Business Account, an approved message template, and a customer phone number on the event. Configure credentials under Settings → WhatsApp."
|
"description": "Deliver the gallery-ready notification via WhatsApp Business API in addition to email. Requires a Meta Business Account, an approved message template, and a customer phone number on the event. Configure credentials under Settings → WhatsApp."
|
||||||
|
},
|
||||||
|
"slideshow": {
|
||||||
|
"title": "Live Slideshow",
|
||||||
|
"description": "A separate fullscreen \"Diashow\" link per event for projectors at live events — auto-picks-up new uploads, with per-event-type presets and global watermark defaults under Settings → Slideshow."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"customerSurface": {
|
"customerSurface": {
|
||||||
@@ -1371,6 +1375,10 @@
|
|||||||
"hourlyRateHint": "Billing fallback used when a customer has no own rate (hours logging). In {{currency}}, major units. Leave blank to require a per-customer or per-entry rate."
|
"hourlyRateHint": "Billing fallback used when a customer has no own rate (hours logging). In {{currency}}, major units. Leave blank to require a per-customer or per-entry rate."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"slideshow": {
|
||||||
|
"title": "Slideshow",
|
||||||
|
"subtitle": "Global defaults for the Live Slideshow. Events and event types can override these."
|
||||||
|
},
|
||||||
"whatsapp": {
|
"whatsapp": {
|
||||||
"title": "WhatsApp",
|
"title": "WhatsApp",
|
||||||
"subtitle": "Configure Meta Business credentials to deliver the gallery-ready notification via WhatsApp alongside email.",
|
"subtitle": "Configure Meta Business credentials to deliver the gallery-ready notification via WhatsApp alongside email.",
|
||||||
|
|||||||
@@ -2159,7 +2159,9 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Live Slideshow ("Diashow") link + live display settings (migration 137) */}
|
{/* Live Slideshow ("Diashow") link + live display settings (migrations 137/138).
|
||||||
|
Gated behind the `slideshow` feature flag. */}
|
||||||
|
{flags.slideshow && (
|
||||||
<SlideshowSettingsCard
|
<SlideshowSettingsCard
|
||||||
eventId={event.id}
|
eventId={event.id}
|
||||||
slug={event.slug}
|
slug={event.slug}
|
||||||
@@ -2179,6 +2181,7 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
onChanged={() => refetchEvent()}
|
onChanged={() => refetchEvent()}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Pre-event reminder override (migration 143). Hidden when
|
{/* Pre-event reminder override (migration 143). Hidden when
|
||||||
the reminderEmails master flag is off — the override here
|
the reminderEmails master flag is off — the override here
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import { Button, Input, Card, Loading } from '../../components/common';
|
|||||||
import { eventTypesService, EventType, CreateEventTypeData, UpdateEventTypeData } from '../../services/eventTypes.service';
|
import { eventTypesService, EventType, CreateEventTypeData, UpdateEventTypeData } from '../../services/eventTypes.service';
|
||||||
import { GALLERY_THEME_PRESETS } from '../../types/theme.types';
|
import { GALLERY_THEME_PRESETS } from '../../types/theme.types';
|
||||||
import { SlideshowStyleFields } from '../../components/admin/SlideshowStyleFields';
|
import { SlideshowStyleFields } from '../../components/admin/SlideshowStyleFields';
|
||||||
import { SlideshowGlobalDefaultsCard } from '../../components/admin/SlideshowGlobalDefaultsCard';
|
|
||||||
import { DEFAULT_SLIDESHOW_STYLE, type SlideshowStyle } from '../../services/slideshow.service';
|
import { DEFAULT_SLIDESHOW_STYLE, type SlideshowStyle } from '../../services/slideshow.service';
|
||||||
|
import { useFeatureEnabled } from '../../contexts/FeatureFlagsContext';
|
||||||
|
|
||||||
// Parse a type's stored slideshow_preset (JSON string | object | null) into a
|
// Parse a type's stored slideshow_preset (JSON string | object | null) into a
|
||||||
// full SlideshowStyle, falling back to defaults for any missing keys.
|
// full SlideshowStyle, falling back to defaults for any missing keys.
|
||||||
@@ -152,9 +152,6 @@ export const EventTypesPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Global Live Slideshow watermark default (per-event/type can override) */}
|
|
||||||
<SlideshowGlobalDefaultsCard />
|
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<Card className="mb-6">
|
<Card className="mb-6">
|
||||||
<div className="p-4 flex flex-col sm:flex-row gap-4">
|
<div className="p-4 flex flex-col sm:flex-row gap-4">
|
||||||
@@ -339,6 +336,7 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isEditing = !!eventType;
|
const isEditing = !!eventType;
|
||||||
|
const slideshowEnabled = useFeatureEnabled('slideshow');
|
||||||
|
|
||||||
const [form, setForm] = useState<CreateEventTypeData>({
|
const [form, setForm] = useState<CreateEventTypeData>({
|
||||||
name: eventType?.name || '',
|
name: eventType?.name || '',
|
||||||
@@ -382,10 +380,10 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
|
|||||||
if (form.emoji !== eventType?.emoji) updates.emoji = form.emoji;
|
if (form.emoji !== eventType?.emoji) updates.emoji = form.emoji;
|
||||||
if (form.theme_preset !== eventType?.theme_preset) updates.theme_preset = form.theme_preset;
|
if (form.theme_preset !== eventType?.theme_preset) updates.theme_preset = form.theme_preset;
|
||||||
const originalStyle = JSON.stringify(parseSlideshowPreset(eventType?.slideshow_preset));
|
const originalStyle = JSON.stringify(parseSlideshowPreset(eventType?.slideshow_preset));
|
||||||
if (JSON.stringify(slideshowStyle) !== originalStyle) updates.slideshow_preset = slideshowStyle;
|
if (slideshowEnabled && JSON.stringify(slideshowStyle) !== originalStyle) updates.slideshow_preset = slideshowStyle;
|
||||||
onSubmit(updates);
|
onSubmit(updates);
|
||||||
} else {
|
} else {
|
||||||
onSubmit({ ...form, slideshow_preset: slideshowStyle });
|
onSubmit({ ...form, ...(slideshowEnabled ? { slideshow_preset: slideshowStyle } : {}) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -488,7 +486,9 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
|
|||||||
|
|
||||||
{/* Live Slideshow preset (migration 138). New events of this type
|
{/* Live Slideshow preset (migration 138). New events of this type
|
||||||
inherit these slideshow defaults; admins can still override
|
inherit these slideshow defaults; admins can still override
|
||||||
per event on the event detail page. */}
|
per event on the event detail page. Gated behind the
|
||||||
|
`slideshow` feature flag. */}
|
||||||
|
{slideshowEnabled && (
|
||||||
<div className="pt-2 border-t border-neutral-200 dark:border-neutral-700">
|
<div className="pt-2 border-t border-neutral-200 dark:border-neutral-700">
|
||||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||||
{t('eventTypes.form.slideshowPreset', 'Slideshow preset')}
|
{t('eventTypes.form.slideshowPreset', 'Slideshow preset')}
|
||||||
@@ -498,6 +498,7 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
<SlideshowStyleFields value={slideshowStyle} onChange={setSlideshowStyle} />
|
<SlideshowStyleFields value={slideshowStyle} onChange={setSlideshowStyle} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Active toggle for editing */}
|
{/* Active toggle for editing */}
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
import { EmailConfigPage } from './EmailConfigPage';
|
import { EmailConfigPage } from './EmailConfigPage';
|
||||||
import { BrandingPage } from './BrandingPage';
|
import { BrandingPage } from './BrandingPage';
|
||||||
import { EventTypesPage } from './EventTypesPage';
|
import { EventTypesPage } from './EventTypesPage';
|
||||||
|
import { SlideshowSettingsPage } from './SlideshowSettingsPage';
|
||||||
import { BackupManagement } from './BackupManagement';
|
import { BackupManagement } from './BackupManagement';
|
||||||
import { CMSPage } from './CMSPage';
|
import { CMSPage } from './CMSPage';
|
||||||
// CRM (#TBD)
|
// CRM (#TBD)
|
||||||
@@ -54,7 +55,7 @@ import { CrmSettingsPage } from './settings/CrmSettingsPage';
|
|||||||
import { ReminderTemplatesPage } from './settings/ReminderTemplatesPage';
|
import { ReminderTemplatesPage } from './settings/ReminderTemplatesPage';
|
||||||
import { BlockLibraryPage } from './contracts/BlockLibraryPage';
|
import { BlockLibraryPage } from './contracts/BlockLibraryPage';
|
||||||
import { useFeatureFlags } from '../../contexts/FeatureFlagsContext';
|
import { useFeatureFlags } from '../../contexts/FeatureFlagsContext';
|
||||||
import { Briefcase, Receipt, ScrollText, Mail, Landmark, Smartphone } from 'lucide-react';
|
import { Briefcase, Receipt, ScrollText, Mail, Landmark, Smartphone, MonitorPlay } from 'lucide-react';
|
||||||
|
|
||||||
// Tab keys driving the inner-nav. Must include every key used in
|
// Tab keys driving the inner-nav. Must include every key used in
|
||||||
// `navGroups` below and in the switch at the bottom of the component.
|
// `navGroups` below and in the switch at the bottom of the component.
|
||||||
@@ -85,7 +86,8 @@ type TabType =
|
|||||||
| 'contracts'
|
| 'contracts'
|
||||||
| 'reminderTemplates'
|
| 'reminderTemplates'
|
||||||
| 'accounting'
|
| 'accounting'
|
||||||
| 'whatsapp';
|
| 'whatsapp'
|
||||||
|
| 'slideshow';
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
key: TabType;
|
key: TabType;
|
||||||
@@ -106,6 +108,7 @@ const ALL_TAB_KEYS: TabType[] = [
|
|||||||
'apiTokens', 'webhooks',
|
'apiTokens', 'webhooks',
|
||||||
'status', 'analytics', 'backup',
|
'status', 'analytics', 'backup',
|
||||||
'businessProfile', 'crm', 'contracts', 'reminderTemplates', 'accounting', 'whatsapp',
|
'businessProfile', 'crm', 'contracts', 'reminderTemplates', 'accounting', 'whatsapp',
|
||||||
|
'slideshow',
|
||||||
];
|
];
|
||||||
|
|
||||||
function isValidTab(value: string | null): value is TabType {
|
function isValidTab(value: string | null): value is TabType {
|
||||||
@@ -203,12 +206,13 @@ export const SettingsPage: React.FC = () => {
|
|||||||
reminderTemplates: !flags.reminderEmails,
|
reminderTemplates: !flags.reminderEmails,
|
||||||
accounting: !flags.accounting,
|
accounting: !flags.accounting,
|
||||||
whatsapp: !flags.whatsapp,
|
whatsapp: !flags.whatsapp,
|
||||||
|
slideshow: !flags.slideshow,
|
||||||
};
|
};
|
||||||
if (gatedOff[activeTab]) {
|
if (gatedOff[activeTab]) {
|
||||||
setActiveTab('features');
|
setActiveTab('features');
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [flagsLoading, flags.quotes, flags.bills, flags.contracts, flags.reminderEmails, flags.accounting, flags.whatsapp, activeTab]);
|
}, [flagsLoading, flags.quotes, flags.bills, flags.contracts, flags.reminderEmails, flags.accounting, flags.whatsapp, flags.slideshow, activeTab]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -240,6 +244,9 @@ export const SettingsPage: React.FC = () => {
|
|||||||
{ key: 'thumbnails', label: t('settings.thumbnails.title', 'Thumbnails'), icon: ImageIcon },
|
{ key: 'thumbnails', label: t('settings.thumbnails.title', 'Thumbnails'), icon: ImageIcon },
|
||||||
{ key: 'styling', label: t('settings.styling.title', 'Custom CSS'), icon: Code },
|
{ key: 'styling', label: t('settings.styling.title', 'Custom CSS'), icon: Code },
|
||||||
{ key: 'cms', label: t('settings.cms.title', 'CMS Pages'), icon: FileText },
|
{ key: 'cms', label: t('settings.cms.title', 'CMS Pages'), icon: FileText },
|
||||||
|
...(flags.slideshow
|
||||||
|
? [{ key: 'slideshow' as const, label: t('settings.slideshow.title', 'Slideshow'), icon: MonitorPlay }]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -433,6 +440,7 @@ export const SettingsPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'eventTypes' && <EventTypesPage />}
|
{activeTab === 'eventTypes' && <EventTypesPage />}
|
||||||
|
{activeTab === 'slideshow' && <SlideshowSettingsPage />}
|
||||||
{activeTab === 'branding' && <BrandingPage />}
|
{activeTab === 'branding' && <BrandingPage />}
|
||||||
{activeTab === 'cms' && <CMSPage />}
|
{activeTab === 'cms' && <CMSPage />}
|
||||||
{activeTab === 'email' && <EmailConfigPage />}
|
{activeTab === 'email' && <EmailConfigPage />}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Settings → Slideshow tab. Top-level (global) Live Slideshow settings.
|
||||||
|
* Currently the global watermark default (source/position/opacity/style) that
|
||||||
|
* every event inherits unless it overrides. Gated behind the `slideshow`
|
||||||
|
* feature flag (see SettingsPage nav).
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { SlideshowGlobalDefaultsCard } from '../../components/admin/SlideshowGlobalDefaultsCard';
|
||||||
|
|
||||||
|
export const SlideshowSettingsPage: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
|
{t('settings.slideshow.title', 'Slideshow')}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1">
|
||||||
|
{t('settings.slideshow.subtitle', 'Global defaults for the Live Slideshow. Events and event types can override these.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SlideshowGlobalDefaultsCard />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SlideshowSettingsPage;
|
||||||
@@ -63,7 +63,10 @@ export type FeatureKey =
|
|||||||
// Strictly opt-in — requires a Meta Business Account, an approved
|
// Strictly opt-in — requires a Meta Business Account, an approved
|
||||||
// message template, and a Meta access token. Independent of email; both
|
// message template, and a Meta access token. Independent of email; both
|
||||||
// can fire on the same event.
|
// can fire on the same event.
|
||||||
| 'whatsapp';
|
| 'whatsapp'
|
||||||
|
// Live Slideshow ("Diashow") — per-event fullscreen kiosk link + presets +
|
||||||
|
// global watermark settings tab. Strictly opt-in; gates all slideshow UI.
|
||||||
|
| 'slideshow';
|
||||||
|
|
||||||
export type FeatureFlags = Record<FeatureKey, boolean>;
|
export type FeatureFlags = Record<FeatureKey, boolean>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user