completed addon checkout in mobile event admin
This commit is contained in:
@@ -5,17 +5,20 @@ namespace App\Services\Addons;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventPackageAddon;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Paddle\PaddleCustomerService;
|
||||
use App\Services\Paddle\PaddleClient;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
|
||||
class EventAddonCheckoutService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EventAddonCatalog $catalog,
|
||||
private readonly PaddleClient $paddle,
|
||||
private readonly PaddleCustomerService $customers,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -29,6 +32,14 @@ class EventAddonCheckoutService
|
||||
$acceptedWaiver = (bool) ($payload['accepted_waiver'] ?? false);
|
||||
$acceptedTerms = (bool) ($payload['accepted_terms'] ?? false);
|
||||
|
||||
try {
|
||||
$customerId = $this->customers->ensureCustomerId($tenant);
|
||||
} catch (Throwable $exception) {
|
||||
throw ValidationException::withMessages([
|
||||
'customer' => __('Konnte Paddle-Kundenkonto nicht anlegen: :message', ['message' => $exception->getMessage()]),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $addonKey || ! $this->catalog->find($addonKey)) {
|
||||
throw ValidationException::withMessages([
|
||||
'addon_key' => __('Unbekanntes Add-on.'),
|
||||
@@ -68,7 +79,7 @@ class EventAddonCheckoutService
|
||||
];
|
||||
|
||||
$requestPayload = array_filter([
|
||||
'customer_id' => $tenant->paddle_customer_id,
|
||||
'customer_id' => $customerId,
|
||||
'items' => [
|
||||
[
|
||||
'price_id' => $priceId,
|
||||
|
||||
@@ -325,6 +325,16 @@
|
||||
"success": {
|
||||
"addonApplied": "Add-on angewendet. Limits aktualisieren sich in Kürze."
|
||||
},
|
||||
"legalConsent": {
|
||||
"title": "Vor dem Kauf",
|
||||
"description": "Bitte bestätige die rechtlichen Hinweise, bevor du ein Add-on kaufst.",
|
||||
"checkboxTerms": "Ich habe die AGB, die Datenschutzerklärung und die Widerrufsbelehrung gelesen und akzeptiere sie.",
|
||||
"checkboxWaiver": "Ich verlange ausdrücklich, dass mit der Bereitstellung der digitalen Leistung (Aktivierung meines Event-Add-ons) vor Ablauf der Widerrufsfrist begonnen wird. Mir ist bekannt, dass ich mein Widerrufsrecht verliere, sobald der Vertrag vollständig erfüllt ist.",
|
||||
"errorTerms": "Bitte bestätige AGB, Datenschutzerklärung und Widerrufsbelehrung.",
|
||||
"errorWaiver": "Bitte bestätige den sofortigen Leistungsbeginn und das vorzeitige Erlöschen des Widerrufsrechts.",
|
||||
"confirm": "Weiter zum Checkout",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"placeholders": {
|
||||
"untitled": "Unbenanntes Event"
|
||||
},
|
||||
|
||||
@@ -928,6 +928,16 @@
|
||||
"success": {
|
||||
"addonApplied": "Add-on applied. Limits will refresh shortly."
|
||||
},
|
||||
"legalConsent": {
|
||||
"title": "Before purchase",
|
||||
"description": "Please confirm the legal notes before buying an add-on.",
|
||||
"checkboxTerms": "I have read and accept the Terms & Conditions, Privacy Policy, and Right of Withdrawal.",
|
||||
"checkboxWaiver": "I expressly request that you begin providing the digital services (activation of my event add-on) before the withdrawal period has expired. I understand that I lose my right of withdrawal once the contract has been fully performed.",
|
||||
"errorTerms": "Please confirm you accept the terms, privacy policy, and right of withdrawal.",
|
||||
"errorWaiver": "Please confirm the immediate start of the digital service and the early expiry of the right of withdrawal.",
|
||||
"confirm": "Continue to checkout",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"placeholders": {
|
||||
"untitled": "Untitled event"
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useTheme } from '@tamagui/core';
|
||||
import { buildLimitWarnings } from '../lib/limitWarnings';
|
||||
import { adminPath } from '../constants';
|
||||
import { scopeDefaults, selectAddonKeyForScope } from './addons';
|
||||
import { LegalConsentSheet } from './components/LegalConsentSheet';
|
||||
|
||||
type FilterKey = 'all' | 'featured' | 'hidden' | 'pending';
|
||||
|
||||
@@ -59,6 +60,9 @@ export default function MobileEventPhotosPage() {
|
||||
const [catalogAddons, setCatalogAddons] = React.useState<EventAddonCatalogItem[]>([]);
|
||||
const [eventAddons, setEventAddons] = React.useState<EventAddonSummary[]>([]);
|
||||
const [busyScope, setBusyScope] = React.useState<string | null>(null);
|
||||
const [consentOpen, setConsentOpen] = React.useState(false);
|
||||
const [consentTarget, setConsentTarget] = React.useState<{ scope: 'photos' | 'gallery' | 'guests'; addonKey: string } | null>(null);
|
||||
const [consentBusy, setConsentBusy] = React.useState(false);
|
||||
const theme = useTheme();
|
||||
const text = String(theme.color?.val ?? '#111827');
|
||||
const muted = String(theme.gray?.val ?? '#4b5563');
|
||||
@@ -209,6 +213,62 @@ export default function MobileEventPhotosPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveScopeAndAddonKey(scopeOrKey: 'photos' | 'gallery' | 'guests' | string) {
|
||||
const scope =
|
||||
scopeOrKey === 'photos' || scopeOrKey === 'gallery' || scopeOrKey === 'guests'
|
||||
? scopeOrKey
|
||||
: scopeOrKey.includes('gallery')
|
||||
? 'gallery'
|
||||
: scopeOrKey.includes('guest')
|
||||
? 'guests'
|
||||
: 'photos';
|
||||
|
||||
const addonKey =
|
||||
scopeOrKey === 'photos' || scopeOrKey === 'gallery' || scopeOrKey === 'guests'
|
||||
? selectAddonKeyForScope(catalogAddons, scope)
|
||||
: scopeOrKey;
|
||||
|
||||
return { scope, addonKey };
|
||||
}
|
||||
|
||||
function startAddonCheckout(scopeOrKey: 'photos' | 'gallery' | 'guests' | string) {
|
||||
if (!slug) return;
|
||||
const { scope, addonKey } = resolveScopeAndAddonKey(scopeOrKey);
|
||||
setConsentTarget({ scope, 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}/photos`)}` : '';
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MobileShell
|
||||
activeTab="uploads"
|
||||
@@ -270,7 +330,7 @@ export default function MobileEventPhotosPage() {
|
||||
<LimitWarnings
|
||||
limits={limits}
|
||||
addons={catalogAddons}
|
||||
onCheckout={(scopeOrKey) => { void handleCheckout(scopeOrKey, slug, catalogAddons, setBusyScope, t); }}
|
||||
onCheckout={startAddonCheckout}
|
||||
busyScope={busyScope}
|
||||
translate={translateLimits(t)}
|
||||
textColor={text}
|
||||
@@ -465,6 +525,18 @@ export default function MobileEventPhotosPage() {
|
||||
<EventAddonList addons={eventAddons} textColor={text} mutedColor={muted} />
|
||||
</YStack>
|
||||
) : null}
|
||||
|
||||
<LegalConsentSheet
|
||||
open={consentOpen}
|
||||
onClose={() => {
|
||||
if (consentBusy) return;
|
||||
setConsentOpen(false);
|
||||
setConsentTarget(null);
|
||||
}}
|
||||
onConfirm={confirmAddonCheckout}
|
||||
busy={consentBusy}
|
||||
t={t}
|
||||
/>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
@@ -648,45 +720,3 @@ function EventAddonList({ addons, textColor, mutedColor }: { addons: EventAddonS
|
||||
</YStack>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleCheckout(
|
||||
scopeOrKey: 'photos' | 'gallery' | 'guests' | string,
|
||||
slug: string | null,
|
||||
addons: EventAddonCatalogItem[],
|
||||
setBusyScope: (scope: string | null) => void,
|
||||
t: (key: string, defaultValue?: string) => string,
|
||||
): Promise<void> {
|
||||
if (!slug) return;
|
||||
const scope =
|
||||
scopeOrKey === 'photos' || scopeOrKey === 'gallery' || scopeOrKey === 'guests'
|
||||
? scopeOrKey
|
||||
: scopeOrKey.includes('gallery')
|
||||
? 'gallery'
|
||||
: scopeOrKey.includes('guest')
|
||||
? 'guests'
|
||||
: 'photos';
|
||||
const addonKey =
|
||||
scopeOrKey === 'photos' || scopeOrKey === 'gallery' || scopeOrKey === 'guests'
|
||||
? selectAddonKeyForScope(addons, scope)
|
||||
: scopeOrKey;
|
||||
const currentUrl = typeof window !== 'undefined' ? `${window.location.origin}${adminPath(`/mobile/events/${slug}/photos`)}` : '';
|
||||
const successUrl = `${currentUrl}?addon_success=1`;
|
||||
setBusyScope(scope);
|
||||
try {
|
||||
const checkout = await createEventAddonCheckout(slug, {
|
||||
addon_key: addonKey,
|
||||
quantity: 1,
|
||||
success_url: successUrl,
|
||||
cancel_url: currentUrl,
|
||||
});
|
||||
if (checkout.checkout_url) {
|
||||
window.location.href = checkout.checkout_url;
|
||||
} else {
|
||||
toast.error(t('mobileBilling.checkoutUnavailable', 'Checkout unavailable right now.'));
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(getApiErrorMessage(err, t('mobileBilling.checkoutFailed', 'Checkout failed.')));
|
||||
} finally {
|
||||
setBusyScope(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { MobileShell } from './components/MobileShell';
|
||||
import { MobileCard, CTAButton, PillBadge } from './components/Primitives';
|
||||
import { adminPath } from '../constants';
|
||||
import { selectAddonKeyForScope } from './addons';
|
||||
import { LegalConsentSheet } from './components/LegalConsentSheet';
|
||||
|
||||
export default function MobileEventRecapPage() {
|
||||
const { slug } = useParams<{ slug?: string }>();
|
||||
@@ -42,6 +43,9 @@ export default function MobileEventRecapPage() {
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const [archiveBusy, setArchiveBusy] = React.useState(false);
|
||||
const [checkoutBusy, setCheckoutBusy] = React.useState(false);
|
||||
const [consentOpen, setConsentOpen] = React.useState(false);
|
||||
const [consentBusy, setConsentBusy] = React.useState(false);
|
||||
const [consentAddonKey, setConsentAddonKey] = React.useState<string | null>(null);
|
||||
|
||||
const load = React.useCallback(async () => {
|
||||
if (!slug) return;
|
||||
@@ -131,18 +135,27 @@ export default function MobileEventRecapPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkoutAddon() {
|
||||
function startAddonCheckout() {
|
||||
if (!slug) return;
|
||||
const addonKey = selectAddonKeyForScope(addons, 'gallery');
|
||||
setConsentAddonKey(addonKey);
|
||||
setConsentOpen(true);
|
||||
}
|
||||
|
||||
async function confirmAddonCheckout(consents: { acceptedTerms: boolean; acceptedWaiver: boolean }) {
|
||||
if (!slug || !consentAddonKey) return;
|
||||
const currentUrl = typeof window !== 'undefined' ? `${window.location.origin}${adminPath(`/mobile/events/${slug}/recap`)}` : '';
|
||||
const successUrl = `${currentUrl}?addon_success=1`;
|
||||
setCheckoutBusy(true);
|
||||
setConsentBusy(true);
|
||||
try {
|
||||
const checkout = await createEventAddonCheckout(slug, {
|
||||
addon_key: addonKey,
|
||||
addon_key: consentAddonKey,
|
||||
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;
|
||||
@@ -155,6 +168,9 @@ export default function MobileEventRecapPage() {
|
||||
}
|
||||
} finally {
|
||||
setCheckoutBusy(false);
|
||||
setConsentBusy(false);
|
||||
setConsentOpen(false);
|
||||
setConsentAddonKey(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +301,7 @@ export default function MobileEventRecapPage() {
|
||||
<CTAButton
|
||||
label={t('events.recap.extendGallery', 'Galerie verlängern')}
|
||||
onPress={() => {
|
||||
void checkoutAddon();
|
||||
startAddonCheckout();
|
||||
}}
|
||||
loading={checkoutBusy}
|
||||
/>
|
||||
@@ -338,6 +354,17 @@ export default function MobileEventRecapPage() {
|
||||
</MobileCard>
|
||||
</YStack>
|
||||
) : null}
|
||||
<LegalConsentSheet
|
||||
open={consentOpen}
|
||||
onClose={() => {
|
||||
if (consentBusy) return;
|
||||
setConsentOpen(false);
|
||||
setConsentAddonKey(null);
|
||||
}}
|
||||
onConfirm={confirmAddonCheckout}
|
||||
busy={consentBusy}
|
||||
t={t}
|
||||
/>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown } from 'lucide-react';
|
||||
import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { ListItem } from '@tamagui/list-item';
|
||||
@@ -408,6 +408,8 @@ export default function MobileEventTasksPage() {
|
||||
<YStack borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
||||
<Pressable onPress={() => setShowTaskSheet(true)}>
|
||||
<ListItem
|
||||
hoverTheme
|
||||
pressTheme
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack
|
||||
@@ -432,11 +434,14 @@ export default function MobileEventTasksPage() {
|
||||
}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
iconAfter={<ChevronRight size={16} color={muted} />}
|
||||
/>
|
||||
</Pressable>
|
||||
<InlineSeparator />
|
||||
<Pressable onPress={() => setShowCollectionSheet(true)}>
|
||||
<ListItem
|
||||
hoverTheme
|
||||
pressTheme
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<YStack
|
||||
@@ -461,6 +466,7 @@ export default function MobileEventTasksPage() {
|
||||
}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
iconAfter={<ChevronRight size={16} color={muted} />}
|
||||
/>
|
||||
</Pressable>
|
||||
</YStack>
|
||||
@@ -502,6 +508,8 @@ export default function MobileEventTasksPage() {
|
||||
<React.Fragment key={task.id}>
|
||||
<Pressable onPress={() => startEdit(task)}>
|
||||
<ListItem
|
||||
hoverTheme
|
||||
pressTheme
|
||||
title={
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{task.title}
|
||||
@@ -522,6 +530,7 @@ export default function MobileEventTasksPage() {
|
||||
<Pressable disabled={busyId === task.id} onPress={() => detachTask(task.id)}>
|
||||
<Trash2 size={14} color={danger} />
|
||||
</Pressable>
|
||||
<ChevronRight size={14} color={subtle} />
|
||||
</XStack>
|
||||
}
|
||||
paddingVertical="$2"
|
||||
@@ -556,6 +565,8 @@ export default function MobileEventTasksPage() {
|
||||
{(expandedLibrary ? library : library.slice(0, 6)).map((task, idx, arr) => (
|
||||
<React.Fragment key={`lib-${task.id}`}>
|
||||
<ListItem
|
||||
hoverTheme
|
||||
pressTheme
|
||||
title={
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{task.title}
|
||||
@@ -569,11 +580,14 @@ export default function MobileEventTasksPage() {
|
||||
) : null
|
||||
}
|
||||
iconAfter={
|
||||
<XStack space="$1.5" alignItems="center">
|
||||
<Pressable onPress={() => quickAssign(task.id)}>
|
||||
<Text fontSize={12} fontWeight="600" color={primary}>
|
||||
{assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<ChevronRight size={14} color={subtle} />
|
||||
</XStack>
|
||||
}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
@@ -609,6 +623,8 @@ export default function MobileEventTasksPage() {
|
||||
{(expandedCollections ? collections : collections.slice(0, 6)).map((collection, idx, arr) => (
|
||||
<React.Fragment key={collection.id}>
|
||||
<ListItem
|
||||
hoverTheme
|
||||
pressTheme
|
||||
title={
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{collection.name}
|
||||
@@ -622,11 +638,14 @@ export default function MobileEventTasksPage() {
|
||||
) : null
|
||||
}
|
||||
iconAfter={
|
||||
<XStack space="$1.5" alignItems="center">
|
||||
<Pressable onPress={() => importCollection(collection.id)}>
|
||||
<Text fontSize={12} fontWeight="600" color={primary}>
|
||||
{t('events.tasks.import', 'Import')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<ChevronRight size={14} color={subtle} />
|
||||
</XStack>
|
||||
}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
@@ -738,6 +757,8 @@ export default function MobileEventTasksPage() {
|
||||
{emotions.map((em) => (
|
||||
<ListItem
|
||||
key={`emo-${em.id}`}
|
||||
hoverTheme
|
||||
pressTheme
|
||||
title={
|
||||
<XStack alignItems="center" space="$2">
|
||||
<Tag label={em.name ?? ''} color={em.color ?? border} />
|
||||
@@ -756,6 +777,7 @@ export default function MobileEventTasksPage() {
|
||||
<Pressable onPress={() => removeEmotion(em.id)}>
|
||||
<Trash2 size={14} color={danger} />
|
||||
</Pressable>
|
||||
<ChevronRight size={14} color={subtle} />
|
||||
</XStack>
|
||||
}
|
||||
/>
|
||||
@@ -829,6 +851,8 @@ export default function MobileEventTasksPage() {
|
||||
>
|
||||
<YStack space="$1">
|
||||
<ListItem
|
||||
hoverTheme
|
||||
pressTheme
|
||||
title={
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{t('events.tasks.addTask', 'Aufgabe hinzufügen')}
|
||||
@@ -840,8 +864,11 @@ export default function MobileEventTasksPage() {
|
||||
}}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
iconAfter={<ChevronRight size={14} color={subtle} />}
|
||||
/>
|
||||
<ListItem
|
||||
hoverTheme
|
||||
pressTheme
|
||||
title={
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{t('events.tasks.bulkAdd', 'Bulk add')}
|
||||
@@ -853,8 +880,11 @@ export default function MobileEventTasksPage() {
|
||||
}}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
iconAfter={<ChevronRight size={14} color={subtle} />}
|
||||
/>
|
||||
<ListItem
|
||||
hoverTheme
|
||||
pressTheme
|
||||
title={
|
||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||
{t('events.tasks.manageEmotions', 'Manage emotions')}
|
||||
@@ -871,6 +901,7 @@ export default function MobileEventTasksPage() {
|
||||
}}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
iconAfter={<ChevronRight size={14} color={subtle} />}
|
||||
/>
|
||||
</YStack>
|
||||
</MobileSheet>
|
||||
|
||||
100
resources/js/admin/mobile/components/LegalConsentSheet.tsx
Normal file
100
resources/js/admin/mobile/components/LegalConsentSheet.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import { YStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { MobileSheet } from './Sheet';
|
||||
import { CTAButton } from './Primitives';
|
||||
|
||||
type Translator = (key: string, defaultValue?: string) => string;
|
||||
|
||||
type LegalConsentSheetProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (consents: { acceptedTerms: boolean; acceptedWaiver: boolean }) => Promise<void> | void;
|
||||
busy?: boolean;
|
||||
requireWaiver?: boolean;
|
||||
t: Translator;
|
||||
};
|
||||
|
||||
export function LegalConsentSheet({ open, onClose, onConfirm, busy = false, requireWaiver = true, t }: LegalConsentSheetProps) {
|
||||
const [acceptedTerms, setAcceptedTerms] = React.useState(false);
|
||||
const [acceptedWaiver, setAcceptedWaiver] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open) {
|
||||
setAcceptedTerms(false);
|
||||
setAcceptedWaiver(false);
|
||||
setError(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
async function handleConfirm() {
|
||||
if (!acceptedTerms) {
|
||||
setError(t('events.legalConsent.errorTerms', 'Please confirm the terms.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (requireWaiver && !acceptedWaiver) {
|
||||
setError(t('events.legalConsent.errorWaiver', 'Please confirm the waiver.'));
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
await onConfirm({ acceptedTerms, acceptedWaiver: requireWaiver ? acceptedWaiver : true });
|
||||
}
|
||||
|
||||
return (
|
||||
<MobileSheet
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={t('events.legalConsent.title', 'Before purchase')}
|
||||
footer={
|
||||
<YStack space="$2">
|
||||
{error ? (
|
||||
<Text fontSize="$sm" color="#b91c1c">
|
||||
{error}
|
||||
</Text>
|
||||
) : null}
|
||||
<CTAButton label={t('events.legalConsent.confirm', 'Continue to checkout')} onPress={handleConfirm} loading={busy} disabled={busy} />
|
||||
<CTAButton label={t('events.legalConsent.cancel', 'Cancel')} tone="ghost" onPress={onClose} disabled={busy} />
|
||||
</YStack>
|
||||
}
|
||||
>
|
||||
<YStack space="$2">
|
||||
<Text fontSize="$sm" color="#111827">
|
||||
{t('events.legalConsent.description', 'Please confirm the legal notes before buying an add-on.')}
|
||||
</Text>
|
||||
<label style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={acceptedTerms}
|
||||
onChange={(event) => setAcceptedTerms(event.target.checked)}
|
||||
style={{ marginTop: 4, width: 16, height: 16 }}
|
||||
/>
|
||||
<Text fontSize="$sm" color="#111827">
|
||||
{t(
|
||||
'events.legalConsent.checkboxTerms',
|
||||
'I have read and accept the Terms & Conditions, Privacy Policy, and Right of Withdrawal.',
|
||||
)}
|
||||
</Text>
|
||||
</label>
|
||||
{requireWaiver ? (
|
||||
<label style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={acceptedWaiver}
|
||||
onChange={(event) => setAcceptedWaiver(event.target.checked)}
|
||||
style={{ marginTop: 4, width: 16, height: 16 }}
|
||||
/>
|
||||
<Text fontSize="$sm" color="#111827">
|
||||
{t(
|
||||
'events.legalConsent.checkboxWaiver',
|
||||
'I expressly request immediate provision of the digital service and understand my right of withdrawal expires once fulfilled.',
|
||||
)}
|
||||
</Text>
|
||||
</label>
|
||||
) : null}
|
||||
</YStack>
|
||||
</MobileSheet>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user