Änderungen (relevant):

- Add‑on Checkout auf Transactions + Transaction‑ID speichern: app/Services/Addons/EventAddonCheckoutService.php
  - Paket/Marketing Checkout auf Transactions: app/Services/Paddle/PaddleCheckoutService.php
  - Gift‑Voucher Checkout: Customer anlegen/finden + Transactions: app/Services/GiftVouchers/
    GiftVoucherCheckoutService.php
  - Tests aktualisiert: tests/Feature/Tenant/EventAddonCheckoutTest.php, tests/Unit/PaddleCheckoutServiceTest.php,
tests/Unit/GiftVoucherCheckoutServiceTest.php
This commit is contained in:
Codex Agent
2025-12-29 18:04:28 +01:00
parent 795e37ee12
commit 5f521d055f
26 changed files with 783 additions and 102 deletions

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { YStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { useTheme } from '@tamagui/core';
import { MobileSheet } from './Sheet';
import { CTAButton } from './Primitives';
@@ -36,9 +37,24 @@ export function LegalConsentSheet({
copy,
t,
}: LegalConsentSheetProps) {
const theme = useTheme();
const [acceptedTerms, setAcceptedTerms] = React.useState(false);
const [acceptedWaiver, setAcceptedWaiver] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const checkboxAccent = String(theme.primary?.val ?? '#2563eb');
const checkboxBorder = String(theme.borderColor?.val ?? '#e5e7eb');
const checkboxSurface = String(theme.surface?.val ?? '#ffffff');
const checkboxStyle = {
marginTop: 4,
width: 18,
height: 18,
accentColor: checkboxAccent,
backgroundColor: checkboxSurface,
border: `1px solid ${checkboxBorder}`,
borderRadius: 4,
appearance: 'auto',
WebkitAppearance: 'auto',
} as const;
React.useEffect(() => {
if (open) {
@@ -103,7 +119,7 @@ export function LegalConsentSheet({
type="checkbox"
checked={acceptedTerms}
onChange={(event) => setAcceptedTerms(event.target.checked)}
style={{ marginTop: 4, width: 16, height: 16 }}
style={checkboxStyle}
/>
<Text fontSize="$sm" color="#111827">
{copy?.checkboxTerms ?? t(
@@ -119,7 +135,7 @@ export function LegalConsentSheet({
type="checkbox"
checked={acceptedWaiver}
onChange={(event) => setAcceptedWaiver(event.target.checked)}
style={{ marginTop: 4, width: 16, height: 16 }}
style={checkboxStyle}
/>
<Text fontSize="$sm" color="#111827">
{copy?.checkboxWaiver ?? t(

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { render } from '@testing-library/react';
vi.mock('@tamagui/core', () => ({
useTheme: () => ({
primary: { val: '#2563eb' },
borderColor: { val: '#e5e7eb' },
surface: { val: '#ffffff' },
}),
}));
vi.mock('@tamagui/stacks', () => ({
YStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
XStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/text', () => ({
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
}));
vi.mock('@tamagui/react-native-web-lite', () => ({
Pressable: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
}));
import { LegalConsentSheet } from '../LegalConsentSheet';
describe('LegalConsentSheet', () => {
it('renders the required consent checkboxes when open', () => {
const { getAllByRole } = render(
<LegalConsentSheet
open
onClose={vi.fn()}
onConfirm={vi.fn()}
t={(key, fallback) => fallback ?? key}
/>
);
expect(getAllByRole('checkbox')).toHaveLength(2);
});
});

View File

@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest';
import { resolveOnboardingRedirect } from './onboardingGuard';
import {
ADMIN_WELCOME_BASE_PATH,
ADMIN_WELCOME_EVENT_PATH,
ADMIN_WELCOME_PACKAGES_PATH,
ADMIN_WELCOME_SUMMARY_PATH,
} from '../../constants';
@@ -15,6 +15,8 @@ describe('resolveOnboardingRedirect', () => {
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBeNull();
});
@@ -24,9 +26,11 @@ describe('resolveOnboardingRedirect', () => {
hasEvents: false,
hasActivePackage: false,
selectedPackageId: null,
pathname: ADMIN_WELCOME_PACKAGES_PATH,
pathname: ADMIN_WELCOME_BASE_PATH,
isWelcomePath: true,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBeNull();
});
@@ -39,6 +43,8 @@ describe('resolveOnboardingRedirect', () => {
pathname: '/event-admin/mobile/billing',
isWelcomePath: false,
isBillingPath: true,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBeNull();
});
@@ -51,6 +57,8 @@ describe('resolveOnboardingRedirect', () => {
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBe(ADMIN_WELCOME_EVENT_PATH);
});
@@ -63,11 +71,13 @@ describe('resolveOnboardingRedirect', () => {
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBe(ADMIN_WELCOME_SUMMARY_PATH);
});
it('redirects to packages when no selection exists', () => {
it('redirects to landing when no selection exists', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: false,
@@ -75,8 +85,10 @@ describe('resolveOnboardingRedirect', () => {
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBe(ADMIN_WELCOME_PACKAGES_PATH);
expect(result).toBe(ADMIN_WELCOME_BASE_PATH);
});
it('does not redirect when already on target', () => {
@@ -84,9 +96,39 @@ describe('resolveOnboardingRedirect', () => {
hasEvents: false,
hasActivePackage: false,
selectedPackageId: null,
pathname: ADMIN_WELCOME_PACKAGES_PATH,
pathname: ADMIN_WELCOME_BASE_PATH,
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: false,
});
expect(result).toBeNull();
});
it('does not redirect when onboarding is dismissed', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: false,
selectedPackageId: null,
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: true,
isOnboardingCompleted: false,
});
expect(result).toBeNull();
});
it('does not redirect when onboarding is completed', () => {
const result = resolveOnboardingRedirect({
hasEvents: false,
hasActivePackage: false,
selectedPackageId: null,
pathname: '/event-admin/mobile/dashboard',
isWelcomePath: false,
isBillingPath: false,
isOnboardingDismissed: false,
isOnboardingCompleted: true,
});
expect(result).toBeNull();
});

View File

@@ -1,6 +1,6 @@
import {
ADMIN_WELCOME_BASE_PATH,
ADMIN_WELCOME_EVENT_PATH,
ADMIN_WELCOME_PACKAGES_PATH,
ADMIN_WELCOME_SUMMARY_PATH,
} from '../../constants';
@@ -11,6 +11,8 @@ type OnboardingRedirectInput = {
pathname: string;
isWelcomePath: boolean;
isBillingPath: boolean;
isOnboardingDismissed?: boolean;
isOnboardingCompleted?: boolean;
};
export function resolveOnboardingRedirect({
@@ -20,11 +22,17 @@ export function resolveOnboardingRedirect({
pathname,
isWelcomePath,
isBillingPath,
isOnboardingDismissed,
isOnboardingCompleted,
}: OnboardingRedirectInput): string | null {
if (hasEvents) {
return null;
}
if (isOnboardingDismissed || isOnboardingCompleted) {
return null;
}
if (isWelcomePath || isBillingPath) {
return null;
}
@@ -34,7 +42,7 @@ export function resolveOnboardingRedirect({
? ADMIN_WELCOME_EVENT_PATH
: shouldContinueSummary
? ADMIN_WELCOME_SUMMARY_PATH
: ADMIN_WELCOME_PACKAGES_PATH;
: ADMIN_WELCOME_BASE_PATH;
if (pathname === target) {
return null;

View File

@@ -8,7 +8,7 @@ import { SizableText as Text } from '@tamagui/text';
import { OnboardingShell } from '../components/OnboardingShell';
import { MobileCard, CTAButton } from '../components/Primitives';
import { ADMIN_HOME_PATH, ADMIN_WELCOME_BASE_PATH, ADMIN_WELCOME_PACKAGES_PATH, ADMIN_WELCOME_SUMMARY_PATH, adminPath } from '../../constants';
import { getTenantPackagesOverview } from '../../api';
import { getTenantPackagesOverview, trackOnboarding } from '../../api';
import { getSelectedPackageId } from '../lib/onboardingSelection';
export default function WelcomeEventPage() {
@@ -24,6 +24,10 @@ export default function WelcomeEventPage() {
const hasActivePackage =
Boolean(overview?.activePackage) || Boolean(overview?.packages?.some((pkg) => pkg.active));
const handleSkip = React.useCallback(() => {
void trackOnboarding('dismissed');
navigate(ADMIN_HOME_PATH);
}, [navigate]);
const backTarget = selectedId
? ADMIN_WELCOME_SUMMARY_PATH
@@ -40,7 +44,7 @@ export default function WelcomeEventPage() {
'Fill in a few details, invite co-hosts, and open your guest gallery for the big day.',
)}
onBack={() => navigate(backTarget)}
onSkip={() => navigate(ADMIN_HOME_PATH)}
onSkip={handleSkip}
skipLabel={t('layout.jumpToDashboard', 'Jump to dashboard')}
>
<MobileCard space="$3">

View File

@@ -7,7 +7,7 @@ import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { MobileCard, CTAButton, PillBadge } from '../components/Primitives';
import { OnboardingShell } from '../components/OnboardingShell';
import { getTenantPackagesOverview } from '../../api';
import { getTenantPackagesOverview, trackOnboarding } from '../../api';
import { useEventContext } from '../../context/EventContext';
import {
ADMIN_HOME_PATH,
@@ -29,6 +29,10 @@ export default function WelcomeLandingPage() {
const hasActivePackage =
Boolean(packagesData?.activePackage) || Boolean(packagesData?.packages?.some((pkg) => pkg.active));
const handleSkip = React.useCallback(() => {
void trackOnboarding('dismissed');
navigate(ADMIN_HOME_PATH);
}, [navigate]);
return (
<OnboardingShell
@@ -38,7 +42,7 @@ export default function WelcomeLandingPage() {
'layout.subtitle',
'Begin with an inspired introduction, secure your package, and craft the perfect guest gallery all optimised for mobile hosts.',
)}
onSkip={() => navigate(ADMIN_HOME_PATH)}
onSkip={handleSkip}
skipLabel={t('layout.jumpToDashboard', 'Jump to dashboard')}
>
<MobileCard space="$3">