Add-ons: Kaufbestaetigung vor Credit-Abbuchung + deutlichere Buttons

Boost-Laufzeiten und Nachweis oeffnen jetzt ein Bestaetigungs-Modal mit
Preis, Guthaben und Restsaldo; erst "Kostenpflichtig buchen" bucht ab.
Boost-Buttons primary mit explizitem Kauf-Label.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-10 16:44:05 +00:00
parent b26dc9057c
commit 93d7f6d480
2 changed files with 191 additions and 15 deletions

View file

@ -48,15 +48,25 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
*/
public ?array $creditShortfall = null;
/**
* Kontext fürs Kauf-Bestätigungs-Modal (Entscheidung 10.07.): Add-ons
* buchen erst nach explizitem „Kostenpflichtig buchen", damit ein Klick
* auf eine Laufzeit nicht sofort Credits abbucht.
*
* @var array{type: 'boost'|'proof', label: string, credits: int, days: int|null}|null
*/
public ?array $pendingPurchase = null;
public function mount(int $id): void
{
$this->id = $id;
$pr = $this->getMyPR();
$this->authorize('view', $pr);
// KI-Grad vorbelegen (bereits deklariert); die verbindliche
// Bestätigung muss bei jedem Submit erneut gesetzt werden.
$this->aiTextDeclaration = $pr->ai_text_declaration?->value;
// KI-Grad vorbelegen (bereits deklariert), gemappt auf die zwei
// Formular-Optionen; die verbindliche Bestätigung muss bei jedem
// Submit erneut gesetzt werden.
$this->aiTextDeclaration = $pr->ai_text_declaration?->formOption()->value;
}
public function submitForReview(): void
@ -99,7 +109,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
Flux::toast(
heading: __('Eingereicht'),
text: __('Die Inhaltsprüfung ist typischerweise innerhalb von 24 h abgeschlossen.'),
text: __('Die automatische Inhaltsprüfung ist in der Regel innerhalb weniger Minuten abgeschlossen.'),
variant: 'success',
);
}
@ -117,6 +127,67 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
Flux::toast(text: __('Vorschau-Link wurde erzeugt.'), variant: 'success');
}
/**
* Kauf-Bestätigung für einen Boost öffnen (Abbuchung erst in
* buyPendingAddOn).
*/
public function confirmBoost(int $days): void
{
$options = app(CreditPricingService::class)->boostOptions();
if (! isset($options[$days])) {
return;
}
$this->pendingPurchase = [
'type' => 'boost',
'label' => __('Boost — :days Tage Platzierung', ['days' => $days]),
'credits' => $options[$days],
'days' => $days,
];
Flux::modal('confirm-addon-purchase')->show();
}
/**
* Kauf-Bestätigung für den Veröffentlichungsnachweis öffnen.
*/
public function confirmProof(): void
{
$this->pendingPurchase = [
'type' => 'proof',
'label' => __('Veröffentlichungsnachweis (PDF)'),
'credits' => app(ProofPdfService::class)->priceCredits(),
'days' => null,
];
Flux::modal('confirm-addon-purchase')->show();
}
/**
* Bestätigten Add-on-Kauf ausführen — der einzige Weg, auf dem die
* Buttons der Add-on-Sektion tatsächlich Credits abbuchen.
*/
public function buyPendingAddOn(): void
{
$pending = $this->pendingPurchase;
$this->pendingPurchase = null;
Flux::modal('confirm-addon-purchase')->close();
if ($pending === null) {
return;
}
if ($pending['type'] === 'boost') {
$this->buyBoost($pending['days']);
return;
}
$this->buyProof();
}
/**
* Boost (Platzierung) aus der Wallet buchen — nur veröffentlichte, grüne
* PMs sind boostbar (Gate im BoostService).
@ -571,9 +642,9 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
@if ($canBoost)
<div class="flex flex-wrap gap-2">
@foreach ($boostOptions as $days => $credits)
<flux:button size="sm" variant="filled"
wire:click="buyBoost({{ $days }})" wire:loading.attr="disabled">
{{ __(':days Tage · :credits Credits', ['days' => $days, 'credits' => $credits]) }}
<flux:button size="sm" variant="primary"
wire:click="confirmBoost({{ $days }})" wire:loading.attr="disabled">
{{ __(':days Tage für :credits Credits buchen', ['days' => $days, 'credits' => $credits]) }}
</flux:button>
@endforeach
</div>
@ -607,7 +678,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
{{ __('Nachweis herunterladen') }}
</flux:button>
@else
<flux:button size="sm" variant="primary" wire:click="buyProof" wire:loading.attr="disabled">
<flux:button size="sm" variant="primary" wire:click="confirmProof" wire:loading.attr="disabled">
{{ __('Für :credits Credits kaufen', ['credits' => $proofPrice]) }}
</flux:button>
@endif
@ -890,14 +961,14 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
<span>{{ __('Platzhalter-Titelbild — laden Sie im Editor ein eigenes Bild hoch.') }}</span>
</div>
@elseif ($titleImage && ($titleImage->copyright || $titleImage->is_ai_generated || $titleImage->ai_declaration?->publicLabel()))
{{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act) --}}
{{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act): kurzes
Badge („KI-generiert" / „KI-bearbeitet"), kein Satz-Wortlaut. --}}
<div class="flex flex-wrap items-center gap-2 border-t border-[color:var(--color-bg-rule)] px-5 py-2.5 text-[12px] text-[color:var(--color-ink-3)]">
@if ($titleImage->is_ai_generated)
<span class="badge hub">{{ __('KI-generiert') }}</span>
@if ($aiImageBadge = $titleImage->ai_declaration?->publicLabel() ?? ($titleImage->is_ai_generated ? __('KI-generiert') : null))
<span class="badge hub">{{ $aiImageBadge }}</span>
@endif
<span>{{ $titleImage->copyright ?? __('Bild: KI-generiert') }}</span>
@if ($aiImageHint = $titleImage->ai_declaration?->publicLabel())
<span>· {{ $aiImageHint }}</span>
@if ($titleImage->copyright)
<span>{{ $titleImage->copyright }}</span>
@endif
</div>
@endif
@ -1004,6 +1075,49 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
</article>
@endif
{{-- ============== MODAL: ADD-ON-KAUF BESTÄTIGEN ==============
Vorgeschaltete Bestätigung (Entscheidung 10.07.): kein Add-on-Klick
bucht direkt Credits ab — erst „Kostenpflichtig buchen" kauft. --}}
<flux:modal name="confirm-addon-purchase" class="w-full max-w-md">
<div class="space-y-5">
<div>
<flux:text class="eyebrow muted">{{ __('Add-on') }}</flux:text>
<flux:heading size="lg">{{ __('Kauf bestätigen') }}</flux:heading>
</div>
@if ($pendingPurchase)
<div class="rounded-[6px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4 text-[12.5px] leading-[1.6] text-[color:var(--color-ink-2)]">
<div class="flex items-center justify-between gap-3">
<span>{{ $pendingPurchase['label'] }}</span>
<span class="font-bold whitespace-nowrap text-[color:var(--color-ink)]">{{ __(':n Credits', ['n' => $pendingPurchase['credits']]) }}</span>
</div>
<p class="m-0 mt-3 pt-3 border-t border-[color:var(--color-bg-rule)] text-[11.5px] text-[color:var(--color-ink-3)]">
@if ($creditBalance >= $pendingPurchase['credits'])
{{ __('Der Betrag wird sofort von Ihrem Guthaben abgebucht: :available Credits → :rest Credits.', [
'available' => $creditBalance,
'rest' => $creditBalance - $pendingPurchase['credits'],
]) }}
@else
{{ __('Ihr Guthaben (:available Credits) reicht dafür nicht aus — es fehlen :missing Credits.', [
'available' => $creditBalance,
'missing' => $pendingPurchase['credits'] - $creditBalance,
]) }}
@endif
</p>
</div>
@endif
<div class="flex justify-end gap-2">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" wire:click="buyPendingAddOn" wire:loading.attr="disabled">
{{ __('Kostenpflichtig buchen') }}
</flux:button>
</div>
</div>
</flux:modal>
{{-- ============== MODAL: NICHT GENÜGEND CREDITS ==============
Öffnet sich serverseitig, wenn ein Add-on-Kauf (Boost/Nachweis)
am Wallet-Guthaben scheitert — mit Direktlink zum Aufladen. --}}