widerrufsbelehrung hinzugefügt und in den checkout mit eingebunden. refund ins backend eingebaut.
This commit is contained in:
22
resources/js/components/ui/scroll-area.tsx
Normal file
22
resources/js/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
viewportClassName?: string;
|
||||
}
|
||||
|
||||
export const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
|
||||
({ className, children, viewportClassName, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('h-full w-full overflow-auto', viewportClassName)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
ScrollArea.displayName = 'ScrollArea';
|
||||
@@ -185,6 +185,8 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const liveRegionRef = useRef<HTMLDivElement | null>(null);
|
||||
const cameraViewportRef = useRef<HTMLDivElement | null>(null);
|
||||
const cameraShellRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const streamRef = useRef<MediaStream | null>(null);
|
||||
const countdownTimerRef = useRef<number | null>(null);
|
||||
@@ -481,6 +483,25 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
setPreferences((prev) => ({ ...prev, flashPreferred: !prev.flashPreferred }));
|
||||
}, []);
|
||||
|
||||
const handleToggleImmersive = useCallback(async () => {
|
||||
setImmersiveMode((prev) => !prev);
|
||||
const shell = cameraShellRef.current;
|
||||
if (!shell) return;
|
||||
const prefersReducedMotion = typeof window !== 'undefined'
|
||||
? window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches
|
||||
: false;
|
||||
if (prefersReducedMotion) return;
|
||||
try {
|
||||
if (!document.fullscreenElement) {
|
||||
await shell.requestFullscreen?.();
|
||||
} else {
|
||||
await document.exitFullscreen?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Fullscreen toggle failed', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const triggerConfetti = useCallback(async () => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches;
|
||||
@@ -883,14 +904,14 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
) : null;
|
||||
|
||||
const heroOverlay = !task && showHeroOverlay && mode !== 'uploading' ? (
|
||||
<div className="absolute left-4 right-4 top-6 z-30 rounded-3xl border border-white/30 bg-black/60 p-4 text-white shadow-2xl backdrop-blur sm:left-6 sm:right-6 sm:top-8">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-white/70">Bereit für dein Foto?</p>
|
||||
<p className="text-lg font-semibold leading-tight">Teile den Moment mit allen Gästen.</p>
|
||||
<p className="text-sm text-white/80">Zieh dir eine Mission oder starte direkt mit der Kamera.</p>
|
||||
<div className="absolute left-4 right-4 top-4 z-30 rounded-2xl border border-white/25 bg-black/60 px-4 py-3 text-white shadow-2xl backdrop-blur sm:left-6 sm:right-6 sm:top-5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-white/70">Bereit für dein Foto?</p>
|
||||
<p className="text-base font-semibold leading-tight">Teile den Moment mit allen Gästen.</p>
|
||||
<p className="text-xs text-white/75">Zieh eine Mission oder starte direkt.</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="rounded-full bg-white/15 px-3 py-1 text-[11px] font-semibold uppercase tracking-wide text-white/90">
|
||||
<Badge variant="secondary" className="rounded-full bg-white/15 px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-white/90">
|
||||
Live
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -898,7 +919,10 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full bg-white text-black shadow"
|
||||
onClick={() => navigate(tasksUrl)}
|
||||
onClick={() => {
|
||||
setShowHeroOverlay(false);
|
||||
navigate(tasksUrl);
|
||||
}}
|
||||
>
|
||||
Mission ziehen
|
||||
</Button>
|
||||
@@ -906,11 +930,14 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full border border-white/30 bg-white/10 text-white"
|
||||
onClick={() => navigate(tasksUrl)}
|
||||
onClick={() => {
|
||||
setShowHeroOverlay(false);
|
||||
navigate(tasksUrl);
|
||||
}}
|
||||
>
|
||||
Stimmung wählen
|
||||
</Button>
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/5 px-3 py-1 text-xs text-white/85">
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-3 py-1 text-[11px] text-white/85">
|
||||
<Sparkles className="h-4 w-4 text-amber-200" />
|
||||
Mini-Mission: Fang ein Lachen ein
|
||||
</span>
|
||||
@@ -1069,14 +1096,26 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
|
||||
return renderWithDialog(
|
||||
<>
|
||||
<div className="relative flex min-h-screen flex-col gap-6 pt-4" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||
<div
|
||||
ref={cameraShellRef as unknown as React.RefObject<HTMLDivElement>}
|
||||
className="relative flex min-h-screen flex-col gap-4 pb-[calc(env(safe-area-inset-bottom,0px)+12px)] pt-3"
|
||||
style={bodyFont ? { fontFamily: bodyFont } : undefined}
|
||||
>
|
||||
{taskFloatingCard}
|
||||
{heroOverlay}
|
||||
<section
|
||||
className="relative flex min-h-[70vh] flex-col overflow-hidden border border-white/10 bg-black text-white shadow-2xl"
|
||||
className="relative flex flex-col overflow-hidden border border-white/10 bg-black text-white shadow-2xl"
|
||||
style={{ borderRadius: radius }}
|
||||
>
|
||||
<div className="relative aspect-[3/4] sm:aspect-video">
|
||||
<div
|
||||
ref={cameraViewportRef}
|
||||
className="relative w-full"
|
||||
style={{
|
||||
height: 'calc(100vh - 160px)',
|
||||
minHeight: '70vh',
|
||||
maxHeight: '90vh',
|
||||
}}
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
className={cn(
|
||||
@@ -1123,7 +1162,7 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-6 z-30 flex justify-center">
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-4 z-30 flex justify-center">
|
||||
<div className="pointer-events-auto flex items-center gap-2 rounded-full border border-white/20 bg-black/40 px-3 py-2 backdrop-blur">
|
||||
<Button
|
||||
size="icon"
|
||||
@@ -1197,7 +1236,7 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
controlIconButtonBase,
|
||||
immersiveMode && 'border-white bg-white text-black'
|
||||
)}
|
||||
onClick={() => setImmersiveMode((prev) => !prev)}
|
||||
onClick={handleToggleImmersive}
|
||||
title={
|
||||
immersiveMode
|
||||
? t('upload.controls.exitFullscreen', 'Menü einblenden')
|
||||
@@ -1299,9 +1338,9 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
)}
|
||||
{isCountdownActive && (
|
||||
<div
|
||||
className="absolute inset-0 rounded-full"
|
||||
className="absolute inset-1 rounded-full"
|
||||
style={{
|
||||
background: `conic-gradient(#fff ${countdownDegrees}deg, rgba(255,255,255,0.15) ${countdownDegrees}deg)`,
|
||||
background: `conic-gradient(#fff ${countdownDegrees}deg, rgba(255,255,255,0.12) ${countdownDegrees}deg)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,7 @@ const Footer: React.FC = () => {
|
||||
impressum: localizedPath('/impressum'),
|
||||
datenschutz: localizedPath('/datenschutz'),
|
||||
agb: localizedPath('/agb'),
|
||||
widerruf: localizedPath('/widerrufsbelehrung'),
|
||||
kontakt: localizedPath('/kontakt'),
|
||||
}), [localizedPath]);
|
||||
|
||||
@@ -57,6 +58,11 @@ const Footer: React.FC = () => {
|
||||
{t('legal:agb')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={links.widerruf} className="transition-colors hover:text-pink-500 dark:hover:text-pink-300">
|
||||
{t('legal:widerrufsbelehrung')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={links.kontakt} className="transition-colors hover:text-pink-500 dark:hover:text-pink-300">
|
||||
{t('marketing:nav.contact')}
|
||||
|
||||
@@ -10,6 +10,10 @@ import { previewCoupon as requestCouponPreview } from '@/lib/coupons';
|
||||
import type { CouponPreviewResponse } from '@/types/coupon';
|
||||
import { cn } from '@/lib/utils';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
type PaymentStatus = 'idle' | 'processing' | 'ready' | 'error';
|
||||
|
||||
@@ -120,6 +124,9 @@ export const PaymentStep: React.FC = () => {
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [initialised, setInitialised] = useState(false);
|
||||
const [inlineActive, setInlineActive] = useState(false);
|
||||
const [acceptedTerms, setAcceptedTerms] = useState(false);
|
||||
const [acceptedWaiver, setAcceptedWaiver] = useState(false);
|
||||
const [consentError, setConsentError] = useState<string | null>(null);
|
||||
const [couponCode, setCouponCode] = useState<string>(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
@@ -142,6 +149,11 @@ export const PaymentStep: React.FC = () => {
|
||||
const eventCallbackRef = useRef<(event: Record<string, unknown>) => void>();
|
||||
const hasAutoAppliedCoupon = useRef(false);
|
||||
const checkoutContainerClass = 'paddle-checkout-container';
|
||||
const [showWithdrawalModal, setShowWithdrawalModal] = useState(false);
|
||||
const [withdrawalHtml, setWithdrawalHtml] = useState<string | null>(null);
|
||||
const [withdrawalTitle, setWithdrawalTitle] = useState<string | null>(null);
|
||||
const [withdrawalLoading, setWithdrawalLoading] = useState(false);
|
||||
const [withdrawalError, setWithdrawalError] = useState<string | null>(null);
|
||||
|
||||
const paddleLocale = useMemo(() => {
|
||||
const sourceLocale = i18n.language || (typeof document !== 'undefined' ? document.documentElement.lang : null);
|
||||
@@ -149,6 +161,7 @@ export const PaymentStep: React.FC = () => {
|
||||
}, [i18n.language]);
|
||||
|
||||
const isFree = useMemo(() => (selectedPackage ? Number(selectedPackage.price) <= 0 : false), [selectedPackage]);
|
||||
const requiresImmediateWaiver = useMemo(() => Boolean(selectedPackage?.activates_immediately), [selectedPackage]);
|
||||
|
||||
const applyCoupon = useCallback(async (code: string) => {
|
||||
if (!selectedPackage) {
|
||||
@@ -228,6 +241,11 @@ export const PaymentStep: React.FC = () => {
|
||||
}, [couponCode]);
|
||||
|
||||
const handleFreeActivation = async () => {
|
||||
if (!acceptedTerms || (requiresImmediateWaiver && !acceptedWaiver)) {
|
||||
setConsentError(t('checkout.legal.checkbox_terms_error'));
|
||||
return;
|
||||
}
|
||||
|
||||
setPaymentCompleted(true);
|
||||
nextStep();
|
||||
};
|
||||
@@ -237,6 +255,11 @@ export const PaymentStep: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!acceptedTerms || (requiresImmediateWaiver && !acceptedWaiver)) {
|
||||
setConsentError(t('checkout.legal.checkbox_terms_error'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPackage.paddle_price_id) {
|
||||
setStatus('error');
|
||||
setMessage(t('checkout.payment_step.paddle_not_configured'));
|
||||
@@ -282,13 +305,15 @@ export const PaymentStep: React.FC = () => {
|
||||
frameInitialHeight: '550',
|
||||
frameStyle: 'width: 100%; min-width: 320px; background-color: transparent; border: none;',
|
||||
theme: 'light',
|
||||
locale: paddleLocale,
|
||||
},
|
||||
customData: {
|
||||
package_id: String(selectedPackage.id),
|
||||
locale: paddleLocale,
|
||||
},
|
||||
};
|
||||
locale: paddleLocale,
|
||||
},
|
||||
customData: {
|
||||
package_id: String(selectedPackage.id),
|
||||
locale: paddleLocale,
|
||||
accepted_terms: acceptedTerms ? '1' : '0',
|
||||
accepted_waiver: requiresImmediateWaiver && acceptedWaiver ? '1' : '0',
|
||||
},
|
||||
};
|
||||
|
||||
const customerEmail = authUser?.email ?? null;
|
||||
if (customerEmail) {
|
||||
@@ -329,6 +354,8 @@ export const PaymentStep: React.FC = () => {
|
||||
package_id: selectedPackage.id,
|
||||
locale: paddleLocale,
|
||||
coupon_code: couponPreview?.coupon.code ?? undefined,
|
||||
accepted_terms: acceptedTerms,
|
||||
accepted_waiver: requiresImmediateWaiver ? acceptedWaiver : false,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -489,6 +516,31 @@ export const PaymentStep: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openWithdrawalModal = useCallback(async () => {
|
||||
setShowWithdrawalModal(true);
|
||||
|
||||
if (withdrawalHtml || withdrawalLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWithdrawalLoading(true);
|
||||
setWithdrawalError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/legal/widerrufsbelehrung?lang=${paddleLocale}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load withdrawal page (${response.status})`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setWithdrawalHtml(data.body_html || '');
|
||||
setWithdrawalTitle(data.title || t('checkout.legal.link_cancellation'));
|
||||
} catch (error) {
|
||||
setWithdrawalError(t('checkout.legal.modal_error'));
|
||||
} finally {
|
||||
setWithdrawalLoading(false);
|
||||
}
|
||||
}, [paddleLocale, t, withdrawalHtml, withdrawalLoading]);
|
||||
|
||||
if (!selectedPackage) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
@@ -554,15 +606,85 @@ export const PaymentStep: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-stretch gap-3 w-full max-w-sm">
|
||||
<PaddleCta
|
||||
onCheckout={startPaddleCheckout}
|
||||
disabled={status === 'processing'}
|
||||
isProcessing={status === 'processing'}
|
||||
className={cn('bg-white text-[#001835] hover:bg-white/90', PRIMARY_CTA_STYLES)}
|
||||
/>
|
||||
<p className="text-xs text-white/70 text-center">
|
||||
{t('checkout.payment_step.guided_cta_hint')}
|
||||
</p>
|
||||
<div className="space-y-3 rounded-xl border border-white/30 bg-white/10 p-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
id="checkout-terms-hero"
|
||||
checked={acceptedTerms}
|
||||
onCheckedChange={(checked) => {
|
||||
setAcceptedTerms(Boolean(checked));
|
||||
if (consentError) {
|
||||
setConsentError(null);
|
||||
}
|
||||
}}
|
||||
className="border-white/60 data-[state=checked]:bg-white data-[state=checked]:text-[#001835]"
|
||||
/>
|
||||
<div className="space-y-1 text-sm">
|
||||
<Label htmlFor="checkout-terms-hero" className="cursor-pointer text-white">
|
||||
{t('checkout.legal.checkbox_terms_label')}
|
||||
</Label>
|
||||
<p className="text-xs text-white/80">
|
||||
{t('checkout.legal.legal_links_intro')}{' '}
|
||||
<button
|
||||
type="button"
|
||||
className="underline underline-offset-2"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openWithdrawalModal();
|
||||
}}
|
||||
>
|
||||
{t('checkout.legal.open_withdrawal')}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{requiresImmediateWaiver && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
id="checkout-waiver-hero"
|
||||
checked={acceptedWaiver}
|
||||
onCheckedChange={(checked) => {
|
||||
setAcceptedWaiver(Boolean(checked));
|
||||
if (consentError) {
|
||||
setConsentError(null);
|
||||
}
|
||||
}}
|
||||
className="border-white/60 data-[state=checked]:bg-white data-[state=checked]:text-[#001835]"
|
||||
/>
|
||||
<div className="space-y-1 text-sm">
|
||||
<Label htmlFor="checkout-waiver-hero" className="cursor-pointer text-white">
|
||||
{t('checkout.legal.checkbox_digital_content_label')}
|
||||
</Label>
|
||||
<p className="text-xs text-white/80">
|
||||
{t('checkout.legal.hint_subscription_withdrawal')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{consentError && (
|
||||
<div className="flex items-center gap-2 text-sm text-red-200">
|
||||
<XCircle className="h-4 w-4" />
|
||||
<span>{consentError}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<PaddleCta
|
||||
onCheckout={startPaddleCheckout}
|
||||
disabled={status === 'processing' || !acceptedTerms || (requiresImmediateWaiver && !acceptedWaiver)}
|
||||
isProcessing={status === 'processing'}
|
||||
className={cn('bg-white text-[#001835] hover:bg-white/90', PRIMARY_CTA_STYLES)}
|
||||
/>
|
||||
<p className="text-xs text-white/70 text-center">
|
||||
{t('checkout.payment_step.guided_cta_hint')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -632,7 +754,7 @@ export const PaymentStep: React.FC = () => {
|
||||
</p>
|
||||
<PaddleCta
|
||||
onCheckout={startPaddleCheckout}
|
||||
disabled={status === 'processing'}
|
||||
disabled={status === 'processing' || !acceptedTerms || (requiresImmediateWaiver && !acceptedWaiver)}
|
||||
isProcessing={status === 'processing'}
|
||||
className={PRIMARY_CTA_STYLES}
|
||||
/>
|
||||
@@ -664,6 +786,37 @@ export const PaymentStep: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={showWithdrawalModal} onOpenChange={setShowWithdrawalModal}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{withdrawalTitle || t('checkout.legal.link_cancellation')}</DialogTitle>
|
||||
<DialogDescription>{t('checkout.legal.modal_description')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="min-h-[200px]">
|
||||
{withdrawalLoading && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
<span>{t('checkout.legal.modal_loading')}</span>
|
||||
</div>
|
||||
)}
|
||||
{withdrawalError && (
|
||||
<div className="flex items-center gap-2 text-sm text-destructive">
|
||||
<XCircle className="h-4 w-4" />
|
||||
<span>{withdrawalError}</span>
|
||||
</div>
|
||||
)}
|
||||
{!withdrawalLoading && !withdrawalError && withdrawalHtml && (
|
||||
<ScrollArea className="max-h-[60vh] rounded-md border" viewportClassName="p-3">
|
||||
<div
|
||||
className="prose prose-sm dark:prose-invert"
|
||||
dangerouslySetInnerHTML={{ __html: withdrawalHtml }}
|
||||
/>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface CheckoutPackage {
|
||||
limits?: Record<string, unknown>;
|
||||
paddle_price_id?: string | null;
|
||||
paddle_product_id?: string | null;
|
||||
activates_immediately?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -153,4 +153,48 @@ return [
|
||||
'open' => 'Feedback im Super Admin öffnen',
|
||||
'received_at' => 'Eingegangen: :date',
|
||||
],
|
||||
|
||||
'refund' => [
|
||||
'subject' => 'Rückerstattung für :package',
|
||||
'greeting' => 'Hallo :name,',
|
||||
'body' => 'Wir haben eine Rückerstattung eingeleitet. Betrag: :amount :currency. Zahlungs-ID: :provider_id.',
|
||||
'reason' => 'Grund: :reason',
|
||||
'footer' => 'Die Rückerstattung wird vom Zahlungsanbieter verarbeitet und kann je nach Bank einige Tage dauern.',
|
||||
],
|
||||
|
||||
'ops' => [
|
||||
'purchase' => [
|
||||
'subject' => 'Neuer Kauf: :package',
|
||||
'greeting' => 'Hallo Ops-Team,',
|
||||
'tenant' => 'Tenant: :tenant',
|
||||
'package' => 'Paket: :package',
|
||||
'amount' => 'Betrag: :amount :currency',
|
||||
'provider' => 'Provider: :provider (ID: :id)',
|
||||
'consents' => 'Consents – Version: :legal, Terms: :terms, Waiver: :waiver',
|
||||
'footer' => 'Bitte prüfen und ggf. verbuchen.',
|
||||
],
|
||||
'addon' => [
|
||||
'subject' => 'Add-on gekauft: :addon',
|
||||
'greeting' => 'Hallo Ops-Team,',
|
||||
'tenant' => 'Tenant: :tenant',
|
||||
'event' => 'Event: :event',
|
||||
'addon' => 'Add-on: :addon (Menge: :quantity)',
|
||||
'amount' => 'Betrag: :amount :currency',
|
||||
'provider' => 'Checkout: :checkout, Transaction: :transaction',
|
||||
'footer' => 'Add-on ist abgeschlossen und angewendet.',
|
||||
],
|
||||
'refund' => [
|
||||
'subject' => 'Refund verarbeitet: :package',
|
||||
'greeting' => 'Hallo Ops-Team,',
|
||||
'tenant' => 'Tenant: :tenant',
|
||||
'package' => 'Paket: :package',
|
||||
'amount' => 'Betrag: :amount :currency',
|
||||
'provider' => 'Provider: :provider (ID: :id)',
|
||||
'status_success' => 'Status: Erfolgreich',
|
||||
'status_failed' => 'Status: Fehlgeschlagen',
|
||||
'reason' => 'Grund: :reason',
|
||||
'error' => 'Fehler: :error',
|
||||
'footer' => 'Bitte prüfen und ggf. manuell nachfassen.',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -35,4 +35,5 @@ return [
|
||||
'version' => 'Version :version',
|
||||
'and' => 'und',
|
||||
'stripe_privacy' => 'Stripe Datenschutz',
|
||||
'widerrufsbelehrung' => 'Widerrufsbelehrung',
|
||||
];
|
||||
|
||||
@@ -100,6 +100,22 @@ return [
|
||||
'company' => 'S.E.B. Fotografie',
|
||||
'rights_reserved' => 'Alle Rechte vorbehalten',
|
||||
],
|
||||
'checkout' => [
|
||||
'headline' => 'Checkout & Bestellübersicht',
|
||||
'summary_title' => 'Ihre Bestellung',
|
||||
'package_label' => 'Ausgewähltes Package',
|
||||
'billing_type_one_time' => 'Einmalkauf (pro Event)',
|
||||
'billing_type_subscription' => 'Abo (wiederkehrend)',
|
||||
'legal_links_intro' => 'Mit Abschluss des Kaufs akzeptieren Sie unsere',
|
||||
'link_terms' => 'AGB',
|
||||
'link_privacy' => 'Datenschutzerklärung',
|
||||
'link_cancellation' => 'Widerrufsbelehrung',
|
||||
'checkbox_terms_label' => 'Ich habe die :terms, die :privacy und die :cancellation gelesen und akzeptiere sie.',
|
||||
'checkbox_terms_error' => 'Bitte bestätigen Sie, dass Sie AGB, Datenschutzerklärung und Widerrufsbelehrung gelesen haben.',
|
||||
'checkbox_digital_content_label' => 'Ich verlange ausdrücklich, dass Sie vor Ablauf der Widerrufsfrist mit der Ausführung der digitalen Dienstleistungen (Freischaltung meines Event-Packages inkl. Galerie und Hosting) beginnen. Mir ist bekannt, dass ich bei vollständiger Vertragserfüllung mein Widerrufsrecht verliere.',
|
||||
'checkbox_digital_content_error' => 'Bitte bestätigen Sie, dass Sie dem sofortigen Beginn der digitalen Dienstleistung und dem damit verbundenen vorzeitigen Erlöschen des Widerrufsrechts zustimmen.',
|
||||
'hint_subscription_withdrawal' => 'Bei Abonnements haben Verbraucher ein 14-tägiges Widerrufsrecht ab Vertragsschluss. Im Falle eines Widerrufs nach Leistungsbeginn behalten wir uns angemessenen Wertersatz für bereits erbrachte Leistungen vor.',
|
||||
],
|
||||
'legal' => [
|
||||
'imprint' => 'Impressum',
|
||||
'privacy' => 'Datenschutz',
|
||||
|
||||
@@ -153,4 +153,48 @@ return [
|
||||
'open' => 'Open feedback in Super Admin',
|
||||
'received_at' => 'Received: :date',
|
||||
],
|
||||
|
||||
'refund' => [
|
||||
'subject' => 'Refund for :package',
|
||||
'greeting' => 'Hi :name,',
|
||||
'body' => 'We have initiated a refund. Amount: :amount :currency. Payment ID: :provider_id.',
|
||||
'reason' => 'Reason: :reason',
|
||||
'footer' => 'The refund is processed by the payment provider and may take a few days depending on your bank.',
|
||||
],
|
||||
|
||||
'ops' => [
|
||||
'purchase' => [
|
||||
'subject' => 'New purchase: :package',
|
||||
'greeting' => 'Hello Ops team,',
|
||||
'tenant' => 'Tenant: :tenant',
|
||||
'package' => 'Package: :package',
|
||||
'amount' => 'Amount: :amount :currency',
|
||||
'provider' => 'Provider: :provider (ID: :id)',
|
||||
'consents' => 'Consents – Version: :legal, Terms: :terms, Waiver: :waiver',
|
||||
'footer' => 'Please review and reconcile.',
|
||||
],
|
||||
'addon' => [
|
||||
'subject' => 'Add-on purchased: :addon',
|
||||
'greeting' => 'Hello Ops team,',
|
||||
'tenant' => 'Tenant: :tenant',
|
||||
'event' => 'Event: :event',
|
||||
'addon' => 'Add-on: :addon (Quantity: :quantity)',
|
||||
'amount' => 'Amount: :amount :currency',
|
||||
'provider' => 'Checkout: :checkout, Transaction: :transaction',
|
||||
'footer' => 'Add-on is completed and applied.',
|
||||
],
|
||||
'refund' => [
|
||||
'subject' => 'Refund processed: :package',
|
||||
'greeting' => 'Hello Ops team,',
|
||||
'tenant' => 'Tenant: :tenant',
|
||||
'package' => 'Package: :package',
|
||||
'amount' => 'Amount: :amount :currency',
|
||||
'provider' => 'Provider: :provider (ID: :id)',
|
||||
'status_success' => 'Status: Success',
|
||||
'status_failed' => 'Status: Failed',
|
||||
'reason' => 'Reason: :reason',
|
||||
'error' => 'Error: :error',
|
||||
'footer' => 'Please review and follow up if needed.',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -30,6 +30,7 @@ return [
|
||||
'data_security' => 'Data Security',
|
||||
'data_security_desc' => 'We use HTTPS, encrypted storage (passwords hashed) and regular backups. Access to data is role-based restricted (Tenant vs SuperAdmin).',
|
||||
'agb' => 'Terms & Conditions',
|
||||
'widerrufsbelehrung' => 'Right of Withdrawal',
|
||||
'headline' => 'Legal',
|
||||
'effective_from' => 'Effective from :date',
|
||||
'version' => 'Version :version',
|
||||
|
||||
@@ -100,6 +100,22 @@ return [
|
||||
'company' => 'S.E.B. Fotografie',
|
||||
'rights_reserved' => 'All rights reserved',
|
||||
],
|
||||
'checkout' => [
|
||||
'headline' => 'Checkout & Order Summary',
|
||||
'summary_title' => 'Your order',
|
||||
'package_label' => 'Selected package',
|
||||
'billing_type_one_time' => 'One-time purchase (per event)',
|
||||
'billing_type_subscription' => 'Subscription (recurring)',
|
||||
'legal_links_intro' => 'By completing your order you accept our',
|
||||
'link_terms' => 'Terms & Conditions',
|
||||
'link_privacy' => 'Privacy Policy',
|
||||
'link_cancellation' => 'Right of Withdrawal',
|
||||
'checkbox_terms_label' => 'I have read and accept the :terms, :privacy and :cancellation.',
|
||||
'checkbox_terms_error' => 'Please confirm that you have read and accepted the Terms, Privacy Policy and Right of Withdrawal.',
|
||||
'checkbox_digital_content_label' => 'I expressly request that you begin providing the digital services (activation of my event package including gallery and hosting) before the withdrawal period has expired. I understand that I lose my right of withdrawal once the contract has been fully performed.',
|
||||
'checkbox_digital_content_error' => 'Please confirm that you agree to the immediate start of the digital service and the related early expiry of the right of withdrawal.',
|
||||
'hint_subscription_withdrawal' => 'For subscriptions, consumers have a 14-day right of withdrawal from the conclusion of the contract. In case of withdrawal after the start of the service, we reserve the right to claim appropriate compensation for the value of services already provided.',
|
||||
],
|
||||
'legal' => [
|
||||
'imprint' => 'Imprint',
|
||||
'privacy' => 'Privacy',
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
<a href="{{ route('datenschutz') }}" class="transition-colors hover:text-[#FFB6C1]">{{ __('legal.datenschutz') }}</a>
|
||||
<span class="text-white/40">•</span>
|
||||
<a href="{{ route('agb') }}" class="transition-colors hover:text-[#FFB6C1]">{{ __('legal.agb') }}</a>
|
||||
<span class="text-white/40">•</span>
|
||||
<a href="{{ route('widerrufsbelehrung') }}" class="transition-colors hover:text-[#FFB6C1]">{{ __('legal.widerrufsbelehrung') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user