further rework to the documentation
This commit is contained in:
15
docs/archive/prp/01-architecture.md
Normal file
15
docs/archive/prp/01-architecture.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 01 — Architecture Overview
|
||||
|
||||
- Backend: Laravel 12 (PHP 8.3), API-first.
|
||||
- Super Admin: Filament 4 web panel (platform operations only).
|
||||
- Tenant Admin: Separate React/Vite PWA, store-ready (Android TWA, iOS Capacitor). See ADR-0006.
|
||||
- Guest PWA: React/Vite, anonymous session for attendees.
|
||||
- Storage: S3-compatible object storage, signed URLs, CDN in front.
|
||||
- Queues: Redis + Horizon for media processing and async jobs.
|
||||
- DB: Single database, row-level multi-tenancy using `tenant_id` and policies.
|
||||
|
||||
Components
|
||||
- API (`/api/v1`) with Sanctum personal access tokens for tenant apps; session auth for Super Admin.
|
||||
- Media pipeline: upload -> scan -> transform (EXIF/orientation/sizes) -> store -> CDN.
|
||||
- Notifications: Web Push (VAPID); Capacitor push for iOS wrapper when needed.
|
||||
- Observability: request ID, structured logs, audit trails for admin actions.
|
||||
21
docs/archive/prp/02-tenancy.md
Normal file
21
docs/archive/prp/02-tenancy.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 02 — Tenancy Model
|
||||
|
||||
Approach
|
||||
- Single database, row-level scoping via `tenant_id` on tenant-owned tables.
|
||||
- Global scope (BelongsToTenant trait) for all tenant-owned models; bypass for Super Admin.
|
||||
- Policies enforce `tenant_admin` and `member` roles; guest upload uses signed, limited-scope tokens.
|
||||
|
||||
Keys & Indexes
|
||||
- Composite uniques include `tenant_id` (e.g., `events`: unique `tenant_id, slug`).
|
||||
- Foreign keys cascade/delete or null-on-delete based on data retention needs.
|
||||
|
||||
Tenant Resolution
|
||||
- Tenant Admin PWA: resolve from authenticated token claim (`tenant_id`).
|
||||
- Guest PWA/custom domains: resolve from host/subdomain; map to event and tenant.
|
||||
|
||||
Impersonation
|
||||
- Super Admin can impersonate tenant users; all actions audited with actor + target + reason.
|
||||
|
||||
Backups & Export
|
||||
- Backups include tenant partitions by `tenant_id`.
|
||||
- Export endpoints provide per-tenant data bundles (photos metadata + links), respecting rate limits.
|
||||
63
docs/archive/prp/03-api.md
Normal file
63
docs/archive/prp/03-api.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 03 — API Contract
|
||||
|
||||
- Base URL: `/api/v1`
|
||||
- Auth
|
||||
- Tenant apps: Sanctum personal access tokens (PATs) issued via `/api/v1/tenant-auth/login` + abilities.
|
||||
- Super Admin: session-authenticated Filament (web only).
|
||||
- Common
|
||||
- Pagination: `page`, `per_page` (max 100).
|
||||
- Errors: `{ error: { code, title, message, meta? } }` across public + tenant APIs.
|
||||
- Rate limits: per-tenant and per-device for tenant apps; 429 with `x-rate-limit-*` headers.
|
||||
|
||||
Key Endpoints (abridged)
|
||||
- Auth: `/api/v1/tenant-auth/login`, `/tenant-auth/exchange`, `/tenant-auth/logout`, `/tenant-auth/me`.
|
||||
- Tenants (Super Admin only): list/read; no create via API for MVP.
|
||||
- Events (tenant): CRUD, publish, archive; unique by `tenant_id + slug`.
|
||||
- Photos (tenant): signed upload URL, create, list, moderate, feature.
|
||||
- Emotions & Tasks: list, tenant overrides; task library scoping.
|
||||
- Purchases & Ledger: create purchase intent, webhook ingest, ledger list.
|
||||
- Settings: read/update tenant theme, limits, legal page links.
|
||||
- Notifications: `GET /api/v1/tenant/notifications/logs` (filterable by `type` / `status`) exposes recent package/limit alerts with timestamps and retry context.
|
||||
|
||||
Guest Polling (no WebSockets in v1)
|
||||
- GET `/events/{token}/stats` — lightweight counters for Home info bar.
|
||||
- Response: `{ online_guests: number, tasks_solved: number, latest_photo_at: ISO8601 }`.
|
||||
- Cache: `Cache-Control: no-store`; include `ETag` for conditional requests.
|
||||
- GET `/events/{token}/photos?since=<ISO8601|cursor>` — incremental gallery refresh.
|
||||
- Response: `{ data: Photo[], next_cursor?: string, latest_photo_at: ISO8601 }`.
|
||||
- Use `If-None-Match` or `If-Modified-Since` to return `304 Not Modified` when unchanged.
|
||||
- Legacy slug-based guest endpoints have been removed; tokens are mandatory for public access.
|
||||
|
||||
Webhooks
|
||||
- Payment provider events, media pipeline status, and deletion callbacks. All signed with shared secret per provider.
|
||||
- RevenueCat webhook: `POST /api/v1/webhooks/revenuecat` signed via `X-Signature` (HMAC SHA1/256). Dispatches `ProcessRevenueCatWebhook` to credit tenants and sync subscription expiry.
|
||||
|
||||
### Error Responses
|
||||
|
||||
- Every non-2xx response returns a JSON body with the `error` envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "photo_limit_exceeded",
|
||||
"title": "Upload Limit Reached",
|
||||
"message": "Es wurden 120 von 120 Fotos hochgeladen. Bitte kontaktiere das Team für ein Upgrade.",
|
||||
"meta": {
|
||||
"limit": 120,
|
||||
"used": 120,
|
||||
"remaining": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `code` is stable for clients; `title` is a short human-friendly label; `message` is localized; `meta` may contain structured data (e.g. `trace_id`, quota counts) when relevant.
|
||||
- Guests and tenant admins consume the same structure to surface tailored UI (toast banners, upload dialogs, etc.).
|
||||
|
||||
Public Gallery
|
||||
- `GET /gallery/{token}`: returns event snapshot + branding colors; responds with `410` once the package gallery window expires.
|
||||
- `GET /gallery/{token}/photos?cursor=&limit=`: cursor-based pagination of approved photos. Response shape `{ data: Photo[], next_cursor: string|null }`.
|
||||
- `GET /gallery/{token}/photos/{photo}/download`: streams or redirects to an approved original. Returns `404` if the asset is gone.
|
||||
|
||||
Tenant Admin Downloads
|
||||
- `GET /tenant/events/{event}/photos/archive`: authenticated ZIP export of all approved photos for an event. Returns `404` when none exist.
|
||||
210
docs/archive/prp/04-data-model-migrations.md
Normal file
210
docs/archive/prp/04-data-model-migrations.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 04 — Data Model & Portable Migrations
|
||||
|
||||
Use Laravel Schema builder; avoid database-specific ENUM/DDL. Composite unique indexes include `tenant_id` for tenant-owned data.
|
||||
|
||||
## Core (Tenants, Users, Events, Settings, Analytics, Purchases, Ledger)
|
||||
|
||||
```php
|
||||
use Illuminate\\Database\\Migrations\\Migration;
|
||||
use Illuminate\\Database\\Schema\\Blueprint;
|
||||
use Illuminate\\Support\\Facades\\Schema;
|
||||
|
||||
// Tenants
|
||||
Schema::create('tenants', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('domain')->nullable()->unique();
|
||||
$table->string('contact_name');
|
||||
$table->string('contact_email');
|
||||
$table->string('contact_phone')->nullable();
|
||||
$table->integer('event_credits_balance')->default(1);
|
||||
$table->timestamp('free_event_granted_at')->nullable();
|
||||
$table->integer('max_photos_per_event')->default(500);
|
||||
$table->integer('max_storage_mb')->default(1024);
|
||||
$table->json('features')->nullable();
|
||||
$table->timestamp('last_activity_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Users: tenancy + role (portable — avoid DB ENUM)
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('role', 32)->default('tenant_user')->index();
|
||||
});
|
||||
|
||||
// Events: include tenant_id and composite unique
|
||||
Schema::table('events', function (Blueprint $table) {
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->unique(['tenant_id', 'slug']);
|
||||
});
|
||||
|
||||
// System settings
|
||||
Schema::create('system_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('key')->unique();
|
||||
$table->text('value')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->boolean('is_public')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Platform analytics
|
||||
Schema::create('platform_analytics', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('metric_name', 100);
|
||||
$table->bigInteger('metric_value');
|
||||
$table->date('metric_date');
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['metric_date', 'metric_name']);
|
||||
$table->index(['tenant_id', 'metric_date']);
|
||||
});
|
||||
|
||||
// Event purchases (event credits)
|
||||
Schema::create('event_purchases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->unsignedInteger('events_purchased')->default(1);
|
||||
$table->decimal('amount', 10, 2);
|
||||
$table->string('currency', 3)->default('EUR');
|
||||
$table->string('provider', 32); // app enum: app_store|play_store|stripe|paypal
|
||||
$table->string('external_receipt_id')->nullable();
|
||||
$table->string('status', 16)->default('pending'); // app enum
|
||||
$table->timestamp('purchased_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['tenant_id', 'purchased_at']);
|
||||
});
|
||||
|
||||
// Credits ledger
|
||||
Schema::create('event_credits_ledger', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->integer('delta');
|
||||
$table->string('reason', 32); // app enum
|
||||
$table->foreignId('related_purchase_id')->nullable()->constrained('event_purchases')->nullOnDelete();
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['tenant_id', 'created_at']);
|
||||
});
|
||||
```
|
||||
|
||||
## Domain (Event Types, Events, Emotions, Tasks, Photos, Likes)
|
||||
|
||||
```php
|
||||
use Illuminate\\Database\\Migrations\\Migration;
|
||||
use Illuminate\\Database\\Schema\\Blueprint;
|
||||
use Illuminate\\Support\\Facades\\Schema;
|
||||
|
||||
Schema::create('event_types', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('name'); // Translatable: { "de": "Hochzeit", "en": "Wedding" }
|
||||
$table->string('slug', 100)->unique();
|
||||
$table->string('icon', 64)->nullable();
|
||||
$table->json('settings')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('events', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->json('name'); // Translatable: { "de": "Event Name", "en": "Event Name" }
|
||||
$table->date('date');
|
||||
$table->string('slug');
|
||||
$table->json('description')->nullable(); // Translatable
|
||||
$table->json('settings')->nullable();
|
||||
$table->foreignId('event_type_id')->constrained('event_types');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->string('default_locale', 5)->default('de'); // For event-specific i18n fallback
|
||||
$table->timestamps();
|
||||
$table->unique(['tenant_id', 'slug']);
|
||||
});
|
||||
|
||||
Schema::create('emotions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('name'); // Translatable: { "de": "Freude", "en": "Joy" }
|
||||
$table->string('icon', 50);
|
||||
$table->string('color', 7);
|
||||
$table->json('description')->nullable(); // Translatable
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
});
|
||||
|
||||
// Pivot: emotion x event_type
|
||||
Schema::create('emotion_event_type', function (Blueprint $table) {
|
||||
$table->foreignId('emotion_id')->constrained('emotions')->cascadeOnDelete();
|
||||
$table->foreignId('event_type_id')->constrained('event_types')->cascadeOnDelete();
|
||||
$table->primary(['emotion_id', 'event_type_id']);
|
||||
});
|
||||
|
||||
Schema::create('tasks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('emotion_id')->constrained('emotions');
|
||||
$table->foreignId('event_type_id')->nullable()->constrained('event_types')->nullOnDelete();
|
||||
$table->json('title'); // Translatable
|
||||
$table->json('description'); // Translatable
|
||||
$table->string('difficulty', 16)->default('easy'); // app enum
|
||||
$table->json('example_text')->nullable(); // Translatable
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete();
|
||||
$table->string('scope', 16)->default('global'); // global|tenant|event
|
||||
$table->foreignId('event_id')->nullable()->constrained('events')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
$table->unique(['tenant_id', 'emotion_id', 'title->de']); // Example for de fallback; adjust for multi-locale
|
||||
});
|
||||
|
||||
// Photos
|
||||
Schema::create('photos', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('event_id')->constrained('events')->cascadeOnDelete();
|
||||
$table->foreignId('emotion_id')->constrained('emotions');
|
||||
$table->foreignId('task_id')->nullable()->constrained('tasks')->nullOnDelete();
|
||||
$table->string('guest_name');
|
||||
$table->string('file_path');
|
||||
$table->string('thumbnail_path');
|
||||
$table->unsignedInteger('likes_count')->default(0);
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['event_id', 'created_at']);
|
||||
});
|
||||
|
||||
// Photo likes
|
||||
Schema::create('photo_likes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('photo_id')->constrained('photos')->cascadeOnDelete();
|
||||
$table->string('guest_name');
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->unique(['photo_id', 'guest_name', 'ip_address']);
|
||||
});
|
||||
```
|
||||
|
||||
## Legal Pages
|
||||
|
||||
```php
|
||||
use Illuminate\\Database\\Schema\\Blueprint;
|
||||
use Illuminate\\Support\\Facades\\Schema;
|
||||
|
||||
Schema::create('legal_pages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete();
|
||||
$table->string('slug', 32); // imprint|privacy|terms|custom
|
||||
$table->json('title'); // Translatable
|
||||
$table->json('body_markdown'); // Translatable Markdown content per locale
|
||||
$table->string('locale_fallback', 5)->default('de');
|
||||
$table->unsignedInteger('version')->default(1);
|
||||
$table->timestamp('effective_from')->nullable();
|
||||
$table->boolean('is_published')->default(false);
|
||||
$table->timestamps();
|
||||
$table->unique(['slug', 'tenant_id', 'version']);
|
||||
});
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Prefer app-level enums (string columns + validation) over DB `ENUM`.
|
||||
- Use `cascadeOnDelete()` only where child data must be removed with parent; otherwise `nullOnDelete()`.
|
||||
- Every tenant-owned table should include `tenant_id` and appropriate composite indexes.
|
||||
- **i18n Integration**: JSON fields (e.g., `name`, `description`) store locale-specific values as `{ "de": "Text", "en": "Text" }`. Use Laravel's `json` cast or spatie/laravel-translatable for access. Fallback to `default_locale` or global fallback ('de'). Update via Filament resources with locale selectors. Ensure indexes on JSON paths if querying (e.g., `->de` for German titles).
|
||||
10
docs/archive/prp/05-admin-superadmin.md
Normal file
10
docs/archive/prp/05-admin-superadmin.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 05 — Super Admin (Filament)
|
||||
|
||||
Scope
|
||||
- Platform operations only: tenants, legal pages, global settings, billing insights, monitoring.
|
||||
- No tenant admin functions; those live in the Tenant Admin PWA.
|
||||
|
||||
Notes
|
||||
- Keep to Filament 4 confirmed APIs; avoid pseudo namespaces.
|
||||
- Impersonation must create an audit trail with actor, target, timestamp, reason.
|
||||
- Read-only mode switch for maintenance; reflected in API `Retry-After` headers.
|
||||
32
docs/archive/prp/06-tenant-admin-pwa.md
Normal file
32
docs/archive/prp/06-tenant-admin-pwa.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 06 — Tenant Admin PWA (Store-Ready)
|
||||
|
||||
Packaging
|
||||
- Android: Trusted Web Activity (TWA) bound to `admin.<domain>`; fallback Capacitor if native plugins needed.
|
||||
- iOS: Capacitor wrapper with push notifications and secure storage.
|
||||
- Installable PWA (A2HS) with offline and background sync.
|
||||
|
||||
Auth & Tenancy
|
||||
- Sanctum personal access tokens via `/api/v1/tenant-auth/*`, persisted to IndexedDB/Keychain and refreshable through the session→token exchange for Google sign-ins.
|
||||
- Tokens carry `tenant_id` and roles; backend enforces scoping.
|
||||
|
||||
Capabilities
|
||||
- Manage events, galleries, members, settings, legal pages, purchases.
|
||||
- Notifications: Web Push (Android TWA) and Capacitor push (iOS).
|
||||
- Conflict handling: ETag/If-Match; audit changes.
|
||||
- Dashboard highlights tenant quota status (photo uploads, guest slots, gallery expiry) with traffic-light cards fed by package limit metrics.
|
||||
- Global toast handler consumes the shared API error schema and surfaces localized error messages for tenant operators.
|
||||
- Guest broadcast module on the Event detail page: tenant admins can compose short guest-facing notifications (broadcast/support tip/upload alert/feedback) with optional CTA links and expirations. Calls `/api/v1/tenant/events/{slug}/guest-notifications` and stores history (last 5 messages) for quick status checks.
|
||||
- Event detail includes notification analytics: total sends, broadcast counts, last send time, type distribution, and the latest guest-visible messages so hosts can monitor reach without leaving the toolkit view.
|
||||
|
||||
Support Playbook (Limits)
|
||||
- Wenn Tenant-Admins Upload- oder Gäste-Limits erreichen, zeigt der Header Warn-Badges + Toast mit derselben Fehlermeldung wie im Backend (`code`, `title`, `message`).
|
||||
- Support-Team kann `php artisan metrics:package-limits` ausführen, um die aggregierten Warn-/Expired-Zähler der letzten Stunden einzusehen und Engpässe zu bestätigen (`--reset` leert die Zähler nach Eskalation).
|
||||
- Empfehlung an Kunden: Paketupgrade oder Kontakt zu Sales; bei abgelaufener Galerie ggf. Verlängerung via Tenant Package.
|
||||
- Bei Fehlalarmen zuerst Logs nach `package_limit_metric` durchsuchen und prüfen, ob der Zähler `recovered` die Credits bereits wieder freigibt.
|
||||
|
||||
Distribution & CI
|
||||
- Play: assetlinks.json at `/.well-known/assetlinks.json`.
|
||||
- App Store: fastlane lanes; privacy manifests.
|
||||
- Version alignment with backend; feature flags synced on login.
|
||||
|
||||
See also: docs/adr/ADR-0006-tenant-admin-pwa.md
|
||||
158
docs/archive/prp/07-guest-pwa-routes-components.md
Normal file
158
docs/archive/prp/07-guest-pwa-routes-components.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# 07a — Guest PWA Routes & Components
|
||||
|
||||
This scaffold describes recommended routes, guards, directories, and components for the Guest PWA. It is framework-leaning (React Router v6 + Vite), but adaptable.
|
||||
|
||||
Routing Principles
|
||||
- Event routes require an event token (from QR/PIN). Guard redirects to Landing when missing/invalid.
|
||||
- Use route-level code splitting for camera/lightbox/slideshow.
|
||||
- Prefer modal routes (photo detail) layered over the gallery.
|
||||
|
||||
Route Map (proposed)
|
||||
- `/` — Landing (QR/PIN input; deep-link handler)
|
||||
- `/setup` — Profile Setup (name/avatar; skippable)
|
||||
- `/e/:slug` — Home/Feed (default gallery view + info bar)
|
||||
- `/e/:slug/tasks` — Task Picker (random/emotion)
|
||||
- `/e/:slug/tasks/:taskId` — Task Detail (card)
|
||||
- `/e/:slug/upload` — Upload Picker (camera/library + tagging)
|
||||
- `/e/:slug/queue` — Upload Queue (progress/retry)
|
||||
- `/e/:slug/gallery` — Gallery index (alias of Home or dedicated page)
|
||||
- `/e/:slug/photo/:photoId` — Photo Lightbox (modal over gallery)
|
||||
- `/e/:slug/achievements` — Achievements (optional)
|
||||
- `/e/:slug/slideshow` — Slideshow (optional, read-only)
|
||||
- `/legal/:page` — Legal pages (imprint/privacy/terms)
|
||||
- `*` — NotFound
|
||||
|
||||
Note: The settings experience is handled via the header sheet (no dedicated route; legal pages stay routable under /legal/:page).
|
||||
|
||||
Guards & Loaders
|
||||
- `EventGuard` — verifies event token in storage; attempts refresh; otherwise redirects to `/`.
|
||||
- `PrefetchEvent` — loads event metadata/theme on `:slug` routes.
|
||||
- `OfflineFallback` — surfaces offline banner and queues mutations.
|
||||
|
||||
Suggested Directory Structure
|
||||
```
|
||||
apps/guest-pwa/
|
||||
src/
|
||||
routes/
|
||||
index.tsx // router config + guards
|
||||
pages/
|
||||
LandingPage.tsx
|
||||
ProfileSetupPage.tsx
|
||||
HomePage.tsx
|
||||
TaskPickerPage.tsx
|
||||
TaskDetailPage.tsx
|
||||
UploadPage.tsx
|
||||
UploadQueuePage.tsx
|
||||
GalleryPage.tsx
|
||||
PhotoLightbox.tsx // modal route
|
||||
AchievementsPage.tsx
|
||||
SlideshowPage.tsx
|
||||
SettingsSheet.tsx
|
||||
LegalPage.tsx
|
||||
NotFoundPage.tsx
|
||||
components/
|
||||
Header.tsx
|
||||
InfoBar.tsx
|
||||
BottomNav.tsx
|
||||
QRPinForm.tsx
|
||||
CTAButtons.tsx
|
||||
EmotionPickerGrid.tsx
|
||||
TaskCard.tsx
|
||||
CameraPicker.tsx // photos only; no video capture
|
||||
UploadPreviewList.tsx
|
||||
UploadQueueList.tsx
|
||||
GalleryMasonry.tsx
|
||||
PhotoCard.tsx
|
||||
FiltersBar.tsx
|
||||
Toast.tsx
|
||||
stores/
|
||||
useEventStore.ts // tenant/event token, theme
|
||||
useProfileStore.ts // name/avatar (local)
|
||||
useUploadQueue.ts // IndexedDB-backed queue
|
||||
services/
|
||||
apiClient.ts // fetch wrapper + trace id
|
||||
eventsApi.ts // GET event meta
|
||||
photosApi.ts // list/finalize/like
|
||||
uploadService.ts // request signed URL, do PUT, finalize; photo only
|
||||
sw.ts // service worker register helpers
|
||||
hooks/
|
||||
useOnline.ts
|
||||
useA2HS.ts
|
||||
usePollStats.ts // polls /events/:slug/stats every 10s
|
||||
usePollGalleryDelta.ts // polls /events/:slug/photos?since=...
|
||||
i18n/
|
||||
config.ts // i18next init with react-i18next, backend loadPath '/lang/\{\{lng\}\}/guest.json'
|
||||
de.json // Namespace: guest (e.g., { "gallery": { "title": "Galerie" } })
|
||||
en.json
|
||||
main.tsx
|
||||
App.tsx
|
||||
```
|
||||
|
||||
Router Sketch (React Router v6)
|
||||
```tsx
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import EventGuard from './routes/EventGuard';
|
||||
import PrefetchEvent from './routes/PrefetchEvent';
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{ path: '/', element: <LandingPage /> },
|
||||
{ path: '/setup', element: <ProfileSetupPage /> },
|
||||
{
|
||||
path: '/e/:slug',
|
||||
element: (
|
||||
<EventGuard>
|
||||
<PrefetchEvent>
|
||||
<HomeLayout />
|
||||
</PrefetchEvent>
|
||||
</EventGuard>
|
||||
),
|
||||
children: [
|
||||
{ index: true, element: <HomePage /> },
|
||||
{ path: 'tasks', element: <TaskPickerPage /> },
|
||||
{ path: 'tasks/:taskId', element: <TaskDetailPage /> },
|
||||
{ path: 'upload', element: <UploadPage /> },
|
||||
{ path: 'queue', element: <UploadQueuePage /> },
|
||||
{ path: 'gallery', element: <GalleryPage /> },
|
||||
{ path: 'photo/:photoId', element: <PhotoLightbox /> },
|
||||
{ path: 'achievements', element: <AchievementsPage /> },
|
||||
{ path: 'slideshow', element: <SlideshowPage /> },
|
||||
],
|
||||
},
|
||||
// Settings sheet is rendered inside Header; no standalone route.
|
||||
{ path: '/legal/:page', element: <LegalPage /> },
|
||||
{ path: '*', element: <NotFoundPage /> },
|
||||
]);
|
||||
```
|
||||
|
||||
Component Checklist
|
||||
- Layout
|
||||
- `Header`, `InfoBar` (X Gäste online • Y Aufgaben gelöst), `BottomNav`, `Toast`.
|
||||
- Entry
|
||||
- `QRPinForm` (QR deep link or PIN fallback), `ProfileForm` (name/avatar).
|
||||
- Home/Feed
|
||||
- `HeroCard` (Willkommensgruess + Eventtitel) und `StatTiles` (online Gaeste, geloeste Aufgaben, letztes Upload).
|
||||
- `CTAButtons` (Aufgabe ziehen, Direkt-Upload, Galerie) + `UploadQueueLink` fuer Warteschlange.
|
||||
- `EmotionPickerGrid` und `GalleryPreview` als inhaltlicher Einstieg.
|
||||
- Tasks
|
||||
- `EmotionPickerGrid`, `TaskCard` (shows duration, group size, actions).
|
||||
- Capture/Upload (photos only)
|
||||
- `CameraPicker`, `UploadPreviewList`, `UploadQueueList`.
|
||||
- Photo View
|
||||
- `PhotoLightbox` (modal), like/share controls, emotion tags.
|
||||
- Settings & Legal
|
||||
- `SettingsSheet` (Header-Overlay mit Namenseditor, eingebetteten Rechtsdokumenten, Cache-Leeren), `LegalPage` Renderer.
|
||||
|
||||
State & Data
|
||||
- TanStack Query for server data (events, photos); optimistic updates for likes.
|
||||
- Zustand store for local-only state (profile, queue, banners).
|
||||
- IndexedDB for upload queue; CacheStorage for shell/assets.
|
||||
- i18n: react-i18next; load 'guest' namespace JSON from /lang/\{locale\}/guest.json; path-based detection for /de/e/:slug, /en/e/:slug; useTranslation('guest') in components.
|
||||
- Polling: focus-aware intervals (10s stats, 30s gallery); use document visibility to pause; backoff on failures.
|
||||
|
||||
Accessibility & Performance
|
||||
- Focus management on modal open/close; trap focus.
|
||||
- Color contrast and minimum tap target sizes (44px).
|
||||
- Code-split camera/lightbox/slideshow; prefetch next gallery page.
|
||||
|
||||
Out of Scope
|
||||
- Video capture/upload is not supported.
|
||||
133
docs/archive/prp/07-guest-pwa.md
Normal file
133
docs/archive/prp/07-guest-pwa.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# 07 — Guest PWA
|
||||
|
||||
Goal
|
||||
- Delight guests with a frictionless, installable photo experience that works offline, respects privacy, and requires no account.
|
||||
|
||||
Non-Goals (MVP)
|
||||
- No comments or chat. No facial recognition. No public profiles. No videos.
|
||||
|
||||
Personas
|
||||
- Guest (attendee) — scans QR, uploads photos, browses and likes.
|
||||
- Host (tenant) — optionally shares event PIN with guests; moderates via Tenant Admin PWA.
|
||||
|
||||
Top Journeys
|
||||
- Join: Scan QR → Open event → Accept terms → Optional PIN → Land on Gallery.
|
||||
- Upload: Add photos → Review → Submit → Background upload → Success/Retry.
|
||||
- Browse: Infinite gallery → Filter (emotion/task) → Open photo → Like/Share → Back.
|
||||
|
||||
Core Features
|
||||
- Event access
|
||||
- Access token embedded in QR/link (short-lived, event-scoped); optional PIN challenge.
|
||||
- Token refresh if event remains open; server can revoke/close event at any time.
|
||||
- Offline-first
|
||||
- App shell + last N gallery items cached; uploads queued with Background Sync.
|
||||
- Conflict-safe retries; user can pause/cancel queued uploads.
|
||||
- Capture & upload
|
||||
- Choose from camera or library; limit file size; show remaining upload cap.
|
||||
- Client-side resize to sane max (e.g., 2560px longest edge); EXIF stripped client-side if available.
|
||||
- Assign optional emotion/task before submit; default to “Uncategorizedâ€.
|
||||
- Upload screen shows quota cards (Photos, Guests) with traffic-light styling and friendly copy when nearing limits.
|
||||
- When the backend blocks uploads (limit reached, device blocked, gallery expired), surface localized dialogs with actionable hints.
|
||||
- Gallery
|
||||
- Masonry grid, lazy-load, pull-to-refresh; open photo lightbox with swipe.
|
||||
- Like (heart) with optimistic UI; share system sheet (URL to CDN variant).
|
||||
- Filters: emotion, featured, mine (local-only tag for items uploaded from this device).
|
||||
- Public share: host can hand out `https://app.domain/g/\{token\}`; guests see a themed, read-only gallery with per-photo downloads.
|
||||
- Banner on gallery header highlights approaching expiry (D-7/D-1) and offers CTA to upload remaining shots before the deadline.
|
||||
- Safety & abuse controls
|
||||
- Rate limits per device and IP; content-length checks; mime/type sniffing.
|
||||
- Upload moderation state: pending → approved/hidden; show local status.
|
||||
- Notification Center
|
||||
- Header bell opens a drawer that merges upload queue stats with server-driven notifications (photo highlights, major achievements, host broadcasts, upload failure hints, feedback reminders).
|
||||
- Data fetched from `/api/v1/events/\{token\}/notifications` with `X-Device-Id` for per-device read receipts; guests can mark items as read/dismissed and follow CTAs (internal routes or external links).
|
||||
- Pull-to-refresh + background poll every 90s to keep single-day events reactive without WS infrastructure.
|
||||
- When push is available (VAPID keys configured) the drawer surfaces a push toggle, persists subscriptions via `/push-subscriptions`, and the service worker refreshes notifications after every push message.
|
||||
- Operations playbook: see `docs/ops/guest-notification-ops.md` for enabling push, required queues, and cron health checks.
|
||||
- Privacy & legal
|
||||
- First run shows legal links (imprint/privacy); consent for push if enabled.
|
||||
- No PII stored; guest name is optional free text and not required by default.
|
||||
|
||||
Screens
|
||||
- Splash/Loading: event lookup + token validation; friendly skeleton.
|
||||
- Slug-based deep links are no longer accepted; guests must enter or scan a join token QR.
|
||||
- Terms & PIN: legal links, optional PIN input; remember choice per event.
|
||||
- Gallery: grid of approved photos; toolbar with filter, upload, settings.
|
||||
- Upload Picker: camera/library, selection preview, emotion/task tagging.
|
||||
- Upload Queue: items with progress, retry, remove; background sync toggle.
|
||||
- Photo Lightbox: zoom, like, share; show emotion tags.
|
||||
- Settings Sheet: Gear-Icon im Header oeffnet eine Sheet-Ansicht mit editierbarem Gastnamen, eingebetteten Rechtsdokumenten (inkl. Zurueck-Navigation) und Cache-Leeren; Theme-Umschalter bleibt im Header.
|
||||
|
||||
Wireframes
|
||||
- See wireframes file at docs/wireframes/guest-pwa.md for low-fidelity layouts and flows.
|
||||
|
||||
Core Pages (Pflichtseiten)
|
||||
- Landing / Einstieg
|
||||
- Purpose: Entry to event via QR/PIN; lowest friction.
|
||||
- UI: Single input (QR deep link preferred, fallback PIN field) and Join button.
|
||||
- States: invalid/expired token, event closed, offline (allow PIN entry and queue attempt).
|
||||
- Profil-Setup (Name/Avatar)
|
||||
- Purpose: Optional personalisation fuer Likes/Statistiken; Name wird lokal im Browser gespeichert.
|
||||
- UI: Name-Feld mit Sofort-Validierung; Avatar folgt spaeter.
|
||||
- Behavior: Nicht verpflichtend, aber empfohlen; Name kann jederzeit im Settings Sheet angepasst oder geloescht werden.
|
||||
- Startseite (Home/Feed)
|
||||
- Purpose: Central hub; begruesst Gaeste mit ihrem hinterlegten Namen und fuehrt zu den wichtigsten Aktionen.
|
||||
- Header: Eventtitel plus Live-Kennzahlen (online Gaeste, geloeste Aufgaben); hero-card zeigt "Hey \{Name\}!".
|
||||
- Highlights: Drei CTA-Karten fuer Aufgabe ziehen, Direkt-Upload und Galerie sowie ein Button fuer die Upload-Warteschlange.
|
||||
- Content: EmotionPicker und GalleryPreview bilden weiterhin den Einstieg in Spielstimmung und aktuelle Fotos.
|
||||
- Aufgaben-Flow
|
||||
- Aufgaben-Picker: Choose random task or emotion mood.
|
||||
- Aufgaben-Detail (Karte): Task text, emoji tag, estimated duration, suggested group size; actions: take photo, new task (same mood), change mood.
|
||||
- Foto-Interaktion
|
||||
- Kamera/Upload: Capture or pick; progress + success message on completion; background sync.
|
||||
- Galerie/Übersicht: Grid/Feed; filters: Neueste, Beliebt, Meine; Like hearts.
|
||||
- Foto-Detailansicht: Fullscreen; likes/reactions; linked task + (optional) uploader name.
|
||||
- Motivation & Spiel
|
||||
- Achievements/Erfolge: Badges (e.g., Erstes Foto, 5 Aufgaben, Beliebtestes Foto); personal progress.
|
||||
- Optionale Ergänzungen
|
||||
- Slideshow/Präsentationsmodus: Auto-rotating gallery for TV/Beamer with likes/task overlay.
|
||||
- Onboarding: 1–2 “So funktioniert das Spiel†hints the first time.
|
||||
- Event-Abschluss: “Danke fürs Mitmachenâ€, summary stats, link/QR to online gallery.
|
||||
|
||||
Technical Notes
|
||||
- Installability: PWA manifest + service worker; prompt A2HS on supported browsers.
|
||||
- Storage: IndexedDB for queue + cache; `CacheStorage` for shell/assets.
|
||||
- Background Sync: use Background Sync API when available; fallback to retry on app open.
|
||||
- Accessibility: large tap targets, high contrast, keyboard support, reduced motion.
|
||||
- i18n: react-i18next with JSON files (`public/lang/\{locale\}/guest.json`); default `de`, fallback `en`; path-based detection (/de/, /en/); RTL not in MVP. Strings for UI (e.g., gallery, upload, tasks) extracted via i18next-scanner; integrate with prefixed routing and middleware.
|
||||
- Media types: Photos only (no videos) — decision locked for MVP and v1.
|
||||
- Realtime model: periodic polling (no WebSockets). Home counters every 10s; gallery delta every 30s with exponential backoff when tab hidden or offline.
|
||||
|
||||
API Touchpoints
|
||||
- GET `/api/v1/events/\{token\}` — public event metadata (when open) + theme.
|
||||
- GET `/api/v1/events/\{token\}/photos` — paginated gallery (approved only).
|
||||
- POST `/api/v1/events/\{token\}/photos` — signed upload initiation; returns URL + fields.
|
||||
- POST (S3) — direct upload to object storage; then backend finalize call.
|
||||
- POST `/api/v1/photos/\{id\}/like` — idempotent like with device token.
|
||||
- GET `/api/v1/events/\{token\}/notifications` — list guest notifications (requires `X-Device-Id`).
|
||||
- POST `/api/v1/events/\{token\}/notifications/\{notification\}/read|dismiss` — mark/dismiss notification with device identity.
|
||||
- POST `/api/v1/events/\{token\}/push-subscriptions` — register a browser push subscription (requires `X-Device-Id` + VAPID public key).
|
||||
- DELETE `/api/v1/events/\{token\}/push-subscriptions` — revoke a stored push subscription by endpoint.
|
||||
|
||||
Limits (MVP defaults)
|
||||
- Max uploads per device per event: 50
|
||||
- Max file size (after client resize): 6 MB per photo
|
||||
- Max resolution: 2560px longest edge per photo
|
||||
|
||||
Edge Cases
|
||||
- Token expired/invalid → Show “Event closed/invalid linkâ€; link to retry.
|
||||
- No connectivity → Queue actions; show badge; retry policy with backoff.
|
||||
- Storage full → Offer to clear cache or deselect files.
|
||||
- Permission denied (camera/photos) → Explain and offer system shortcut.
|
||||
|
||||
Decisions
|
||||
- Videos are not supported (capture/upload strictly photos).
|
||||
|
||||
Appendix: Page-to-Feature Mapping
|
||||
- Landing: Access, validation, join; deep-link handling.
|
||||
- Profile Setup: Local profile persistence; optional avatar assets.
|
||||
- Home: Header, info bar, CTAs, feed preview.
|
||||
- Tasks: Picker (random/emotion), detail card actions.
|
||||
- Capture/Upload: Camera permissions, client resize, queue handling.
|
||||
- Gallery: Filters, pagination, lightbox.
|
||||
- Achievements: Local badges (MVP), server-backed later.
|
||||
- Slideshow (optional): Read-only, auto-advance, idle-safe.
|
||||
67
docs/archive/prp/08-billing.md
Normal file
67
docs/archive/prp/08-billing.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Billing and Payments
|
||||
|
||||
## Overview
|
||||
The Fotospiel platform supports multiple payment providers for package purchases: Stripe for one-time and subscription payments, and Paddle for orders and subscriptions. Billing is handled through a freemium model with endcustomer event packages and reseller subscriptions. All payments are processed via API integrations, with webhooks for asynchronous updates.
|
||||
|
||||
## Stripe Integration
|
||||
- **One-time Payments**: Use Stripe Checkout for endcustomer event packages. Create PaymentIntent via `StripeController@createPaymentIntent`.
|
||||
- **Subscriptions**: Reseller subscriptions use Stripe Subscriptions API. Webhook handling in `StripeWebhookController@handleWebhook` for events like `invoice.paid`, `customer.subscription.deleted`.
|
||||
- **Configuration**: Keys in `config/services.php` under `stripe`. Sandbox mode based on `APP_ENV`.
|
||||
- **Models**: `PackagePurchase` records all transactions with `provider_id` (Stripe PI ID), `status`, `metadata`.
|
||||
- **Frontend**: PurchaseWizard.tsx handles client-side Stripe Elements for card input and confirmation.
|
||||
- **Webhook Matrix**:
|
||||
|
||||
| Event | Purpose | Internal handler |
|
||||
| --- | --- | --- |
|
||||
| `payment_intent.succeeded` | Completes single-event package purchase | `StripeWebhookController@handlePaymentIntentSucceeded` |
|
||||
| `payment_intent.payment_failed` | Logs failure, triggers recovery emails | `handlePaymentIntentFailed` |
|
||||
| `invoice.paid` | Confirms subscription renewal, extends package | `handleInvoicePaid` |
|
||||
| `invoice.payment_failed` | Flags tenant for follow-up, sends alerts | `handleInvoicePaymentFailed` |
|
||||
| `customer.subscription.deleted` | Finalises cancellation/downgrade | `handleSubscriptionDeleted` |
|
||||
|
||||
## Paddle Integration
|
||||
- **SDK**: Migrated to Paddle Server SDK v1.0+ (`paypal/paypal-server-sdk`). Uses Builder pattern for requests and Controllers for API calls.
|
||||
- **Orders (One-time Payments)**: Endcustomer event packages via Orders API. `PaddleController@createOrder` uses `OrderRequestBuilder` with `CheckoutPaymentIntent::CAPTURE`, custom_id for metadata (tenant_id, package_id, type). Capture in `@captureOrder` using `OrdersController->captureOrder`. DB creation in `processPurchaseFromOrder`.
|
||||
- **Subscriptions (Recurring Payments)**: Reseller subscriptions via Orders API with StoredPaymentSource for recurring setup (no dedicated SubscriptionsController in SDK). `PaddleController@createSubscription` uses `OrderRequestBuilder` with `StoredPaymentSource` (payment_initiator: CUSTOMER, payment_type: RECURRING, usage: FIRST), custom_id including plan_id. Initial order capture sets up subscription; subsequent billing via Paddle dashboard or webhooks. DB records created on initial capture, with expires_at for annual billing.
|
||||
- **Differences**: One-time: Standard Order with payment_type ONE_TIME (default). Recurring: Order with StoredPaymentSource RECURRING to enable future charges without new approvals. plan_id stored in metadata for reference; no separate subscription ID from SDK.
|
||||
- **Client Setup**: OAuth2 Client Credentials flow. Builder: `PaypalServerSdkClientBuilder::init()->clientCredentialsAuthCredentials(ClientCredentialsAuthCredentialsBuilder::init(client_id, secret))->environment(Environment::SANDBOX/PRODUCTION)->build()`.
|
||||
- **Webhooks**: `PaddleWebhookController@verify` handles events like `PAYMENT.CAPTURE.COMPLETED` (process initial/renewal purchase), `BILLING.SUBSCRIPTION.CANCELLED` or equivalent order events (deactivate package). Simplified signature verification (TODO: Implement `VerifyWebhookSignature`).
|
||||
- **Webhook Matrix**:
|
||||
|
||||
| Event | Purpose | Internal handler |
|
||||
| --- | --- | --- |
|
||||
| `PAYMENT.CAPTURE.COMPLETED` | Confirms one-time order, activates tenant package | `handleCapture` |
|
||||
| `BILLING.SUBSCRIPTION.ACTIVATED`, `BILLING.SUBSCRIPTION.UPDATED` | Syncs reseller subscription status/expiry | `handleSubscription` |
|
||||
| `BILLING.SUBSCRIPTION.CANCELLED`, `BILLING.SUBSCRIPTION.EXPIRED` | Marks package inactive and downgrades tenant | `handleSubscription` |
|
||||
| `BILLING.SUBSCRIPTION.SUSPENDED` | Pauses package benefits pending review | `handleSubscription` |
|
||||
- **Idempotency**: Check `provider_id` in `PackagePurchase` before creation. Transactions for DB safety.
|
||||
- **Configuration**: Keys in `config/services.php` under `paypal`. Sandbox mode via `paypal.sandbox`.
|
||||
- **Migration Notes**: Replaced old Checkout SDK (`PaddleCheckoutSdk`). Updated imports, requests (e.g., OrdersCreateRequest -> OrderRequestBuilder, Subscriptions -> StoredPaymentSource in Orders). Responses: Use `getStatusCode()` and `getResult()`. Tests mocked new structures. No breaking changes in auth or metadata handling; recurring flows now unified under Orders API.
|
||||
|
||||
## Database Models
|
||||
- **PackagePurchase**: Records purchases with `tenant_id`, `package_id`, `provider_id` (Stripe PI/Paddle Order ID), `price`, `type` (endcustomer_event/reseller_subscription), `status` (completed/refunded), `metadata` (JSON with provider details).
|
||||
- **TenantPackage**: Active packages with `tenant_id`, `package_id`, `price`, `purchased_at`, `expires_at`, `active` flag. Updated on purchase/cancellation.
|
||||
- **Constraints**: `type` CHECK (endcustomer_event, reseller_subscription), `price` NOT NULL.
|
||||
|
||||
## Flows
|
||||
1. **Purchase Initiation**: User selects package in PurchaseWizard. For free: direct assignment. Paid: Redirect to provider (Stripe Checkout or Paddle approve link).
|
||||
2. **Completion**: Provider callback/webhook triggers capture/confirmation. Create `PackagePurchase` and `TenantPackage`. Update tenant `subscription_status` to 'active'.
|
||||
3. **Cancellation/Refund**: Webhook updates status to 'cancelled', deactivates `TenantPackage`.
|
||||
4. **Trial**: First reseller subscription gets 14-day trial (`expires_at = now() + 14 days`).
|
||||
|
||||
## Error Handling
|
||||
- Validation: Request validation for IDs, consent.
|
||||
- API Errors: Catch exceptions, log, return 400/500 JSON.
|
||||
- Idempotency: Prevent duplicate processing.
|
||||
- Webhook: Verify signature, handle unhandled events with logging.
|
||||
|
||||
## Testing
|
||||
- Unit: Mock providers in `PurchaseTest.php` for order creation, capture, webhooks.
|
||||
- Integration: Sandbox keys for end-to-end. Assertions on DB state, responses.
|
||||
- Edge Cases: Failures, idempotency, trials, limits.
|
||||
|
||||
## Security & Compliance
|
||||
- GDPR: No PII in logs/metadata beyond necessary (tenant_id anonymous).
|
||||
- Auth: Sanctum tokens for API, CSRF for web.
|
||||
- Webhooks: IP whitelisting (Paddle IPs), signature verification.
|
||||
- Retention: Purchases retained per Privacy policy; update on changes.
|
||||
26
docs/archive/prp/09-security-compliance.md
Normal file
26
docs/archive/prp/09-security-compliance.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 09 — Security & Compliance
|
||||
|
||||
- Roles: `super_admin`, `tenant_admin`, `member`; guest upload via signed tokens.
|
||||
- Policies: all tenant-owned models gated; Super Admin bypass via explicit ability.
|
||||
- Audit: record impersonation and destructive actions with actor, target, reason.
|
||||
- Logging: structured, no PII; add request/trace IDs; redact secrets.
|
||||
- GDPR: retention settings per tenant; deletion workflows; legal pages managed via CMS-like resource.
|
||||
- Rate limits: per-tenant, per-user, per-device; protect upload and admin mutations.
|
||||
|
||||
## 2025 Hardening Priorities
|
||||
|
||||
- **Identity & Token Management** — *Owner: Backend Platform*
|
||||
Track Sanctum PAT issuance and revocation. Provide tooling to list/revoke active PATs per tenant admin and document forced re-login procedures for compromised devices.
|
||||
- **Guest Join Tokens** — *Owner: Guest Platform*
|
||||
Hash stored join tokens, add anomaly metrics (usage spikes, stale tokens), and tighten gallery/photo rate limits with visibility in storage dashboards. Join-token access is now logged to `event_join_token_events` with summaries surfaced in the Event admin modal.
|
||||
- **Public API Resilience** — *Owner: Core API*
|
||||
Ensure gallery/download endpoints serve signed URLs, expand abuse throttles (token + IP), and document incident response runbooks in ops guides. See `docs/ops/deployment/public-api-incident-playbook.md` for the response checklist.
|
||||
- **Media Pipeline & Storage** — *Owner: Media Services*
|
||||
Introduce antivirus + EXIF scrubbing workers, stream uploads to disk to avoid buffering, and enforce checksum verification during hot→archive transfers with configurable alerts from `StorageHealthService`.
|
||||
- Queue `media-security` (job: `ProcessPhotoSecurityScan`) performs antivirus + EXIF sanitisation per upload; configure via `config/security.php`.
|
||||
- **Payments & Webhooks** — *Owner: Billing*
|
||||
Align legacy Stripe hooks with checkout sessions, add idempotency locks/signature expiry checks, and plug failed capture notifications into the credit ledger audit trail.
|
||||
- **Frontend & CSP** — *Owner: Marketing Frontend*
|
||||
Replace unsafe-inline allowances (Stripe/Matomo) with nonce or hashed CSP rules, gate analytics injection behind consent, and localise cookie-banner copy that discloses data sharing.
|
||||
|
||||
Progress updates belong in `docs/process/changes/` and roadmap status in `docs/process/roadmap.md`.
|
||||
7
docs/archive/prp/10-storage-media-pipeline.md
Normal file
7
docs/archive/prp/10-storage-media-pipeline.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 10 — Storage & Media Pipeline
|
||||
|
||||
- Storage: S3-compatible bucket with server-side encryption; path pattern `tenants/{tenant_uuid}/events/{event_uuid}/photos/{photo_uuid}/`.
|
||||
- Variants: original + derived sizes in `variants/` with content hashing for cache-busting.
|
||||
- Processing: queue jobs for EXIF strip, orientation, resize (multiple sizes), and virus scan; idempotent by UUID.
|
||||
- Delivery: signed URLs for admin; CDN public for guest gallery variants.
|
||||
- Deletion: soft-delete metadata, schedule object purge respecting retention policies.
|
||||
7
docs/archive/prp/11-ops-ci-cd.md
Normal file
7
docs/archive/prp/11-ops-ci-cd.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 11 — Ops, Environments, CI/CD
|
||||
|
||||
- Environments: local (Docker), staging, production. Feature flags managed in DB.
|
||||
- CI: PHPStan/Larastan, PHPUnit/Pest, Pint, ESLint/TypeScript, build PWAs.
|
||||
- Releases: semantic versioning; changelog from PRs; migration notes required.
|
||||
- Observability: uptime checks, queue health (Horizon), error tracking.
|
||||
- Mobile: fastlane lanes for Capacitor iOS; TWA build pipeline for Android.
|
||||
29
docs/archive/prp/11-public-gallery.md
Normal file
29
docs/archive/prp/11-public-gallery.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 11 — Public Guest Gallery
|
||||
|
||||
Purpose
|
||||
- Provide a shareable, mobile-friendly gallery for guests who only need to view and download approved photos.
|
||||
- Respect existing join-token security and automatically disable access once a package’s gallery duration expires.
|
||||
|
||||
Access Model
|
||||
- URL pattern: `https://<app-domain>/g/{joinToken}`. Tokens are the same join tokens tenants already issue; revoking or expiring a token immediately locks the gallery.
|
||||
- Tokens expire when the associated event package’s `gallery_expires_at` passes; guests receive an explanatory message (HTTP `410`).
|
||||
- Only *approved* photos appear; pending/rejected items remain hidden.
|
||||
|
||||
Client Experience
|
||||
- Responsive grid with lazy-loaded thumbnails (IntersectionObserver) and infinite scroll/pagination.
|
||||
- Event branding colours (primary, secondary, background) are applied via CSS custom properties fetched from the API.
|
||||
- Fullscreen lightbox shows creation timestamp + guest label when available and exposes a single-photo download button (streams the original asset).
|
||||
|
||||
API Touchpoints (see 03 — API Contract for details)
|
||||
- `GET /api/v1/gallery/{token}` → event snapshot + branding.
|
||||
- `GET /api/v1/gallery/{token}/photos` → cursor-based pagination of approved photos.
|
||||
- `GET /api/v1/gallery/{token}/photos/{photo}/download` → single-photo download/redirect.
|
||||
|
||||
Tenant Admin Support
|
||||
- Filament action “Download all photos” (Event resource) queues a server-side ZIP export via `GET /tenant/events/{event}/photos/archive` for authenticated tenants.
|
||||
- Only approved photos are included; failed assets are skipped with logging.
|
||||
|
||||
Future Enhancements
|
||||
- Background job + notification for large ZIP exports (current implementation streams synchronously).
|
||||
- Optional passcode/PIN layered on top of join tokens for sensitive events.
|
||||
- Aggregate analytics (views/downloads per photo) presented in Tenant Admin dashboards.
|
||||
78
docs/archive/prp/12-i18n.md
Normal file
78
docs/archive/prp/12-i18n.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 12 — Internationalization (i18n)
|
||||
|
||||
## Overview
|
||||
- **Default Locale**: `de` (Deutsch), fallback `en` (English).
|
||||
- **Supported Locales**: MVP focuses on `de` and `en`; expandable via config/app.php locales array.
|
||||
- **Strategy**: Hybrid approach – Laravel PHP for backend/Blade views, react-i18next for React/Vite PWAs (Marketing, Tenant Admin, Guest).
|
||||
- **Translatable Fields**: JSON columns in DB for dynamic content (e.g., `name`, `description`, `title`, `body_markdown` in models like Package, EventType, LegalPage).
|
||||
- **Namespaces**: Organized by feature (e.g., `marketing`, `auth`, `guest`, `admin`) to avoid key collisions; glossary terms centralized in a shared namespace if needed.
|
||||
- **Date/Number Formatting**: Locale-aware via Laravel's Carbon/Intl; PWAs use Intl API or date-fns with locale bundles.
|
||||
- **RTL Support**: Not in MVP; future via CSS classes and i18next RTL plugin.
|
||||
- **SEO**: Multilingual URLs with prefixes (/de/, /en/), hreflang tags, canonical links, translated meta (title, description, og:).
|
||||
|
||||
## Backend (Laravel/PHP)
|
||||
- **Config**: `config/app.php` – `locale => 'de'`, `fallback_locale => 'en'`, `available_locales => ['de', 'en']`.
|
||||
- **Translation Files**:
|
||||
- PHP arrays: `resources/lang/\{locale\}/\{group\}.php` (e.g., `marketing.php`, `auth.php`, `legal.php`) for Blade and API responses.
|
||||
- JSON for PWAs: `public/lang/\{locale\}/\{namespace\}.json` (e.g., `public/lang/de/marketing.json`) – migrated from PHP where possible; loaded via dedicated route.
|
||||
- **Routing**:
|
||||
- Prefixed groups: `Route::prefix('\{locale?\}')->where(['locale' => 'de|en'])->middleware('SetLocale')` in `routes/web.php`.
|
||||
- Fallbacks: Non-prefixed routes redirect to `/de/\{path\}` (e.g., `/login` → `/de/login`).
|
||||
- Auth routes (login, register, logout): Prefixed and named (e.g., `Route::get('/login', ...)->name('login')`).
|
||||
- API routes: Locale from header/session; no URL prefix for `/api/v1`.
|
||||
- **Middleware**: `SetLocale` – Extracts locale from URL segment(1), sets `App::setLocale()`, stores in session; defaults to 'de'.
|
||||
- **JSON Loader Route**: `Route::get('/lang/\{locale\}/\{namespace\}.json', ...)` – Serves from `public_path('lang/\{locale\}/\{namespace\}.json')`; Vite proxy forwards requests.
|
||||
- **DB Translations**: Use JSON fields with spatie/laravel-translatable or native casts; admin UI (Filament) for editing per locale.
|
||||
- **Legal Pages**: Dynamic via LegalPage model; rendered with `__($key)` or JSON for PWAs.
|
||||
|
||||
## Frontend (React/Vite PWAs)
|
||||
- **Library**: react-i18next with i18next-http-backend for async JSON loads.
|
||||
- **Setup** (`resources/js/i18n.js`):
|
||||
- Init: `i18n.use(Backend).use(LanguageDetector).init({ lng: 'de', fallbackLng: 'en', ns: ['marketing', 'auth'], backend: { loadPath: '/lang/\{\{lng\}\}/\{\{ns\}\}.json' } })`.
|
||||
- Detection: Path-based (`order: ['path']`, `lookupFromPathIndex: 0`) for prefixed URLs; cookie/session fallback.
|
||||
- Provider: Wrap `<App>` in `<I18nextProvider i18n=\{i18n\}>` in `app.tsx`.
|
||||
- **Usage**:
|
||||
- Hook: `const { t } = useTranslation('namespace');` in components (e.g., `t('marketing.home.title')`).
|
||||
- Interpolation: Placeholders `\{count\}`; pluralization via i18next rules.
|
||||
- Dynamic Keys: Avoid; use namespaces for organization.
|
||||
- **Inertia Integration**:
|
||||
- Page Resolver: `` resolvePageComponent(`./Pages/\${name}.tsx`, import.meta.glob('./Pages/**/*.tsx')) `` – Matches capital 'Pages' directory.
|
||||
- Props: Pass `locale` from middleware to pages.
|
||||
- Links: `<Link href={`/${'{'}locale{'}'}/path`}>` für prefixed navigation (e.g., Header.tsx).
|
||||
- **Marketing Frontend**:
|
||||
- Namespaces: `marketing` (Home, Packages, Blog, Features), `auth` (Login, Register).
|
||||
- Components: All hard-coded strings replaced (e.g., Home.tsx: `t('marketing.hero.title')`); SEO meta via `Head` with `t()`.
|
||||
- Header: Locale selector; dynamic links (e.g., `/\{locale\}/login` with `t('auth.header.login')`).
|
||||
- **Guest/Tenant PWAs**:
|
||||
- Similar setup; load JSON on app init.
|
||||
- Guest: Anonymous, locale from URL or default 'de'; strings for UI (e.g., gallery, upload).
|
||||
- Tenant Admin: User-preferred locale from profile; sync with backend.
|
||||
- **Automation**:
|
||||
- Extraction: i18next-scanner (npm script: `i18next-scanner --config i18next-scanner.config.js`) to scan TSX for `t('key')` and update JSON.
|
||||
- Validation: Missing keys log warnings; dev mode strict.
|
||||
|
||||
## SEO & Accessibility
|
||||
- **Multilingual URLs**: `/de/home`, `/en/home`; 301 redirects for non-prefixed.
|
||||
- **Hreflang**: `<link rel="alternate" hreflang="de" href="/de/home">` in `<Head>`.
|
||||
- **Canonical**: `<link rel="canonical" href=\{currentUrl\}>` based on detected locale.
|
||||
- **Meta**: Translated via `t('seo.title')`; og:locale='de_DE'.
|
||||
- **Sitemap**: Generate with `de/` and `en/` variants; update `public/sitemap.xml`.
|
||||
- **Robots.txt**: Allow both locales; noindex for dev.
|
||||
- **Accessibility**: ARIA labels with `t()`; screen reader support for language switches.
|
||||
|
||||
## Migration from PHP to JSON
|
||||
- Extract keys from `resources/lang/\{locale\}/marketing.php` to `public/lang/\{locale\}/marketing.json`.
|
||||
- Consolidate: Remove duplicates; use nested objects (e.g., `{ "header": { "login": "Anmelden" } }`).
|
||||
- Fallback: PHP arrays remain for backend; JSON for PWAs.
|
||||
|
||||
## Testing & Maintenance
|
||||
- **Tests**: PHPUnit for backend (`__('key')`); Vitest/Jest for frontend (`t('key')` renders correctly).
|
||||
- **Linting**: ESLint rule for missing translations; i18next-scanner in pre-commit.
|
||||
- **Deployment**: JSON files in public; cache-bust via Vite hashes.
|
||||
- **Expansion**: Add locales via config; migrate more namespaces (e.g., `guest`, `admin`).
|
||||
|
||||
## Decisions & Trade-offs
|
||||
- Path-based detection over query params for SEO/clean URLs.
|
||||
- JSON over PHP for PWAs: Faster async loads, no server roundtrips.
|
||||
- No auto-redirect on locale mismatch in MVP; user chooses via selector.
|
||||
- ADR: Use react-i18next over next-intl (Inertia compatibility).
|
||||
129
docs/archive/prp/13-backend-authentication.md
Normal file
129
docs/archive/prp/13-backend-authentication.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 13 - Backend Authentication Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Tenant authentication now uses a hybrid Sanctum setup:
|
||||
|
||||
- **Tenant Admin PWA** obtains Laravel Sanctum personal access tokens (PATs) via first-party endpoints under `/api/v1/tenant-auth/*`. Tokens carry ability strings (e.g. `tenant-admin`, `tenant:{id}`) and are required for all `/api/v1/tenant/*` routes.
|
||||
- **First-party web flows** (marketing checkout, Filament admin, Google OAuth sign-in) rely on the classic session guard. When a session-based login needs API access (e.g. inside the PWA shell after a Google callback) the frontend exchanges the session for a PAT using the same Sanctum endpoints.
|
||||
- **Legacy compatibility** is preserved through `/api/v1/tenant/me`, which now proxies the Sanctum-backed data while keeping the historical payload shape consumed by older clients.
|
||||
|
||||
All previous OAuth2/PKCE code paths, tables, and environment variables have been removed. No external authorization server is required.
|
||||
|
||||
## Tenant Admin PAT Flow
|
||||
|
||||
### Login
|
||||
- **Endpoint**: `POST /api/v1/tenant-auth/login`
|
||||
- **Body** (`application/json`):
|
||||
```json
|
||||
{
|
||||
"login": "tenant@example.com",
|
||||
"password": "secret"
|
||||
}
|
||||
```
|
||||
`login` accepts either an email address or username. Passwords are validated against the user record.
|
||||
- **Response** (`200`):
|
||||
```json
|
||||
{
|
||||
"token": "plain-text-pat",
|
||||
"token_type": "Bearer",
|
||||
"abilities": ["tenant-admin", "tenant:42"],
|
||||
"user": {
|
||||
"id": 17,
|
||||
"email": "tenant@example.com",
|
||||
"name": "Tenant Admin",
|
||||
"role": "tenant_admin",
|
||||
"tenant_id": 42
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Failure codes**:
|
||||
- `422` with `login` field errors for invalid credentials, unverified email addresses, or roles without tenant access.
|
||||
- `429` throttle when the `tenant-auth` rate limit is exceeded.
|
||||
|
||||
Every successful login revokes any previous `tenant-admin` PAT for that user before issuing a new token. PATs are plain-text once in the response; the hash is stored in `personal_access_tokens`.
|
||||
|
||||
### Token Exchange (session → PAT)
|
||||
- **Endpoint**: `POST /api/v1/tenant-auth/exchange`
|
||||
- **Guards**: `web` session (cookies) with Sanctum stateful middleware.
|
||||
- **Response**: identical payload to `/login`.
|
||||
- **Usage**: The Tenant Admin PWA calls this after a browser session login (e.g. Google Socialite callback or marketing checkout user who opens the PWA shell) to synchronise state without a password prompt.
|
||||
|
||||
### Token Introspection & Logout
|
||||
- **`GET /api/v1/tenant-auth/me`** (auth:sanctum) returns the current user, tenant snapshot, and active abilities for the PAT.
|
||||
- **`POST /api/v1/tenant-auth/logout`** invalidates the current PAT and clears it from storage.
|
||||
- **Legacy Proxy**: `/api/v1/tenant/me` now delegates to the new controller to return the historical payload seen by existing tooling while relying on Sanctum for authentication.
|
||||
|
||||
### Calling Tenant APIs
|
||||
All tenant APIs continue to require the abilities enforced by middleware:
|
||||
- `auth:sanctum` authenticates the PAT.
|
||||
- `tenant.admin` gate checks that the user has `tenant-admin` or `super-admin` ability.
|
||||
- `tenant.isolation` ensures the `tenant:{id}` ability matches the route tenant, guarding cross-tenant access.
|
||||
|
||||
Requests must send the PAT in the `Authorization: Bearer {token}` header. Tokens have no server-side expiration (`sanctum.expiration = null`); clients should refresh proactively (e.g. re-login) when a token is revoked or rejected.
|
||||
|
||||
## Session-Based Flows
|
||||
|
||||
### Marketing Checkout Login
|
||||
- **Endpoint**: `POST /checkout/login`
|
||||
- **Body**: `{ identifier, password, locale? }`
|
||||
- **Remember behaviour**: the checkbox is hidden on the new Tenant Admin login screen but applied automatically – sessions persist via `Auth::login($user, true)`.
|
||||
- Successful logins store a `redirect_intent` in the session when the flow originates from `/event-admin/*`, ensuring post-login navigation goes to `/event-admin/dashboard`.
|
||||
|
||||
### Tenant Admin PWA Login Screen
|
||||
- The React shell renders `/event-admin/login` inside the PWA layout. It uses the same credentials form as `/checkout/login` but hits `/api/v1/tenant-auth/login` with fetch requests.
|
||||
- After a successful PAT response, the token is persisted to both `localStorage` and `sessionStorage`; abilities are cached client-side to power feature flags.
|
||||
|
||||
### Google Sign-In
|
||||
- Socialite handles the Google redirect/callback under `/event-admin/auth/google`.
|
||||
- On success the user is logged into the web session, their email is marked verified (if needed), and the browser redirects back into the PWA shell.
|
||||
- The shell immediately calls `/api/v1/tenant-auth/exchange` to obtain a PAT, keeping the flow consistent with password logins.
|
||||
- Failures redirect back to the login page with query-string error codes that the PWA surfaces via i18n strings.
|
||||
|
||||
## Middleware & Guards
|
||||
|
||||
| Middleware | Purpose |
|
||||
| --- | --- |
|
||||
| `auth:sanctum` | Validates PATs issued by Sanctum. |
|
||||
| `tenant.admin` | Ensures the authenticated user is a tenant admin or super admin. |
|
||||
| `tenant.isolation` | Loads the tenant model by token ability and adds it to the request for downstream controllers. |
|
||||
| `throttle:tenant-auth` | Rate limits login, exchange, and logout endpoints. |
|
||||
| `EncryptCookies`, `AddQueuedCookiesToResponse`, `StartSession` | Applied to exchange routes so session cookies are available. |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- PATs are hashed at rest (`personal_access_tokens`), revocation is handled via database deletes.
|
||||
- Tokens are limited to one active entry per user (`tenant-admin` name). Issuing a new token automatically stairs old devices out.
|
||||
- All auth endpoints return generic validation errors to avoid username enumeration.
|
||||
- Session regeneration occurs on every successful login (password or Google) to prevent fixation.
|
||||
- Frontend storage keeps a second copy of the PAT in `sessionStorage` to reduce exposure when a user clears only persistent storage.
|
||||
- Rate limits: configure `tenant-auth` in `RouteServiceProvider` (default `10 requests / minute`).
|
||||
- CSP / XSRF: stateful Sanctum middleware is configured with `SANCTUM_STATEFUL_DOMAINS` so SPA requests inherit `XSRF-TOKEN` cookies automatically.
|
||||
|
||||
## Database & Infrastructure
|
||||
|
||||
- OAuth tables (`oauth_clients`, `oauth_codes`, `refresh_tokens`) have been removed. Sanctum uses the stock `personal_access_tokens` migration.
|
||||
- No background jobs are required for PAT issuance or rotation.
|
||||
- Audit requirements should log `personal_access_tokens` changes via database auditing (e.g. telescope/horizon logs) if needed.
|
||||
|
||||
## Configuration & Environment
|
||||
|
||||
Set the following environment variables to support the hybrid flow:
|
||||
|
||||
- `SANCTUM_STATEFUL_DOMAINS` – comma-separated list of domains running the Tenant Admin PWA (TWA, Capacitor, staging). Ensures cookies are considered first-party for the exchange endpoint.
|
||||
- `SANCTUM_TOKEN_PREFIX` – optional prefix to enable secret-scanning detection.
|
||||
- Remove the deprecated OAuth variables:
|
||||
- `VITE_OAUTH_CLIENT_ID`
|
||||
- `OAUTH_JWT_KID`
|
||||
- `OAUTH_KEY_STORE`
|
||||
- `OAUTH_REFRESH_*`
|
||||
|
||||
Google OAuth credentials (`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`) remain for Socialite but only power the browser session login; token exchange happens through Sanctum.
|
||||
|
||||
## Testing
|
||||
|
||||
- Feature tests cover `/api/v1/tenant-auth/login`, `/exchange`, and `/logout` (`tests/Feature/Auth/TenantProfileApiTest.php`, `tests/Feature/TenantCreditsTest.php`).
|
||||
- Frontend Playwright fixtures (`tests/e2e/utils/test-fixtures.ts`) provide helpers to obtain PATs when seeding test users.
|
||||
- When adding new tenant API endpoints ensure coverage under Sanctum by using `->actingAs($user, 'sanctum')` in PHPUnit tests and by asserting abilities with `Sanctum::actingAs()` helpers where required.
|
||||
|
||||
This Sanctum-based approach keeps login logic inside the Laravel application, avoids custom OAuth infrastructure, and works uniformly across web, PWA, and the planned React Native wrapper.
|
||||
153
docs/archive/prp/14-freemium-business-model.md
Normal file
153
docs/archive/prp/14-freemium-business-model.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# 14 - Freemium Business Model Implementation
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document details the Package-based business model for the Fotospiel tenant app, combining free access with purchases of predefined packages. The model prioritizes user acquisition through a free test package while monetizing through value-driven upgrades. Key metrics: 5-10% conversion rate, ARPU €10-15, scalable to 100k+ users. See 15-packages-design.md for package details.
|
||||
|
||||
## Model Analysis
|
||||
|
||||
### Model A: Paid App (€4.99) with Free First Event
|
||||
**Pros:**
|
||||
- Higher paying user conversion (20-30% retention)
|
||||
- Premium perception for professional users
|
||||
- Stable revenue from app sales (70% net after store fees)
|
||||
- No freeloader problem
|
||||
|
||||
**Cons:**
|
||||
- Lower acquisition (lower than 5% download conversion)
|
||||
- High churn if first event doesn't impress
|
||||
- Slower scaling, higher marketing costs per install (€2-5)
|
||||
|
||||
### Model B: Free App with In-App Purchases (Recommended Base)
|
||||
**Pros:**
|
||||
- 10x more downloads, viral potential
|
||||
- Low acquisition costs (€0.50-1 per install)
|
||||
- Flexible pricing (starter €4.99, pro €9.99)
|
||||
- Better App Store ranking for free apps
|
||||
|
||||
**Cons:**
|
||||
- 80-95% freeloader rate
|
||||
- Complex IAP setup and testing
|
||||
- Requires strong onboarding to drive conversions
|
||||
|
||||
### Package-based Recommendation
|
||||
**Core Strategy:** Free app with limited test package for first event, upgrades via package purchases (Einmalkäufe for Endkunden, Subscriptions for Reseller).
|
||||
|
||||
**Pricing Structure:** See 15-packages-design.md for Endkunden (pro Event: Free/Test 0€, Starter 19€, etc.) and Reseller (jährlich: S 149€, etc.).
|
||||
- **Free Tier:** Test package (30 photos, 10 guests, 3 days gallery, 1 task, standard watermark)
|
||||
- **Endkunden Packages:** Einmalkäufe pro Event with increasing limits/features.
|
||||
- **Reseller Packages:** Annual subscriptions with event limits and branding options.
|
||||
|
||||
**Expected Metrics:**
|
||||
- Downloads: 50k/year
|
||||
- Conversion: 5-8% (2,500-4,000 paying users)
|
||||
- ARPU: €12 (mix of one-time + recurring)
|
||||
- Annual Revenue: €30,000-48,000 (after 30% store fees)
|
||||
|
||||
## User Acquisition Strategy
|
||||
|
||||
### App Store Optimization (ASO)
|
||||
- **Keywords:** "Event Foto App", "Hochzeit Galerie", "Party Foto Sharing", "Event Planner kostenlos"
|
||||
- **Screenshots:** 5-6 screens showing value (event creation, photo upload, QR sharing)
|
||||
- **Description:** 4,000 chars emphasizing "Free start, upgrade anytime"
|
||||
- **App Preview Video:** 30s demo of creating/sharing first event
|
||||
|
||||
### Marketing Channels
|
||||
- **Paid Ads:** Facebook/Instagram targeting wedding planners (€1-2/install)
|
||||
- **Organic:** Wedding forums, photographer communities, local event groups
|
||||
- **Partnerships:** Affiliate program for event agencies (20% commission)
|
||||
- **Content Marketing:** Blog posts "How to create digital photo galleries for events"
|
||||
|
||||
### Retention & Re-engagement
|
||||
- **Push Notifications:** "Your event ends soon - extend for €2.99" (opt-in only)
|
||||
- **Email Marketing:** Post-event surveys with upgrade offers
|
||||
- **In-App Messaging:** Contextual upgrade prompts after value demonstration
|
||||
|
||||
## Conversion Optimization
|
||||
|
||||
### Onboarding Funnel
|
||||
1. **Download → First Launch:** 90% completion target
|
||||
2. **Onboarding Wizard → First Event:** 80% completion
|
||||
3. **First Event Success → Upgrade Prompt:** 10% conversion
|
||||
4. **Repeat Usage → Subscription:** 20% of one-time buyers
|
||||
|
||||
### A/B Testing Plan
|
||||
- **CTA Text:** "Get Started Free" vs "Create First Event"
|
||||
- **Pricing Display:** Static prices vs dynamic local currency
|
||||
- **Upgrade Timing:** After first photo upload vs after event sharing
|
||||
- **Tools:** Firebase Remote Config + RevenueCat Experiments
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
### Frontend (React/Capacitor)
|
||||
- **IAP Integration:** RevenueCat SDK for cross-platform purchases
|
||||
- **State Management:** Context API for credits, React Query for API sync
|
||||
- **Offline Support:** Capacitor Storage for temporary event data
|
||||
- **Analytics:** Firebase for funnel tracking, RevenueCat for purchase events
|
||||
|
||||
### Backend API Extensions
|
||||
- **Package Management:** `/api/v1/packages` and `/api/v1/tenant/packages` endpoints
|
||||
- **Purchase Validation:** Webhook receiver from Stripe
|
||||
- **Event Limiting:** Middleware checking credit balance before creation
|
||||
- **Subscription Sync:** Real-time updates via WebSockets (optional)
|
||||
|
||||
### Database Schema Additions
|
||||
See 15-packages-design.md for updated schema: `packages`, `event_packages`, `tenant_packages`, `package_purchases`.
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### Store Compliance
|
||||
- **No Paywalls Before Value:** First event creation always free
|
||||
- **Clear Descriptions:** App Store screenshots show free tier capabilities
|
||||
- **Receipt Validation:** All purchases server-side verified
|
||||
- **Refund Policy:** 14-day money-back for subscriptions
|
||||
|
||||
### Technical Risks
|
||||
- **IAP Failures:** Fallback to manual credit allocation via support
|
||||
- **Sync Issues:** Offline-first approach, sync on reconnect
|
||||
- **Store Rejections:** Beta testing with TestFlight/Internal Testing
|
||||
- **Revenue Recognition:** Use RevenueCat for proper accounting
|
||||
|
||||
### Business Risks
|
||||
- **Low Conversion:** Monthly A/B testing and user surveys
|
||||
- **Churn:** Re-engagement campaigns for inactive users
|
||||
- **Competition:** Regular competitor analysis and feature updates
|
||||
- **Platform Changes:** Monitor Apple/Google policy updates
|
||||
|
||||
## Launch Timeline
|
||||
|
||||
### Phase 1: MVP (4 weeks)
|
||||
- Week 1: RevenueCat integration, basic credit system
|
||||
- Week 2: IAP store page, purchase flow
|
||||
- Week 3: Backend endpoints, database schema
|
||||
- Week 4: Testing, beta distribution
|
||||
|
||||
### Phase 2: Optimization (Ongoing)
|
||||
- Month 1: A/B testing of upgrade prompts
|
||||
- Month 2: Push notifications, email marketing
|
||||
- Month 3: Subscription model introduction
|
||||
- Quarterly: Feature updates based on user feedback
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Acquisition
|
||||
- Downloads: 10k in first 3 months
|
||||
- Cost per Install: less than €1.50
|
||||
- App Store Rating: more than 4.5 stars
|
||||
|
||||
### Conversion
|
||||
- Free → Paid: 5% within 30 days
|
||||
- First Event → Upgrade: 15% conversion
|
||||
- Average Order Value: €8-12
|
||||
|
||||
### Retention
|
||||
- Day 1 Retention: more than 40%
|
||||
- Day 7 Retention: more than 25%
|
||||
- Monthly Active Users: 20% of downloads
|
||||
|
||||
### Revenue
|
||||
- Month 1 Revenue: €1,000-2,000
|
||||
- ARPU: €0.50-1.00 overall
|
||||
- LTV more than 3x acquisition cost
|
||||
|
||||
This Freemium model balances user acquisition with sustainable revenue growth, leveraging the event-based nature of the app for recurring purchases while maintaining an accessible entry point.
|
||||
115
docs/archive/prp/15-packages-design.md
Normal file
115
docs/archive/prp/15-packages-design.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Packages-Design für Fotospiel
|
||||
|
||||
## Überblick
|
||||
Dieses Dokument definiert das neue package-basierte Business Model, das das bestehende Credits-System ersetzt. Packages sind vordefinierte Bündel mit Limits und Features, die als Einmalkäufe pro Event (für Endkunden) oder jährliche Subscriptions (für Reseller/Agenturen) verkauft werden. Das Modell priorisiert Einfachheit: Bei Event-Erstellung wählt der User ein Package, das Limits für diesen Event setzt. Für Reseller limitiert das Tenant-Package die Anzahl Events pro Jahr und globale Features.
|
||||
|
||||
Ziele:
|
||||
- Ersetze Credits vollständig (keine Balance mehr, sondern Event-spezifische Limits).
|
||||
- Unterstütze Freemium: Free-Paket für Einstieg.
|
||||
- Skalierbar: Endkunden pro Event, Reseller jährlich.
|
||||
- Integration: Stripe für Zahlungen (Einmalkäufe/Subscriptions), Ledger für Transaktionen.
|
||||
|
||||
## Endkunden-Pakete (pro Event, Einmalkauf)
|
||||
Diese Pakete werden bei Event-Erstellung ausgewählt und gekauft. Sie definieren Limits für den spezifischen Event (z.B. max_photos, gallery_duration). Preise basierend auf User-Vorschlag.
|
||||
|
||||
| Paket | Preis | max_photos | max_guests | gallery_days | max_tasks | watermark | branding | Extra Features |
|
||||
|-----------|-------|------------|------------|--------------|-----------|-----------|----------|----------------|
|
||||
| Free/Test | 0 € | 30 | 10 | 3 | 1 | Standard | Nein | - |
|
||||
| Starter | 19 € | 300 | 50 | 14 | 5 | Standard | Nein | - |
|
||||
| Standard | 39 € | 1000 | 150 | 30 | 10 | Custom | Ja | Logo |
|
||||
| Premium | 79 € | 3000 | 500 | 180 | 20 | Kein | Ja | Live-Slideshow, Analytics |
|
||||
|
||||
## Reseller/Agentur-Pakete (jährlich, Subscription)
|
||||
Diese Pakete werden auf Tenant-Ebene gekauft und limitieren Events pro Jahr, mit erweiterten Features (z.B. White-Label). Preise als Richtwerte.
|
||||
|
||||
| Paket | Preis/Jahr | max_events/year | Per-Event Limits | Branding | Extra |
|
||||
|------------|------------|-----------------|------------------|----------|-------|
|
||||
| Reseller S | 149 € | 5 | Standard | Eingeschränkt | - |
|
||||
| Reseller M | 299 € | 15 | Standard | Eigene Logos | 3 Monate Galerie |
|
||||
| Reseller L | 599 € | 40 | Premium | White-Label | - |
|
||||
| Enterprise | ab 999 € | Unlimited | Premium | Voll | Custom Domain, Support |
|
||||
|
||||
## Datenbank-Schema
|
||||
### Globale Packages-Tabelle (für alle Pakete, geteilt)
|
||||
```php
|
||||
Schema::create('packages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name'); // z.B. 'Starter', 'Reseller M'
|
||||
$table->string('type'); // 'endcustomer' oder 'reseller'
|
||||
$table->decimal('price', 8, 2); // Preis in EUR
|
||||
$table->integer('max_photos')->nullable(); // Null für Reseller (vererbt an Events)
|
||||
$table->integer('max_guests')->nullable();
|
||||
$table->integer('gallery_days')->nullable();
|
||||
$table->integer('max_tasks')->nullable();
|
||||
$table->boolean('watermark_allowed')->default(true);
|
||||
$table->boolean('branding_allowed')->default(false);
|
||||
$table->integer('max_events_per_year')->nullable(); // Für Reseller
|
||||
$table->timestamp('expires_after')->nullable(); // Für Subscriptions
|
||||
$table->json('features')->nullable(); // z.B. ['live_slideshow', 'analytics']
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
Seeder: Füge die obigen Pakete ein.
|
||||
|
||||
### Event-Packages (Zuordnung Event zu Endkunden-Paket)
|
||||
```php
|
||||
Schema::create('event_packages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('event_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('package_id')->constrained()->cascadeOnDelete();
|
||||
$table->decimal('purchased_price', 8, 2);
|
||||
$table->timestamp('purchased_at');
|
||||
$table->integer('used_photos')->default(0); // Counter für Limits
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
- Bei Event-Create: Package auswählen/kaufen, Eintrag erstellen.
|
||||
- Checks: z.B. if ($event->package->used_photos >= $event->package->max_photos) abort(403);
|
||||
|
||||
### Tenant-Packages (für Reseller)
|
||||
```php
|
||||
Schema::create('tenant_packages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('package_id')->constrained()->cascadeOnDelete();
|
||||
$table->decimal('price', 8, 2);
|
||||
$table->timestamp('purchased_at');
|
||||
$table->timestamp('expires_at'); // z.B. +1 Jahr
|
||||
$table->integer('used_events')->default(0); // Counter für max_events_per_year
|
||||
$table->boolean('active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
- Bei Tenant-Registrierung: Free-Reseller oder Upgrade.
|
||||
- Check: if ($tenant->activePackage->used_events >= $tenant->activePackage->max_events_per_year) block new Event.
|
||||
|
||||
### Ledger und Purchases (ersetzt Credits-Ledger)
|
||||
```php
|
||||
Schema::create('package_purchases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->nullable(); // Null für Event-spezifisch
|
||||
$table->foreignId('event_id')->constrained()->nullable(); // Für Endkunden
|
||||
$table->foreignId('package_id')->constrained();
|
||||
$table->string('stripe_id'); // Für Webhook/Idempotenz
|
||||
$table->decimal('price', 8, 2);
|
||||
$table->string('type'); // 'endcustomer_event', 'reseller_subscription'
|
||||
$table->json('metadata'); // z.B. {'event_id': 123}
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
- Kein separater Ledger nötig; Purchases tracken alles.
|
||||
|
||||
## Integration und Logik
|
||||
- **Event-Create Flow**: User wählt Package → Stripe-Checkout (Einmalkauf) → Webhook bestätigt → Event mit package_id erstellen.
|
||||
- **Reseller-Upgrade**: Im Admin-Dashboard Package auswählen → Subscription erstellen → expires_at setzen.
|
||||
- **Limits-Checks**: Middleware prüft Event-Package-Limits (Photos/Uploads), Tenant-Package für Event-Anzahl.
|
||||
- **Fallback**: Bestehende Tenants migrieren zu Free-Paket (z.B. if old_credits > 100 → Standard).
|
||||
- **UI/Features**: Wasserzeichen: if (!$package->watermark_allowed) hide; Branding: Custom Logo-Upload if allowed.
|
||||
- **Billing**: Stripe Products für jedes Package; Webhooks updaten Status (z.B. Subscription cancel → active=false).
|
||||
|
||||
## Migration von Altem System
|
||||
- Entferne: event_credits_balance aus tenants, event_purchases, event_credits_ledger.
|
||||
- Migriere: Für Events mit Credits → Zuordnen zu Free-Paket; Tenant-Balance → Initial Reseller S.
|
||||
- Skript: Artisan Command `php artisan migrate:packages` für Daten-Transfer.
|
||||
|
||||
Dieses Design ist final und bereit für Implementierung. Updates via PR.
|
||||
7
docs/archive/prp/99-glossary.md
Normal file
7
docs/archive/prp/99-glossary.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 99 — Glossary
|
||||
|
||||
- Tenant: Paying customer account (e.g., a couple, company).
|
||||
- Event: A photo session context under a tenant (wedding, party, etc.).
|
||||
- Guest PWA: Public attendee interface, anonymous by default.
|
||||
- Tenant Admin PWA: Store-ready management app for tenants.
|
||||
- Super Admin: Platform-level operator; Filament web panel only.
|
||||
25
docs/archive/prp/README.md
Normal file
25
docs/archive/prp/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Event Photo Platform — Product Requirement Plan (PRP)
|
||||
|
||||
Status: Active (split version)
|
||||
Date: 2025-09-08
|
||||
|
||||
This directory supersedes the legacy `fotospiel_prp.md`. Content is split into small, reviewable documents. See ADR-0006 for the Tenant Admin PWA decision.
|
||||
|
||||
- 01-architecture.md — System overview and components
|
||||
- 02-tenancy.md — Multi-tenant model and enforcement
|
||||
- 03-api.md — API-first contract and auth
|
||||
- 04-data-model-migrations.md — Portable migration intent (Schema builder)
|
||||
- 05-admin-superadmin.md — Super Admin web console (Filament)
|
||||
- 06-tenant-admin-pwa.md — Store-ready Tenant Admin PWA
|
||||
- 07-guest-pwa.md — Guest (event attendee) PWA
|
||||
- 08-billing.md — Packages (Einmalkäufe/Subscriptions), ledger, purchases
|
||||
- 15-packages-design.md — Package definitions, schema, integration
|
||||
- 09-security-compliance.md — RBAC, audit, GDPR
|
||||
- 10-storage-media-pipeline.md — Object storage, processing, CDN
|
||||
- 11-ops-ci-cd.md — CI, releases, environments
|
||||
- 12-i18n.md — Languages, locales, copy strategy
|
||||
- 99-glossary.md — Terms and roles
|
||||
|
||||
Notes
|
||||
- The original `fotospiel_prp.md` remains as historical reference and will not be edited further.
|
||||
- Any divergence should be resolved here; update ADRs when major decisions change.
|
||||
135
docs/archive/prp/marketing-checkout-payment-architecture.md
Normal file
135
docs/archive/prp/marketing-checkout-payment-architecture.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Marketing Checkout Payment Architecture (2025 Refactor)
|
||||
|
||||
## Goals
|
||||
- Replace the legacy marketing checkout flow with a single `CheckoutController` that owns auth, payment, and confirmation steps.
|
||||
- Support Stripe card payments and Paddle orders with a consistent state machine that can be extended to other providers.
|
||||
- Keep package activation logic idempotent and traceable while respecting GDPR (no new PII logging, no leaked tokens).
|
||||
- Prepare the frontend wizard to drive the flow as an SPA without relying on server-side redirects.
|
||||
|
||||
## Core Building Blocks
|
||||
- **CheckoutSession model/table** keeps one purchase attempt per user + package. It stores provider choice, status, pricing snapshot, and external ids (Stripe intent, Paddle order, etc.).
|
||||
- **CheckoutPaymentService** orchestrates provider-specific actions (create intent/order, capture, sync metadata) and normalises responses for the wizard.
|
||||
- **CheckoutAssignmentService** performs the idempotent write workflow (create/update TenantPackage, PackagePurchase, tenant subscription fields, welcome mail) once payment succeeds.
|
||||
- **Wizard API surface** (JSON routes under `/checkout/*`) is session-authenticated, CSRF-protected, and returns structured payloads consumed by the PWA.
|
||||
- **Webhooks** (Stripe, Paddle) map incoming provider events back to `CheckoutSession` rows to guarantee reconciliation and support 3DS / async capture paths.
|
||||
- **Feature Flag**: `config/checkout.php` exposes `CHECKOUT_WIZARD_ENABLED` and `CHECKOUT_WIZARD_FLAG` so the SPA flow can be toggled or gradual-rolled out during launch.
|
||||
- **Operational**: Track Sanctum PAT issuance via `personal_access_tokens` and document forced logout procedures (no OAuth key rotation required anymore).
|
||||
|
||||
## Payment State Machine
|
||||
State constants live on `CheckoutSession` (`status` column, enum):
|
||||
|
||||
| State | When it is used | Transitions |
|
||||
| --- | --- | --- |
|
||||
| `draft` | Session created, package locked in, no provider chosen. | `awaiting_payment_method`, `completed` (free), `cancelled` |
|
||||
| `awaiting_payment_method` | Paid package; provider picked, waiting for client to initialise SDK. | `requires_customer_action`, `processing`, `cancelled` |
|
||||
| `requires_customer_action` | Stripe 3DS, Paddle approval window open, or additional customer steps needed. | `processing`, `failed`, `cancelled` |
|
||||
| `processing` | Provider reported success, backend validating / capturing / assigning. | `completed`, `failed` |
|
||||
| `completed` | Checkout finished, package assigned, confirmation step unblocked. | none |
|
||||
| `failed` | Provider declined or capture check failed; retain reason. | `awaiting_payment_method` (retry), `cancelled` |
|
||||
| `cancelled` | User backed out or session expired (TTL 30 minutes). | none |
|
||||
|
||||
Each transition is recorded with a `status_history` JSON column (array of `{status, reason, at}`) for debugging.
|
||||
|
||||
## Backend Architecture
|
||||
|
||||
### Data Model
|
||||
Add `checkout_sessions` table + model:
|
||||
- `id` UUID primary key.
|
||||
- `user_id` (FK users) and optional `tenant_id` (set after registration creates tenant).
|
||||
- `package_id` (FK packages) and `package_snapshot` JSON (price, currency, package name, type, feature hash).
|
||||
- `status` enum (states above), `provider` enum (`stripe`, `paypal`, `free`, `none`).
|
||||
- `currency`, `amount_subtotal`, `amount_total` (DECIMAL(10,2)).
|
||||
- Provider fields: `stripe_payment_intent_id`, `stripe_customer_id`, `paypal_order_id`, `paypal_subscription_id`, `provider_metadata` JSON.
|
||||
- `locale`, `expires_at` (default now()+30 minutes), `completed_at`.
|
||||
- Timestamps + soft deletes (retain audit trail).
|
||||
|
||||
Create Eloquent model `App\Models\CheckoutSession` with casts for JSON columns and helper scopes (`active`, `byProviderId`).
|
||||
|
||||
### Routes & Controller Methods
|
||||
Group under `web.php` with `middleware(['auth', 'verified', 'locale', 'throttle:checkout'])`:
|
||||
- `POST /checkout/session` (`CheckoutController@storeSession`): create or resume active session for selected package, return `{id, status, amount, package_snapshot}`.
|
||||
- `PATCH /checkout/session/{session}/package` (`updatePackage`): allow switching package before payment; resets provider-specific fields and status to `draft`.
|
||||
- `POST /checkout/session/{session}/provider` (`selectProvider`): set provider (`stripe` or `paypal`), transitions to `awaiting_payment_method` and returns provider configuration (publishable key, Paddle client id, feature flags).
|
||||
- `POST /checkout/session/{session}/stripe-intent` (`createStripeIntent`): idempotently create/update PaymentIntent with metadata (user, tenant, package, session id) and deliver `{client_secret, intent_id}`.
|
||||
- `POST /checkout/session/{session}/stripe/confirm` (`confirmStripeIntent`): server-side verify PaymentIntent status (retrieve from Stripe) and transition to `processing` when `succeeded` or `requires_action`.
|
||||
- `POST /checkout/session/{session}/paypal/order` (`createPaddleOrder`): create order with `custom_id` payload (session, tenant, package) and return `{order_id, approve_url}`.
|
||||
- `POST /checkout/session/{session}/paypal/capture` (`capturePaddleOrder`): capture order server-side, transition to `processing` if status `COMPLETED`.
|
||||
- `POST /checkout/session/{session}/free` (`activateFreePackage`): bypass providers, run assignment service, mark `completed`.
|
||||
- `POST /checkout/session/{session}/complete` (`finalise`): provider-agnostic finishing hook used after `processing` to run `CheckoutAssignmentService`, persist `PackagePurchase`, queue mails, and respond with summary.
|
||||
- `GET /checkout/session/{session}` (`show`): used by wizard polling to keep state in sync (status, provider display data, failure reasons).
|
||||
- `DELETE /checkout/session/{session}` (`cancel`): expire session, clean provider artefacts (cancel intent/order if applicable).
|
||||
|
||||
Paddle routes remain under `routes/web.php` but call into new service classes; legacy marketing payment methods are removed once parity is verified.
|
||||
|
||||
### Services & Jobs
|
||||
- `CheckoutSessionService`: create/resume session, guard transitions, enforce TTL, and wrap DB transactions.
|
||||
- `CheckoutPaymentService`: entry point with methods `initialiseStripe`, `confirmStripe`, `initialisePaddle`, `capturePaddle`, `finaliseFree`. Delegates to provider-specific helpers (Stripe SDK, Paddle SDK) and persists external ids.
|
||||
- `CheckoutAssignmentService`: generates or reuses tenant, writes `TenantPackage`, `PackagePurchase`, updates user role/status, dispatches `Welcome` + purchase receipts, and emits domain events (`CheckoutCompleted`).
|
||||
- `SyncCheckoutFromWebhook` job: invoked by webhook controllers with provider payload, looks up `CheckoutSession` via provider id, runs assignment if needed, records failure states.
|
||||
|
||||
### Webhook Alignment
|
||||
- Update `StripeWebhookController` to resolve `CheckoutSession::where('stripe_payment_intent_id', intentId)`; when event indicates success, transition to `processing` (if not already), enqueue `SyncCheckoutFromWebhook` to finish assignment, and mark `completed` once done.
|
||||
- Update `PaddleWebhookController` similarly using `paypal_order_id` or `paypal_subscription_id`.
|
||||
- Webhooks become source-of-truth for delayed confirmations; wizard polls `GET /checkout/session/{id}` until `completed`.
|
||||
|
||||
### Validation & Security
|
||||
- All mutating routes use CSRF tokens and `auth` guard (session-based). Add `EnsureCheckoutSessionOwner` middleware enforcing that the session belongs to `request->user()`.
|
||||
- Input validation via dedicated Form Request classes (e.g., `StoreCheckoutSessionRequest`, `StripeIntentRequest`).
|
||||
- Provider responses are never logged raw; only store ids + safe metadata.
|
||||
- Abandon expired sessions via scheduler (`checkout:expire-sessions` artisan command). Command cancels open PaymentIntents and Paddle orders.
|
||||
|
||||
## Frontend Touchpoints
|
||||
|
||||
### Wizard Context Enhancements
|
||||
- Extend `CheckoutWizardState` to include `checkoutSessionId`, `paymentStatus`, `paymentError`, `isProcessing`, `provider`.
|
||||
- Add actions `initialiseSession(packageId)`, `selectProvider(provider)`, `updatePaymentStatus(status, payload)`.
|
||||
- Persist `checkoutSessionId` + status in `sessionStorage` for reload resilience (respect TTL).
|
||||
|
||||
### PaymentStep Implementation Plan
|
||||
- On mount (and whenever package changes), call `/checkout/session` to create/resume session. Reset state if API reports new id.
|
||||
- Provider tabs call `selectProvider`. Stripe tab loads Stripe.js dynamically (import from `@stripe/stripe-js`) and mounts Elements once `client_secret` arrives.
|
||||
- Stripe flow: submit button triggers `stripe.confirmCardPayment(clientSecret)`, handle `requires_action`, then POST `/checkout/session/{id}/stripe/confirm`. On success, call `/checkout/session/{id}/complete` and advance to confirmation step.
|
||||
- Paddle flow: render Paddle Buttons with `createOrder` -> call `/checkout/session/{id}/paypal/order`; `onApprove` -> POST `/checkout/session/{id}/paypal/capture`, then `/checkout/session/{id}/complete`.
|
||||
- Free packages skip provider selection; call `/checkout/session/{id}/free` and immediately advance.
|
||||
- Display status toasts based on `paymentStatus`; show inline error block when `failed` with `failure_reason` from API.
|
||||
|
||||
### Confirmation Step & Profile Link
|
||||
- Confirmation fetches `/checkout/session/{id}` summary (package, next steps, admin URL) and surfaces the dashboard link. Update `resources/js/pages/Profile/Index.tsx` to show "Checkout history" link pointing to marketing success page (from TODO item 4/5).
|
||||
|
||||
## Provider Flows
|
||||
|
||||
### Stripe (one-off and subscription)
|
||||
1. Session created (`draft`).
|
||||
2. Provider selected `stripe` -> `awaiting_payment_method`.
|
||||
3. `createStripeIntent` builds PaymentIntent with amount from package snapshot, metadata: session_id, package_id, user_id, tenant_id (if known), package_type.
|
||||
4. Frontend confirms card payment. If Stripe returns `requires_action`, wizard stores `requires_customer_action` and surfaces modal.
|
||||
5. Once Stripe marks intent `succeeded`, backend transitions to `processing`, calls `CheckoutAssignmentService`, and marks `completed`.
|
||||
6. For reseller packages, Stripe subscription is created after assignment using configured price ids; resulting subscription id stored on session + tenant record.
|
||||
|
||||
### Paddle (one-off and subscription)
|
||||
1. Session `draft` -> provider `paypal`.
|
||||
2. `createPaddleOrder` returns order id + approval link. Metadata includes session, tenant, package, package_type.
|
||||
3. After approval, `capturePaddleOrder` verifies capture status; on `COMPLETED`, transitions to `processing`.
|
||||
4. Assignment service runs, storing order id as `provider_id`. For subscriptions, capture handler stores subscription id and updates tenant subscription status.
|
||||
5. Webhooks handle late captures or cancellations (updates session -> `failed` or `cancelled`).
|
||||
|
||||
### Free Packages
|
||||
- `activateFreePackage` bypasses payment providers, writes TenantPackage + PackagePurchase with `provider_id = 'free'`, marks `completed`, and pushes the user to confirmation immediately.
|
||||
|
||||
## Migration Strategy
|
||||
1. **Phase 1** (current): land schema, services, new API routes; keep legacy MarketingController flow for fallback.
|
||||
2. **Phase 2**: wire the new wizard PaymentStep behind feature flag `checkout_v2` (in `.env` / config). Run internal QA with Paddle sandbox.
|
||||
3. **Phase 3**: enable feature flag for production tenants, monitor Paddle events, then delete legacy marketing payment paths and routes.
|
||||
4. **Phase 4**: tighten webhook logic and remove `MarketingController::checkout`, `::paypalCheckout`, `::stripeSubscription` once new flow is stable.
|
||||
|
||||
## Testing & QA
|
||||
- **Feature tests**: JSON endpoints for session lifecycle (create, provider select, intent creation, capture success/failure, free activation). Include multi-locale assertions.
|
||||
- **Payment integration tests**: use Stripe + Paddle SDK test doubles to simulate success, requires_action, cancellation, and ensure state machine behaves.
|
||||
- **Playwright**: wizard flow covering Stripe happy path, Stripe 3DS stub, Paddle approval, failure retry, free package shortcut, session resume after refresh.
|
||||
- **Webhooks**: unit tests for mapping provider ids to sessions, plus job tests for idempotent assignment.
|
||||
- **Scheduler**: test `checkout:expire-sessions` to confirm PaymentIntents are cancelled and sessions flagged `cancelled`.
|
||||
|
||||
## Open Questions / Follow-Ups
|
||||
- Map package records to Stripe price ids and Paddle plan ids (store on `packages` table or config?).
|
||||
- Confirm legal copy updates for new checkout experience before GA.
|
||||
- Align email templates (welcome, receipt) with new assignment service outputs.
|
||||
70
docs/archive/prp/marketing-frontend-unification.md
Normal file
70
docs/archive/prp/marketing-frontend-unification.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Vereinheitlichung des Marketing-Frontends: Von Hybrid (Blade + React) zu konsistentem Inertia/React-Layout
|
||||
|
||||
## Problemstellung
|
||||
Das aktuelle Marketing-Frontend kombiniert Blade-Templates (z.B. für statische Seiten wie Blog, Legal) mit Vite/React-Komponenten (z.B. Packages, Register). Bei rein React-gerenderten Seiten fehlt das Layout (Header, Footer) und das Styling (z.B. Aurora-Gradient, Fonts), was zu inkonsistentem UX führt:
|
||||
- Blade-Seiten: Vollständiges Layout via @extends('layouts.marketing').
|
||||
- React-Seiten: Nur Komponente, kein Wrapper → Kein Header/Footer, anderes Styling.
|
||||
|
||||
Ziel: Vollständige Migration zu Inertia.js für SPA-ähnliche Konsistenz, mit einem zentralen React-Layout für alle Marketing-Seiten. Vorteile: Einheitliches Design, bessere Navigation, einfachere Wartung.
|
||||
|
||||
## Architektur-Vorschlag
|
||||
### 1. Kernkomponenten
|
||||
- **MarketingLayout.tsx** (resources/js/layouts/MarketingLayout.tsx): Wrapper für alle Marketing-Seiten.
|
||||
- Header: Logo, Navigation (Home, Packages, Blog, Occasions, Register/Login).
|
||||
- Hauptinhalt: `{children}` (die spezifische Page-Komponente).
|
||||
- Footer: Impressum, Datenschutz, Social-Links, Copyright.
|
||||
- Styling: Tailwind-Klassen für Aurora-Gradient (bg-aurora-enhanced), Fonts (Playfair Display für Überschriften, Montserrat für Text).
|
||||
- **Globale Styles** (resources/css/app.css):
|
||||
- @font-face für Montserrat und Playfair Display (via Google Fonts oder lokal).
|
||||
- .bg-aurora-enhanced: radial-gradient(circle at 20% 80%, #a8edea 0%, #fed6e3 50%, #d299c2 100%) + linear-gradient + animation (shadcn-Style).
|
||||
- Theme: Primärfarbe #FFB6C1, responsive (mobile-first).
|
||||
|
||||
### 2. Routing & Controller
|
||||
- **web.php** (routes/web.php): Alle /marketing/*-Routes zu Inertia::render umstellen.
|
||||
- Beispiel: Route::inertia('/marketing/packages', 'Marketing/Packages');
|
||||
- **MarketingController.php** (app/Http/Controllers/MarketingController.php):
|
||||
- Methoden (z.B. packages(), blog(), register()) liefern Props (z.B. packages: Package::all()->map(fn($p) => ['id' => $p->id, 'features' => $p->features, 'limits' => $p->limits])).
|
||||
- Für dynamische Inhalte: DB-Queries (z.B. BlogPosts für /blog).
|
||||
|
||||
### 3. Page-Komponenten
|
||||
- Alle Marketing-Seiten als React/TSX (resources/js/pages/marketing/*.tsx):
|
||||
- z.B. Packages.tsx: Rendert Paket-Karten in Grid/Carousel (shadcn), mit Modal für Details/Upsell.
|
||||
- Wrapper: In App.tsx oder router.tsx: if (route.startsWith('/marketing')) return `<MarketingLayout><Page /></MarketingLayout>`;
|
||||
- Migration-Reihenfolge:
|
||||
1. Statische Seiten (Home, Blog-Index): Von Blade zu Inertia.
|
||||
2. Dynamische (Packages, Register): Props integrieren.
|
||||
3. Legal-Seiten: Als einfache Inertia-Pages (statischer Text).
|
||||
|
||||
### 4. Technische Umsetzung
|
||||
- **Inertia-Setup**: Stelle sicher, config/inertia.php hat middleware für SSR (optional) und shared props (z.B. auth, flash).
|
||||
- **Auth-Integration**: usePage().props.auth für CTAs (z.B. Login-Button im Header).
|
||||
- **Responsive Design**: Tailwind: block md:hidden für Mobile-Carousel in Packages.
|
||||
- **Fonts & Assets**: Vite-Plugin für Fonts/SVGs; preload in Layout.
|
||||
- **Analytics (Matomo)**: Aktivierung via `.env` (`MATOMO_ENABLED=true`, `MATOMO_URL`, `MATOMO_SITE_ID`). `AppServiceProvider` teilt die Konfiguration als `analytics.matomo`; `MarketingLayout` rendert `MatomoTracker`, der das Snippet aus `/docs/piwik-trackingcode.txt` nur bei erteilter Analyse-Zustimmung lädt, `disableCookies` setzt und bei jedem Inertia-Navigationsevent `trackPageView` sendet. Ein lokalisierter Consent-Banner (DE/EN) übernimmt die DSGVO-konforme Einwilligung und ist über den Footer erneut erreichbar.
|
||||
- **Tests**: E2E mit Playwright (z.B. navigate to /packages, check header/footer presence).
|
||||
|
||||
### 5. Diagramm: Layout-Struktur
|
||||
```
|
||||
MarketingLayout.tsx
|
||||
├── Header (Navigation, Logo)
|
||||
├── {children} (z.B. Packages.tsx)
|
||||
│ ├── Hero (Aurora-Gradient)
|
||||
│ ├── Content (Grid/Carousel)
|
||||
│ └── CTA-Section
|
||||
└── Footer (Legal-Links)
|
||||
```
|
||||
|
||||
### 6. Migrations-Schritte (für Code-Modus)
|
||||
1. Erstelle MarketingLayout.tsx und integriere in Router.
|
||||
2. Migriere eine Test-Seite (z.B. /packages): Controller + Page-Komponente.
|
||||
3. Passe app.css an (Fonts, Gradients).
|
||||
4. Test: npm run dev, Browser-Check auf Layout-Konsistenz.
|
||||
5. Vollständige Migration: Alle Blade-Seiten umstellen.
|
||||
6. Edge-Cases: SEO (Inertia Head), Performance (Lazy-Loading).
|
||||
|
||||
### 7. Risiken & Mitigation
|
||||
- Layout-Brüche während Migration: Fallback zu Blade via Feature-Flag.
|
||||
- Styling-Konflikte: CSS-Isolation mit Tailwind-Prefix.
|
||||
- Performance: Code-Splitting für große Pages.
|
||||
|
||||
Dieser Plan basiert auf bestehender Struktur (docs/prp/ als Referenz). Nach Umsetzung: Update PRP (docs/prp/01-architecture.md).
|
||||
41
docs/archive/prp/packages-ui-improvements.md
Normal file
41
docs/archive/prp/packages-ui-improvements.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Packages-Seite UI-Verbesserungen: Analyse und Plan
|
||||
|
||||
## Recherche-Zusammenfassung
|
||||
### Features und Abgrenzungen (aus docs/prp/15-packages-design.md, Model/Seeder)
|
||||
- **Endkunden-Pakete (Einmalkauf pro Event)**:
|
||||
- Free/Test (0€): max_photos=30, max_guests=50, gallery_days=7, max_tasks=5, watermark_allowed=true, features=['basic_uploads', 'limited_sharing'].
|
||||
- Starter (29€): max_photos=200, max_guests=100, gallery_days=30, max_tasks=10, watermark_allowed=true, features=['basic_uploads', 'unlimited_sharing', 'no_watermark', 'custom_tasks'].
|
||||
- Pro (79€): max_photos=1000, max_guests=500, gallery_days=90, max_tasks=20, watermark_allowed=false, features=['basic_uploads', 'unlimited_sharing', 'no_watermark', 'custom_tasks', 'advanced_analytics', 'priority_support', 'live_slideshow'].
|
||||
- **Reseller-Pakete (Subscription jährlich)**:
|
||||
- S (199€): max_events_per_year=5, max_photos=500 (per Event), features=['reseller_dashboard', 'custom_branding', 'priority_support'].
|
||||
- M (399€): max_events_per_year=15, max_photos=1000, features=['reseller_dashboard', 'custom_branding', 'priority_support', 'advanced_reporting'].
|
||||
- Enterprise (999€+): Unlimited, White-Label, Custom Domain.
|
||||
- **Limits**: max_photos, max_guests, gallery_days, max_tasks, max_events_per_year (vererbt an Events).
|
||||
- **Abgrenzungen**: Watermark/Branding (bool), Support (priority), Analytics (advanced), Galerie-Dauer, Task-Anzahl. Filament: TenantResource trackt active_package, remaining_events; RelationManagers für Purchases/Packages (manual add).
|
||||
|
||||
### Carousel-Umsetzung
|
||||
- Ja, vollständig: shadcn Carousel für mobile (block md:hidden, Swipe mit Previous/Next Buttons, basis-full Items); Desktop: hidden md:block Grid (md:grid-cols-3/2). Dynamisch aus Props (endcustomerPackages, resellerPackages).
|
||||
|
||||
### Umgesetzte UI-Verbesserungen
|
||||
- Responsive Design: Mobile Carousel vs Desktop Grid.
|
||||
- Dynamische Darstellung: Features map() aus JSON, partial Limits (max_photos, max_tenants).
|
||||
- Hero-Section: Aurora-Gradient (bg-aurora-enhanced), Playfair Display Überschrift, Montserrat Text.
|
||||
- Konsistentes Layout: MarketingLayout (Header mit Nav, Footer mit Legal-Links).
|
||||
- CTA: Links zu /buy-packages/\{id\}, Hover-Transitions.
|
||||
- Fonts: font-display (Playfair), font-sans-marketing (Montserrat).
|
||||
|
||||
### Offene/Mögliche Verbesserungen
|
||||
1. **Multi-Step-Modal auf Card-Click**: Dialog (shadcn) mit Tabs (Step 1: Details + Social Proof/Testimonials (3 Cards mit Stars); Step 2: Upsell-Tabelle (shadcn Table, Spalten: Features/Limits, Zeilen: alle Packages, Highlight selected mit bg-[#FFB6C1]); Step 3: CTA (usePage().props.auth ? Link /buy-packages : /register?package_id, localStorage pre-fill für Name/Email)).
|
||||
2. **Erweiterte Limits-Darstellung**: Vollständig in Cards (gallery_days, max_guests, max_tasks als <li>, watermark/branding als Badge/Check/X-Icons).
|
||||
3. **UI-Enhancements**: Progress Bar (33/66/100% für Steps), Micro-Interactions (Card-Hover: scale-105/shadow-lg), FAQ-Section (Accordion mit 4 Fragen: Free-Paket, Upgrade, Reseller, Zahlung), Testimonials-Section (3 Cards mit Quotes/Ratings).
|
||||
4. **Desktop Pricing Table**: Toggle-Button neben Grid (View: Table-Modus, Vergleichs-View mit Checkmarks für Features).
|
||||
5. **Weitere**: A/B-Testing (CTAs), Accessibility (ARIA-Labels für Carousel/Modal, Keyboard-Nav), SEO (Head meta description pro Package), Performance (Lazy Testimonials), Integration (Track Clicks mit Analytics).
|
||||
|
||||
## Implementierungs-Plan (Code-Modus)
|
||||
1. **Modal hinzufügen**: useState für open/selected/step; Dialog mit Tabs; Step 1: Details + Testimonials; Step 2: Table (alle Packages); Step 3: CTA (auth-check, pre-fill).
|
||||
2. **Limits erweitern**: In Cards <li> für gallery_days/max_guests/max_tasks; Badges für watermark/branding.
|
||||
3. **UI-Verbesserungen**: Progress in Modal, Hover auf Cards, FAQ-Accordion, Testimonials-Section.
|
||||
4. **Pricing Table**: useState für viewMode (Grid/Table); Table mit Check/X für Features.
|
||||
5. **Test**: npm run build/dev; Browser: Card-Click → Modal-Steps, Tabelle-Vergleich, CTA-Redirect, Responsiveness.
|
||||
|
||||
Nach Umsetzung: Update PRP (docs/prp/15-packages-design.md mit UI-Details).
|
||||
28
docs/archive/prp/public-entrypoints.md
Normal file
28
docs/archive/prp/public-entrypoints.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Public Entry Points
|
||||
|
||||
This overview lists every user-facing URL surface, grouped by persona, and notes authentication/expiry rules.
|
||||
|
||||
## Marketing Site
|
||||
- `/` — marketing landing page.
|
||||
- `/packages` — package overview.
|
||||
- `/checkout/{package}` — checkout wizard (requires logged-in tenant or email login within flow).
|
||||
- `/blog`, `/contact`, `/impressum`, `/datenschutz`, `/agb` — legal and marketing content.
|
||||
|
||||
## Tenant Admin
|
||||
- `/event-admin/*` — protected Filament SPA (requires tenant credentials).
|
||||
- `/tenant/events/{event}/photos/archive` — authenticated ZIP export for approved photos (tenant ownership enforced).
|
||||
|
||||
## Guest PWA (event-bound)
|
||||
- `/event` — landing for new guests (code entry / QR).
|
||||
- `/e/{token}` — full guest experience (home, tasks, gallery, upload) gated by join token; token expiry revokes access.
|
||||
- `/g/{token}` — read-only public gallery (new). Shows approved photos themed by event branding; downloads allowed while token valid and gallery duration active.
|
||||
- `/setup/{token}` — onboarding/profile setup for guests.
|
||||
|
||||
## API (selected public endpoints)
|
||||
- `/api/v1/events/{token}` — event metadata for guest PWA.
|
||||
- `/api/v1/events/{token}/photos` — guest gallery polling (legacy PWA).
|
||||
- `/api/v1/gallery/{token}` — public gallery metadata (new).
|
||||
- `/api/v1/gallery/{token}/photos` — public gallery pagination (new).
|
||||
- `/api/v1/gallery/{token}/photos/{photo}/download` — single photo download (new).
|
||||
|
||||
All other `/api/v1/*` routes require authenticated tenant or super-admin access as documented in `docs/prp/03-api.md`.
|
||||
50
docs/archive/prp/tenant-app-specs/README.md
Normal file
50
docs/archive/prp/tenant-app-specs/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Detaillierte PRP für Tenant Admin App (Capacitor + Framework7)
|
||||
|
||||
## Status
|
||||
- **Aktualisiert**: 2025-10-17 (Onboarding Fusion & QR Revamp)
|
||||
- **Version**: 1.2.0
|
||||
- **Autor**: Core Platform Team (Codex)
|
||||
- **Supersedes**: docs/prp/06-tenant-admin-pwa.md (legacy Framework7 reference)
|
||||
|
||||
## Überblick
|
||||
Diese detaillierte Product Requirement Plan (PRP) beschreibt die Spezifikationen für die Tenant Admin App. Die App ist eine store-ready mobile Anwendung, die mit Capacitor für iOS und Trusted Web Activity (TWA) für Android gepackt wird. Die UI basiert auf Framework7 für ein natives Mobile-Erlebnis. Die App ermöglicht Tenant-Admins (z.B. Event-Organisatoren) die vollständige Verwaltung ihrer Events, Galerien, Mitglieder, Einstellungen und Käufe über eine API-first Backend-Integration.
|
||||
|
||||
Die App ersetzt das frühere Filament-basierte Tenant-Panel und fokussiert auf eine Mobile-First-UX mit Offline-Fähigkeiten, Push-Notifications und sicherer Authentifizierung. Sie respektiert das Multi-Tenancy-Modell und GDPR-Anforderungen. Seit Oktober 2025 wird das UI in React 19 + Vite + Tailwind/shadcn/ui umgesetzt; die alte Framework7-Schicht bleibt nur als historische Referenz erhalten.
|
||||
|
||||
## Aktuelle Highlights (Q4 2025)
|
||||
- **Geführtes Onboarding**: `/event-admin/welcome/*` orchestriert den Welcome Flow (Hero → How-It-Works → Paketwahl → Zusammenfassung → Event Setup). Guarding erfolgt über `onboarding_completed_at`.
|
||||
- **Direkter Checkout**: Paddle sind in die Paketwahl des Welcome Flows eingebettet; Fortschritt wird im Onboarding-Context persistiert.
|
||||
- **Filament Wizard**: Für Super-Admins existiert ein paralleler QR/Join-Token-Wizard in Filament (Token-Generierung, Layout-Downloads, Rotation).
|
||||
- **Join Tokens only**: Gäste erhalten ausschließlich join-token-basierte Links/QRs; slug-basierte URLs wurden deaktiviert. QR-Drucklayouts liegen unter `resources/views/pdf/join-tokens/*`.
|
||||
- **Auth Alignment**: Sanctum-PATs über `/api/v1/tenant-auth/login` + `/tenant-auth/exchange`; siehe `docs/prp/tenant-app-specs/api-usage.md`.
|
||||
|
||||
## Kernziele
|
||||
- **Deliverables**: Voll funktionsfähige App mit CRUD-Operationen für Tenant-Ressourcen (Events, Photos, Tasks, etc.).
|
||||
- **UI/UX**: Framework7-Komponenten für konsistente, native Mobile-Interfaces (iOS/Android).
|
||||
- **Technologie-Stack**: React/Vite (Core), Framework7 (UI), Capacitor (Native), Sanctum PATs (Auth).
|
||||
- **Distribution**: App Store (iOS), Google Play (Android), PWA-Install (Web).
|
||||
|
||||
## Struktur dieser PRP
|
||||
- **README.md**: Dieser Überblick.
|
||||
- **functional-specs.md**: Funktionale Anforderungen, Capabilities und API-Integration.
|
||||
- **pages-ui.md**: Detaillierte Seitenbeschreibungen, Framework7-Komponenten und Wireframe-Ideen.
|
||||
- **settings-config.md**: App- und Tenant-spezifische Einstellungen, Capacitor-Plugins.
|
||||
- **capacitor-setup.md**: Packaging, Distribution und Native-Features.
|
||||
|
||||
## Referenzen
|
||||
- **Haupt-PRP**: docs/prp/README.md
|
||||
- **Tenancy**: docs/prp/02-tenancy.md
|
||||
- **API**: docs/prp/03-api.md
|
||||
- **Bestehende Tenant PWA**: docs/prp/06-tenant-admin-pwa.md
|
||||
- **Addendum**: docs/prp-addendum-2025-09-08-tenant-admin-pwa.md
|
||||
- **ADR**: docs/adr/ADR-0006-tenant-admin-pwa.md
|
||||
- **Billing**: docs/prp/08-billing.md
|
||||
- **Glossar**: docs/prp/99-glossary.md
|
||||
|
||||
## Änderungen und Erweiterungen
|
||||
Diese PRP erweitert die knappe Beschreibung in 06-tenant-admin-pwa.md um:
|
||||
- Spezifische Seiten und UI-Elemente mit Framework7.
|
||||
- Detaillierte Settings und Capacitor-Integration.
|
||||
- Mobile-spezifische Features wie Push-Notifications und Offline-Sync.
|
||||
|
||||
Für Feedback oder Änderungen: Siehe TODO.md oder Issues.
|
||||
278
docs/archive/prp/tenant-app-specs/api-usage.md
Normal file
278
docs/archive/prp/tenant-app-specs/api-usage.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# API-Nutzung der Tenant Admin App
|
||||
|
||||
Diese Dokumentation beschreibt alle API-Endpunkte der Tenant Admin App. Alle Requests sind tenant-scoped und erfordern ein Sanctum Personal Access Token (PAT) mit der Fähigkeit `tenant-admin` bzw. `tenant:\{id\}`.
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
### Passwort-Login (PAT)
|
||||
- **Endpoint**: `POST /api/v1/tenant-auth/login`
|
||||
- **Body** (`application/json`): `{ "login": "username oder email", "password": "••••" }`
|
||||
- **Response**: `{ token, token_type, abilities[], user { ... } }`
|
||||
- **Fehler**: `422` bei ungültigen Daten, `429` bei Rate-Limit (`throttle:tenant-auth`).
|
||||
- **Hinweis**: `login` akzeptiert E-Mail _oder_ Usernamen. Erfolgreiche Logins revoken vorhandene PATs mit Name `tenant-admin`.
|
||||
|
||||
### Session→PAT Exchange (Google/Login-Shell)
|
||||
- **Endpoint**: `POST /api/v1/tenant-auth/exchange`
|
||||
- **Middleware**: Sanctum stateful cookies (`SANCTUM_STATEFUL_DOMAINS`), `throttle:tenant-auth`.
|
||||
- **Use-Case**: Nach Google-Login oder Marketing-Checkout Session wird das PAT ohne erneute Passwort-Eingabe ausgegeben.
|
||||
- **Response**: identisch zum Passwort-Login.
|
||||
|
||||
### Token Status & Logout
|
||||
- **`GET /api/v1/tenant-auth/me`** – liefert `{ user, tenant, abilities }` und spiegelt die PAT-Fähigkeiten wider.
|
||||
- **`POST /api/v1/tenant-auth/logout`** – löscht das aktuelle PAT aus `personal_access_tokens`.
|
||||
|
||||
### Legacy Payload `/tenant/me`
|
||||
- **Endpoint**: `GET /api/v1/tenant/me`
|
||||
- **Response**: historisches Format (`tenant_id`, `remaining_events`, `scopes`), betrieben über denselben PAT.
|
||||
- **Kompatibilität**: Dient älteren Clients als Übergang; neue Features sollten `/tenant-auth/me` verwenden.
|
||||
|
||||
## Dashboard
|
||||
|
||||
### Stats laden
|
||||
- **GET /api/v1/tenant/dashboard**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`
|
||||
- **Response**: `{ active_package, active_events, new_photos, task_progress }`
|
||||
- **Zweck**: Übersicht-Daten für Dashboard-Cards (active_package: current tenant package info)
|
||||
|
||||
## Events
|
||||
|
||||
### Events-Liste
|
||||
- **GET /api/v1/tenant/events**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`
|
||||
- **Params**:
|
||||
- `page=1` (Pagination)
|
||||
- `per_page=50` (max für Mobile)
|
||||
- `status=draft|active|archived` (Filter)
|
||||
- `search=query` (Suche in Titel/Ort)
|
||||
- **Response**: `{ data: Event[], current_page, last_page, total }`
|
||||
- **Event-Shape**: `{ id, title, date, location, status, photoCount, slug }`
|
||||
|
||||
### Event erstellen
|
||||
- **POST /api/v1/tenant/events**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `Content-Type: application/json`
|
||||
- **Body**: `{ title, date, location, description, package_id }`
|
||||
- **Response**: 201 Created mit erstelltem Event
|
||||
- **Validierung**: Prüft Tenant-Package (Reseller-Limit) und erstellt Event-Package (Einmalkauf oder Free)
|
||||
|
||||
### Event-Details
|
||||
- **GET /api/v1/tenant/events/\{slug\}**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`
|
||||
- **Response**: Erweitertes Event mit `{ tasks[], members, stats { likes, views, uploads } }`
|
||||
|
||||
### Event updaten
|
||||
- **PATCH /api/v1/tenant/events/\{slug\}**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `Content-Type: application/json`, `If-Match: \{etag\}`
|
||||
- **Body**: Partial Event-Daten (title, date, location, description)
|
||||
- **Response**: Updated Event
|
||||
|
||||
### Event archivieren
|
||||
- **DELETE /api/v1/tenant/events/\{slug\}**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `If-Match: \{etag\}`
|
||||
- **Response**: 204 No Content (soft-delete)
|
||||
|
||||
## Photos
|
||||
|
||||
### Photos laden
|
||||
- **GET /api/v1/tenant/events/\{event_id\}/photos**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`
|
||||
- **Params**:
|
||||
- `page=1`, `per_page=50`
|
||||
- `status=pending|approved|rejected|featured`
|
||||
- `sort=date|likes`
|
||||
- `since=cursor` (für Infinite Scroll)
|
||||
- **Response**: `{ data: Photo[], current_page, last_page }`
|
||||
- **Photo-Shape**: `{ id, eventId, url, thumbnail, uploadedAt, status, likes, views, uploader, etag }`
|
||||
|
||||
### Upload-URL anfordern
|
||||
- **POST /api/v1/tenant/events/\{event_id\}/photos**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `Content-Type: application/json`
|
||||
- **Body**: `{ file_name, description? }`
|
||||
- **Response**: `{ id, upload_url (S3 signed), thumbnail_url }`
|
||||
|
||||
### Photo moderieren
|
||||
- **PATCH /api/v1/tenant/photos/\{id\}**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `Content-Type: application/json`, `If-Match: \{etag\}`
|
||||
- **Body**: `{ status: 'approved'|'rejected'|'featured', featured?, reason? }`
|
||||
- **Response**: Updated Photo
|
||||
|
||||
### Photo löschen
|
||||
- **DELETE /api/v1/tenant/photos/\{id\}**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `If-Match: \{etag\}`
|
||||
- **Response**: 204 No Content
|
||||
|
||||
## Members
|
||||
|
||||
### Mitglieder laden
|
||||
- **GET /api/v1/tenant/events/\{event_id\}/members**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`
|
||||
- **Params**: `page`, `per_page`, `status=pending|active|invited`
|
||||
- **Response**: `{ data: Member[], current_page, last_page }`
|
||||
- **Member-Shape**: `{ id, name, email, role, joinedAt, avatar?, status }`
|
||||
|
||||
### Mitglied einladen
|
||||
- **POST /api/v1/tenant/events/\{event_id\}/members**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `Content-Type: application/json`
|
||||
- **Body**: `{ email, role: 'member'|'guest', name? }`
|
||||
- **Response**: 201 Created, E-Mail wird versendet
|
||||
|
||||
## Tasks
|
||||
|
||||
### Tasks laden
|
||||
- **GET /api/v1/tasks**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`
|
||||
- **Params**:
|
||||
- `global=true/false` (globale vs. tenant Tasks)
|
||||
- `tenant_id=me` (nur eigene Tasks)
|
||||
- `category=setup|photo|event|cleanup`
|
||||
- **Response**: `{ data: Task[], global: boolean }`
|
||||
- **Task-Shape**: `{ id, title, description?, category, isGlobal, tenantId?, createdAt, color? }`
|
||||
|
||||
### Event-Tasks laden
|
||||
- **GET /api/v1/tenant/events/\{event_id\}/tasks**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`
|
||||
- **Response**: `{ data: EventTask[], overall_progress }`
|
||||
- **EventTask-Shape**: `{ id, eventId, taskId, task: Task, order, completed, assignedTo?, progress }`
|
||||
|
||||
### Tasks bulk zuweisen
|
||||
- **POST /api/v1/tenant/events/\{event_id\}/tasks/bulk**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `Content-Type: application/json`
|
||||
- **Body**: `{ task_ids: string[], order: number[] }`
|
||||
- **Response**: Updated EventTasks mit neuer Reihenfolge
|
||||
|
||||
## Settings
|
||||
|
||||
### Settings laden
|
||||
- **GET /api/v1/tenant/settings**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`
|
||||
- **Response**: `{ primaryColor, tenantName, maxEventsPerMonth, enableTasks, enableEmotions, legalPages { impressumUrl, privacyUrl } }`
|
||||
|
||||
### Settings updaten
|
||||
- **PATCH /api/v1/tenant/settings**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `Content-Type: application/json`
|
||||
- **Body**: Partial Settings-Daten
|
||||
- **Response**: Updated Settings
|
||||
|
||||
## Billing
|
||||
|
||||
### Balance laden
|
||||
- **GET /api/v1/tenant/credits/balance**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`
|
||||
- **Response**: `{ balance: number }`
|
||||
|
||||
### Ledger-Verlauf
|
||||
- **GET /api/v1/tenant/credits/ledger**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`
|
||||
- **Params**: `page`, `per_page` (Pagination)
|
||||
- **Response**: `{ data: LedgerEntry[], current_page, last_page }`
|
||||
- **LedgerEntry**: `{ id, type, amount, credits, date, description, transactionId? }`
|
||||
|
||||
### Credits kaufen (In-App)
|
||||
- **POST /api/v1/tenant/credits/purchase**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `Content-Type: application/json`
|
||||
- **Body**: `{ package_id: string, credits_added: number, platform?: 'capacitor'|'web', transaction_id?: string, subscription_active?: boolean }`
|
||||
- **Response**: `{ message, balance, subscription_active }`
|
||||
- **Hinweis**: Wird nach erfolgreichen In-App-Kuferfolgen aufgerufen, aktualisiert Balance & Ledger.
|
||||
|
||||
### Credits synchronisieren
|
||||
- **POST /api/v1/tenant/credits/sync**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `Content-Type: application/json`
|
||||
- **Body**: `{ balance: number, subscription_active: boolean, last_sync: ISODateString }`
|
||||
- **Response**: `{ balance, subscription_active, server_time }`
|
||||
- **Hinweis**: Client meldet lokalen Stand; Server gibt Quelle-der-Wahrheit zurcck.
|
||||
|
||||
### Kauf-Intent erstellen
|
||||
- **POST /api/v1/tenant/purchases/intent**
|
||||
- **Headers**: `Authorization: Bearer \{token\}`, `Content-Type: application/json`
|
||||
- **Body**: `{ package_id }`
|
||||
- **Response**: `{ checkout_url: string }` (Stripe-Checkout)
|
||||
- **Nach dem Kauf**: Webhook-Handling auf Backend für Balance-Update
|
||||
|
||||
## Allgemeine Headers
|
||||
|
||||
Alle API-Requests enthalten:
|
||||
- **Authorization**: `Bearer \{access_token\}` (Sanctum PAT mit Fähigkeit `tenant:\{id\}`)
|
||||
- **Content-Type**: `application/json` (für POST/PATCH)
|
||||
- **If-Match**: `\{etag\}` (für Concurrency-Control bei Updates)
|
||||
- **Accept**: `application/json`
|
||||
|
||||
## Error-Handling
|
||||
|
||||
**Standard-Error-Response:**
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "PACKAGE_LIMIT_EXCEEDED",
|
||||
"message": "Package-Limit überschritten (z.B. max_photos)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP Status Codes:**
|
||||
- 200: Erfolg
|
||||
- 201: Created
|
||||
- 400: Bad Request (Validierungsfehler)
|
||||
- 401: Unauthorized (Token-Refresh wird versucht)
|
||||
- 403: Forbidden (RBAC-Verletzung)
|
||||
- 404: Not Found
|
||||
- 422: Unprocessable Entity
|
||||
- 429: Rate Limited (Client retry mit Backoff)
|
||||
|
||||
## Pagination
|
||||
|
||||
Alle Listen-Endpunkte unterstützen:
|
||||
- **page**: Aktuelle Seite (default 1)
|
||||
- **per_page**: Einträge pro Seite (default 20, max 50 für Mobile)
|
||||
- **Response**: `{ data: [], current_page, last_page, per_page, total }`
|
||||
|
||||
## Headers für Concurrency
|
||||
|
||||
Mutierende Endpunkte (PATCH/DELETE) erfordern:
|
||||
- **If-Match**: `\{etag\}` aus GET-Response
|
||||
- **Response**: 412 Precondition Failed bei Conflict
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- **Tenant-Isolation**: Middleware vergleicht PAT-Fähigkeit (`tenant:\{id\}`) mit dem angefragten Tenant
|
||||
- **RBAC**: Nur tenant_admin kann mutieren, member kann nur lesen/hochladen
|
||||
- **Rate Limiting**: 100 Requests/Minute pro Tenant
|
||||
- **ETag**: Automatische Concurrency-Control
|
||||
- **No PII-Logging**: Keine sensiblen Daten in Logs
|
||||
|
||||
## Testing
|
||||
|
||||
### API-Test-Setup
|
||||
1. **Backend starten**: Stelle sicher, dass die Hauptapp läuft und Endpunkte verfügbar sind.
|
||||
2. **Token erzeugen**: Verwende Postman mit gültigem Access-Token.
|
||||
3. **Endpoints testen**: Jeder Endpunkt einzeln mit curl oder Postman.
|
||||
|
||||
### Beispiel curl (mit Token)
|
||||
```bash
|
||||
curl -H "Authorization: Bearer \{token\}" \
|
||||
-H "Content-Type: application/json" \
|
||||
https://api.fotospiel.com/api/v1/tenant/events
|
||||
```
|
||||
|
||||
### Frontend-Test
|
||||
1. `.env` mit korrekter API-URL konfigurieren.
|
||||
2. `npm run dev` starten.
|
||||
3. Browser-Network-Tab überprüfen für API-Calls.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Environment-Variablen
|
||||
- **VITE_API_URL**: Backend-API-URL (Pflicht)
|
||||
- **VITE_ENABLE_TENANT_SWITCHER**: Dev-Flag um Tenants im Header auszuwählen (optional, Default `false`)
|
||||
- **VITE_REVENUECAT_PUBLIC_KEY**: Optional für In-App-Käufe (RevenueCat)
|
||||
- **SANCTUM_STATEFUL_DOMAINS** (Backend): Enthält die Origins des Admin-PWA/TWA, damit der Session→PAT-Exchange funktioniert.
|
||||
- **REVENUECAT_WEBHOOK_SECRET / REVENUECAT_PRODUCT_MAPPINGS / REVENUECAT_APP_USER_PREFIX / REVENUECAT_WEBHOOK_QUEUE**: Backend-Konfiguration für RevenueCat-Webhooks, siehe `config/services.php`.
|
||||
|
||||
### Build & Deploy
|
||||
1. **Development**: `npm run dev`
|
||||
2. **Production**: `npm run build`
|
||||
3. **Vorschau**: `npm run preview`
|
||||
|
||||
### PWA-Installation
|
||||
- App ist PWA-fähig (Manifest, Service Worker).
|
||||
- Installierbar auf Desktop/Mobile via "Zum Startbildschirm hinzufügen".
|
||||
|
||||
Für weitere Details siehe die spezifischen Dokumentationsdateien.
|
||||
155
docs/archive/prp/tenant-app-specs/capacitor-setup.md
Normal file
155
docs/archive/prp/tenant-app-specs/capacitor-setup.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Capacitor Setup für die Tenant Admin App
|
||||
|
||||
## Status
|
||||
- **Version**: 1.0.0 (2025-09-13)
|
||||
- **Fokus**: Mobile Packaging, Distribution und Native-Integration basierend auf ADR-0006.
|
||||
|
||||
## Überblick
|
||||
Die Tenant Admin App wird als Progressive Web App (PWA) entwickelt und mit Capacitor für native Mobile-Distribution gepackt. Dies ermöglicht:
|
||||
- **Android**: Trusted Web Activity (TWA) für Chrome-basierte Installation (Google Play) oder voller Capacitor-Build bei Bedarf an Native APIs.
|
||||
- **iOS**: Capacitor-App für App Store-Distribution mit Push-Notifications und Keychain-Support.
|
||||
- **Web/PWA**: Fallback für Browser-Installation (A2HS) mit Service Worker.
|
||||
|
||||
Der Build-Prozess integriert sich in das bestehende Vite-Setup (resources/js/admin). Nach `npm run build` wird `npx cap sync` ausgeführt, um die Web-Assets in native Projekte zu kopieren.
|
||||
|
||||
## Projektstruktur
|
||||
```
|
||||
apps/admin-pwa/ # React/Vite Source (Haupt-App)
|
||||
├── src/ # Components, Pages, API
|
||||
├── vite.config.ts # Framework7 + Capacitor-Integration
|
||||
├── capacitor.config.ts # Native Config
|
||||
├── android/ # Capacitor Android-Projekt (TWA oder App)
|
||||
├── ios/ # Capacitor iOS-Projekt (Xcode)
|
||||
└── package.json # Dependencies (framework7, @capacitor/*)
|
||||
|
||||
packages/mobile/ # Shared Native-Config (optional)
|
||||
├── fastlane/ # iOS/Android Deployment
|
||||
├── assetlinks.json # TWA Digital Asset Links
|
||||
└── privacy-manifest.json # iOS Privacy
|
||||
```
|
||||
|
||||
## Packaging und Build-Prozess
|
||||
|
||||
### 1. Vite + Framework7 Setup
|
||||
- **Dependencies**: `framework7`, `framework7-react`, `@capacitor/core`, `@capacitor/cli`.
|
||||
- **vite.config.ts** (Auszug):
|
||||
```typescript
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { framework7 } from 'framework7/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), framework7()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: true,
|
||||
},
|
||||
define: {
|
||||
__CAPACITOR__: true, // Enable Capacitor globals
|
||||
},
|
||||
});
|
||||
```
|
||||
- **Build-Skript** (package.json): `"build": "vite build && npx cap sync"`.
|
||||
|
||||
### 2. Capacitor Initialisierung
|
||||
- **Befehle**:
|
||||
```
|
||||
npx cap init com.fotospiel.tenantadmin "Event Photo Admin"
|
||||
npx cap add android
|
||||
npx cap add ios
|
||||
npx cap sync # Kopiert dist/ in native Projekte
|
||||
```
|
||||
- **capacitor.config.ts** (siehe settings-config.md für erweiterte Plugins).
|
||||
|
||||
### 3. Android Packaging (TWA bevorzugt)
|
||||
- **Trusted Web Activity (TWA)**: Für store-ready Distribution ohne vollen Native-Wrapper.
|
||||
- **Voraussetzungen**: App bound an `admin.fotospiel.app` (HTTPS); Digital Asset Links.
|
||||
- **assetlinks.json** (public/.well-known/assetlinks.json):
|
||||
```json
|
||||
[{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.fotospiel.tenantadmin",
|
||||
"sha256_cert_fingerprints": ["DE:AD:BE:EF:..."] // Von Play Console
|
||||
}
|
||||
}]
|
||||
```
|
||||
- **Build**: `npx cap open android` → Android Studio → Generate Signed Bundle (AAB für Play Store).
|
||||
- **Vorteile**: Kleiner Footprint, Web-Push via Chrome; Fallback zu Capacitor bei Bedarf (z.B. Biometrie).
|
||||
- **TWA-Tools**: `npm i -g @bubblewrap/cli` → `bubblewrap init --manifest=https://admin.fotospiel.app/manifest.json`.
|
||||
|
||||
- **Vollständiger Capacitor-Build** (falls Native-Plugins benötigt):
|
||||
- `npx cap sync android`
|
||||
- `npx cap open android` → Build APK/AAB mit ProGuard.
|
||||
|
||||
### 4. iOS Packaging (Capacitor)
|
||||
- **Xcode-Setup**: `npx cap open ios` → Signing mit Apple Developer Account.
|
||||
- **Privacy Manifest** (ios/App/App/PrivacyInfo.xcprivacy):
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string> <!-- Preferences für Settings -->
|
||||
</array>
|
||||
</dict>
|
||||
<!-- Push, Camera, etc. -->
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
- **Build**: Archive in Xcode → Upload zu App Store Connect.
|
||||
- **Entitlements**: Push-Notifications (APNs), Background App Refresh für Sync.
|
||||
|
||||
### 5. Distribution und CI/CD
|
||||
- **Google Play**:
|
||||
- **TWA**: Internal Testing Track; Target SDK 34+; Feature: Installable.
|
||||
- **Fastlane**: `packages/mobile/fastlane/android` mit `supply` für Metadata/Screenshots.
|
||||
- **Versioning**: Align mit Backend (z.B. 1.0.0); Feature Flags via API.
|
||||
|
||||
- **Apple App Store**:
|
||||
- **Capacitor**: Review-Guidelines beachten (HTTPS-only, No Crash-Reporting ohne Consent).
|
||||
- **Fastlane**: `packages/mobile/fastlane/ios` mit `deliver` für Upload.
|
||||
- **Privacy**: Usage Descriptions in Info.plist (z.B. "Kamera für QR-Scans").
|
||||
|
||||
- **PWA-Fallback** (Web):
|
||||
- **manifest.json**: liegt unter `public/manifest.json` (Scope `/event-admin/`, Theme-Farbe `#f43f5e`, Shortcuts für Welcome & Dashboard).
|
||||
- **Service Worker**: `public/admin-sw.js` cached Shell + Assets, liefert Offline-Fallback `/event-admin` für Navigations-Anfragen.
|
||||
- **Distribution**: Hosting auf `admin.fotospiel.app` mit A2HS-Prompt; Bubblewrap/TWA nutzt `https://admin.fotospiel.app/manifest.json`.
|
||||
|
||||
### Native Features (Erweiterung zu settings-config.md)
|
||||
- **Push-Notifications**:
|
||||
- **Android**: FCM-Integration; Token an Backend (`POST /tenant/push-tokens`).
|
||||
- **iOS**: APNs; Silent-Pushes für Sync-Triggers.
|
||||
- **Cross-Platform**: Capacitor Push-Plugin handhabt Plattform-Unterschiede.
|
||||
|
||||
- **Secure Storage**:
|
||||
- **Tokens**: Capacitor Preferences mit Auto-Encryption.
|
||||
- **Offline Data**: IndexedDB (Web) + Native Storage; Encryption via Web Crypto API.
|
||||
|
||||
- **Background Processing**:
|
||||
- **Sync**: Capacitor App-State-Listener für Foreground/Background; Queue Mutations.
|
||||
- **iOS**: Background Fetch (via Plugin); Android: WorkManager für periodische Syncs.
|
||||
|
||||
- **Biometrie (optional)**: `@capacitor-community/biometric-auth` für App-Lock.
|
||||
|
||||
### Testing und Deployment
|
||||
- **E2E-Tests**: Cypress für Web; Detox/Appium für Native (z.B. Push-Handling).
|
||||
- **CI/CD**: GitHub Actions oder Gogs-Integration (siehe docs/prp/11-ops-ci-cd.md).
|
||||
- Steps: Build → Test → Cap Sync → Fastlane Deploy (Staging → Production).
|
||||
- **Version Alignment**: App-Version matcht Backend-API-Version; Changelog in Store-Listing.
|
||||
|
||||
### Constraints & Red-Lines
|
||||
- **GDPR**: Keine implizite Tracking; Explizite Consent für Push/Camera.
|
||||
- **Security**: HTTPS-only; Token-Rotation alle 24h; No Jailbreak-Detection.
|
||||
- **Performance**: Bundle-Size < 10MB (Web-Assets komprimiert); Lazy-Loading.
|
||||
|
||||
Diese Setup ergänzt die funktionalen Specs und UI-Beschreibungen. Für Repo-Integration siehe ADR-0006.
|
||||
89
docs/archive/prp/tenant-app-specs/functional-specs.md
Normal file
89
docs/archive/prp/tenant-app-specs/functional-specs.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Funktionale Spezifikationen – Tenant-Admin-App
|
||||
|
||||
## Status
|
||||
- **Version**: 1.1.0 (Stand 2025-10-13)
|
||||
- **Ersetzt**: docs/prp/06-tenant-admin-pwa.md, docs/prp-addendum-2025-09-08-tenant-admin-pwa.md (Legacy-Referenz über Git History).
|
||||
|
||||
## Deliverables
|
||||
Die Admin-App muss folgende Kernfunktionen bereitstellen:
|
||||
- **Geführtes Onboarding**: Welcome Flow (Hero, How-It-Works, Paketwahl, Zusammenfassung, Erstes Event). Automatische Weiterleitung für Tenants ohne aktive Events.
|
||||
- **Event-Management**: Erstellen, Bearbeiten, Veröffentlichen, Archivieren; Join-Token-Verwaltung.
|
||||
- **Galerie-Management**: Upload, Moderation, Feature-Flags, Analytics.
|
||||
- **Mitglieder-Verwaltung**: Einladungen, Rollen, Zugriffskontrolle.
|
||||
- **Tasks & Emotions**: Bibliothek, Zuweisung, Fortschritts-Tracking.
|
||||
- **Abrechnung**: Paketübersicht, Paddle Checkout, Ledger.
|
||||
- **Einstellungen**: Branding, Limits, Rechtstexte, Benachrichtigungen.
|
||||
- **Offline-Support**: App-Shell-Caching, Queueing von Mutationen, Sync bei Reconnect.
|
||||
- **Compliance**: Audit-Logging, GDPR-konforme Löschung, ETag-basierte Konfliktlösung.
|
||||
|
||||
## Capabilities
|
||||
### Authentifizierung & Autorisierung
|
||||
- Sanctum Personal Access Tokens (`/api/v1/tenant-auth/login|exchange`) mit Fähigkeiten `tenant-admin`, `tenant:{id}`; Speicherung in IndexedDB sowie Keychain/Keystore für Capacitor.
|
||||
- Rollenbasierte Fähigkeiten: `tenant_admin` (vollständig), `member` (read-only, Upload). Token-Exchange nach Google-Login oder Session-Login via `/tenant-auth/exchange`.
|
||||
|
||||
### Onboarding Journey
|
||||
- Routen `/event-admin/welcome/*` bilden den Flow.
|
||||
- Filament stellt einen korrespondierenden Onboarding-Wizard (QR/Join-Token, Layout-Download) bereit; Abschluss setzt `onboarding_completed_at` serverseitig.
|
||||
- `useOnboardingProgress` persistiert Fortschritt (localStorage) und synchronisiert mit Backend (`onboarding_completed_at`).
|
||||
- Paketwahl nutzt `GET /tenant/packages`; Paddle-Fallbacks informieren bei fehlender Konfiguration.
|
||||
- Dashboard weist per CTA auf offenes Onboarding hin, bis ein erstes Event erstellt wurde.
|
||||
|
||||
### Event Lifecycle
|
||||
- Erstellung prüft Paketverfügbarkeit; generiert Join-Token (EventJoinToken-Service).
|
||||
- QR-Layouts und Token-Rotation erfolgen über `/event-admin/welcome` bzw. das Filament-Panel; slug-basierte QR-Links wurden deaktiviert.
|
||||
- Bearbeiten erlaubt Statuswechsel, Aufgaben, Emotions, Join-Token-Verwaltung.
|
||||
- Veröffentlichen schaltet Guest-PWA frei; Archivieren respektiert Retention-Policy.
|
||||
|
||||
### Medien & Moderation
|
||||
- Direktupload via signed URLs, Thumbnail-Generierung serverseitig.
|
||||
- Moderations-Grid mit Bulk-Aktionen, Filter (Neu, Genehmigt, Featured).
|
||||
- Analytics: Likes, Uploadzahlen, aktive Gäste.
|
||||
|
||||
### Tasks & Emotions
|
||||
- Globale + Tenant-spezifische Bibliothek.
|
||||
- Drag-and-Drop Zuweisung, Fortschritt je Event, Emotion-Tagging.
|
||||
|
||||
### Billing & Checkout
|
||||
- Pakete + Credit-Balance anzeigen.
|
||||
- Stripe PaymentIntent & Paddle Smart Buttons; Fallback-Meldung bei fehlender Konfiguration.
|
||||
- Ledger mit Historie (Paginierung, Filter).
|
||||
|
||||
### Settings
|
||||
- Branding (Logo, Farben), Domain/Links, Legal Pages.
|
||||
- Notification Preferences, Paketlimits, Onboarding-Reset.
|
||||
|
||||
### Offline & Sync
|
||||
- Service Worker `public/admin-sw.js` cached App-Shell `/event-admin` und statische Assets, liefert Offline-Fallback für Navigation.
|
||||
- Mutationen werden gequeued und nach Reconnect synchronisiert.
|
||||
- ETag / If-Match für konfliktfreie Updates, Optimistic UI mit Rollback.
|
||||
|
||||
### Fehlerbehandlung & UX
|
||||
- Rate-Limit (429) → Retry-Hinweis.
|
||||
- Offline-Banner + Retry-Buttons an kritischen Stellen (Checkout, Upload).
|
||||
- i18n via `react-i18next` (de/en); Strings in `public/lang/{locale}/admin.json`.
|
||||
|
||||
## API-Integration
|
||||
Die App nutzt Endpunkte aus `docs/prp/03-api.md`.
|
||||
|
||||
| Bereich | Endpunkte |
|
||||
| --- | --- |
|
||||
| Auth | `POST /tenant-auth/login`, `POST /tenant-auth/exchange`, `POST /tenant-auth/logout` |
|
||||
| Onboarding | `GET /tenant/me` (Progress Flags), `GET /tenant/packages`, `POST /tenant/events` |
|
||||
| Events | `GET/POST/PATCH/DELETE /tenant/events`, `POST /tenant/events/{event}/toggle`, Join-Token Routen |
|
||||
| Medien | `GET /tenant/events/{event}/photos`, `POST /tenant/events/{event}/photos`, `PATCH /tenant/photos/{id}` |
|
||||
| Tasks & Emotions | `GET /tenant/tasks`, `POST /tenant/events/{event}/tasks`, `GET /tenant/emotions` |
|
||||
| Settings | `GET/PATCH /tenant/settings`, `GET /tenant/credits/balance`, `POST /tenant/purchases/intent` |
|
||||
|
||||
## Nicht-funktionale Anforderungen
|
||||
- **Performance**: Ladezeit < 2s; Code-Splitting der Onboarding-Screens.
|
||||
- **Sicherheit**: Keine sensiblen Logs; CSRF abgesichert via Sanctum stateful middleware; Token-Neuausstellung durch erneutes Login oder Exchange.
|
||||
- **Accessibility**: Tastaturbedienung, Fokus-Indikatoren, `prefers-reduced-motion`.
|
||||
- **Internationalisierung**: Sprachumschaltung in Einstellungen; Standard de, Fallback en.
|
||||
|
||||
## Teststrategie
|
||||
- **PHPUnit**: Feature-Tests für Auth-Guards (Tenant ohne Events → Welcome Flow).
|
||||
- **React Testing Library**: `TenantWelcomeLayout`, `PackageSelection`, `OnboardingGuard`, `OrderSummary`.
|
||||
- **Playwright**: `tests/e2e/tenant-onboarding-flow.test.ts` deckt Login, Welcome → Packages → Summary → Event Setup ab; Erweiterung um Paddle Happy Paths und Offline/Retry geplant.
|
||||
- **Smoke Tests**: `npm run test:e2e` in CI mit optionalen Credentials (`E2E_TENANT_EMAIL`, `E2E_TENANT_PASSWORD`, Paddle Keys).
|
||||
|
||||
Für UI-Details siehe `docs/prp/tenant-app-specs/pages-ui.md`. Einstellungen werden in `docs/prp/tenant-app-specs/settings-config.md` beschrieben.
|
||||
9
docs/archive/prp/tenant-app-specs/pages-ui-legacy.md
Normal file
9
docs/archive/prp/tenant-app-specs/pages-ui-legacy.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Legacy-UI-Referenz (Framework7, Stand 2025-09-13)
|
||||
|
||||
Die ursprüngliche Dokumentation zur Framework7-basierten Tenant-Admin-App wurde im Commit-Verlauf abgelegt (`pages-ui.md` bis einschließlich 2025-09-13).
|
||||
|
||||
Sie enthält:
|
||||
- Wireframes für Login, Dashboard, Events, Fotos, Members, Tasks, Settings, Billing.
|
||||
- Hinweise zur Nutzung von Framework7-Komponenten (Toolbar, List, Card, Pull-to-Refresh, Infinite Scroll).
|
||||
|
||||
Für Migrations- oder Vergleichszwecke kann die Version über `git show <commit>:docs/prp/tenant-app-specs/pages-ui.md` abgerufen werden. Die aktuelle Implementierung basiert dagegen auf React + Tailwind (siehe `pages-ui.md`).
|
||||
68
docs/archive/prp/tenant-app-specs/pages-ui.md
Normal file
68
docs/archive/prp/tenant-app-specs/pages-ui.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Seiten- und UI-Design für die Tenant-Admin-App
|
||||
|
||||
## Status
|
||||
- **Version**: 1.1.0 (Stand 2025-10-13)
|
||||
- **Technologie**: React 19, TailwindCSS (shadcn/ui), React Router 7, React Query 5.
|
||||
- **Hinweis**: Die Framework7-Wireframes aus 2025-09 bleiben als historische Referenz erhalten und sind im Anhang dokumentiert.
|
||||
|
||||
## Design-Grundlagen
|
||||
- **Design Tokens**: Farbverlauf `#f43f5e → #6366f1`, Slate-Neutrals für Typografie, Primärschrift `Clash Display`, Fließtext `Inter`.
|
||||
- **Komponentensystem**: shadcn/ui-Basis (Button, Card, Tabs, Sheet, Dialog). Erweiterungen unter `resources/js/admin/components`.
|
||||
- **Navigation**: Obere App-Bar mit Breadcrumb & Quick Actions, mobile Tabbar (Dashboard, Events, Tasks, Einstellungen).
|
||||
- **Responsiveness**: Breakpoints `sm` (375px), `md` (768px), `xl` (1280px). Onboarding-Screens nutzen Full-Height Layouts auf Mobile, Split-Grid auf Desktop.
|
||||
- **Accessibility**: `prefers-reduced-motion`, Fokus-Ringe (Tailwind Plugin), ARIA für Carousel, Tabs, Dialoge.
|
||||
- **Offline UX**: Banner oben rechts (`Offline – deine Änderungen werden synchronisiert, sobald du wieder online bist`) + Retry-CTA.
|
||||
|
||||
## Geführtes Onboarding (Welcome Flow)
|
||||
| Schritt | Route | Komponenten | Besondere Elemente |
|
||||
| --- | --- | --- | --- |
|
||||
| Hero | `/event-admin/welcome` | `TenantWelcomeLayout`, `WelcomeStepCard`, `EmblaCarousel` | CTA „Pakete entdecken“, sekundärer Link „Später entscheiden“ |
|
||||
| How It Works | `/event-admin/welcome` (Carousel Slide) | Icon Cards, Animated Gradients | 3 Vorteile (Fotos festhalten, Aufgaben, Gäste aktivieren) |
|
||||
| Paketwahl | `/event-admin/welcome/packages` | `PackageCard`, `PricingToggle`, `QueryPackageList` | Paddle Pricing, Feature-Badges, Auswahl persistiert im Onboarding-Context |
|
||||
| Zusammenfassung | `/event-admin/welcome/summary` | `OrderSummaryCard`, Stripe Elements, Paddle Buttons | Hinweise bei fehlender Zahlungs-Konfiguration, CTA „Weiter zum Setup“ |
|
||||
| Event Setup | `/event-admin/welcome/event` | `FirstEventForm`, `FormStepper`, Toasts | Formular (Name, Datum, Sprache, Feature-Toggles) + Abschluss CTA „Event erstellen“ |
|
||||
|
||||
### Guards & Fortschritt
|
||||
- `useOnboardingProgress` (Context + localStorage) speichert `activeStep`, ausgewähltes Paket und Event-Entwurf.
|
||||
- Auth-Guard leitet Tenant ohne Events auf `/event-admin/welcome` um; nach Abschluss setzt Backend `onboarding_completed_at`.
|
||||
- Dashboard hero banner zeigt CTA „Geführtes Setup fortsetzen“, solange `onboarding_completed_at` fehlt.
|
||||
- Offlinezustand: Payment-Sektion zeigt Fallback-Karte „Zahlungsdienste offline – bitte erneut versuchen“.
|
||||
|
||||
### Assets & PWA
|
||||
- Manifest: `public/manifest.json` (Scope `/event-admin/`, Theme-Farbe `#f43f5e`, Shortcuts für Welcome & Dashboard).
|
||||
- Service Worker: `public/admin-sw.js` cached Shell `/event-admin` + statische Assets; Navigation requests → Netzwerk-First mit Cache-Fallback.
|
||||
- Registrierung: erfolgt in `resources/js/admin/main.tsx` beim `window.load` Event.
|
||||
|
||||
## Kernseiten nach dem Onboarding
|
||||
- **Dashboard**: Hero-CTA zum Welcome Flow, Statistik-Kacheln (Events aktiv, Uploads, Credits), Quick Actions (Event anlegen, Fotos moderieren, Tasks verwalten).
|
||||
- **Events**: Suchfeld + Filter Pills, Card-Layout mit Status-Badges (`Draft`, `Live`, `Archived`), Detail-Drawer mit Quick Stats.
|
||||
- **Fotos**: Moderationsgrid (Masonry), Filter (Neu, Genehmigt, Featured), Bulk-Aktionen in Sticky-Footer.
|
||||
- **Tasks**: Tabs (Bibliothek, Zuweisungen), Drag-and-Drop (React Beautiful DnD), Inline-Editor für Aufgaben.
|
||||
- **Einstellungen**: Accordion-Struktur (Branding, Legal Pages, Benachrichtigungen, Abrechnung). Preview-Panel für Farben und Logos.
|
||||
- **Abrechnung**: Kreditübersicht, Kauflog (infinite-scroll), Zahlungsoptionen (Stripe Karte, Paddle Checkout).
|
||||
|
||||
## Informationsarchitektur (aktuelle React-Router-Konfiguration)
|
||||
```
|
||||
/event-admin
|
||||
├── dashboard
|
||||
├── events
|
||||
│ ├── :slug (Detailseiten, Tabs: Overview, Tasks, Media, Members, Stats)
|
||||
│ └── :slug/edit
|
||||
├── tasks
|
||||
├── members
|
||||
├── settings
|
||||
├── billing
|
||||
└── welcome
|
||||
├── (index) # Hero + How It Works Carousel
|
||||
├── packages
|
||||
├── summary
|
||||
└── event
|
||||
```
|
||||
|
||||
## Testabdeckung (UI)
|
||||
- **Jest/RTL**: `TenantWelcomeLayout`, `WelcomeStepCard`, `PackageSelection`, `OnboardingGuard`.
|
||||
- **Playwright**: `tests/e2e/tenant-onboarding-flow.test.ts` (Login Guard, Welcome → Packages → Summary → Event Setup). Erweiterbar um Paddle-Happy-Path sowie Offline-/Retry-Szenarien.
|
||||
|
||||
## Legacy-Referenz (Framework7 Entwurf 2025-09)
|
||||
Die ursprünglichen Wireframes für Framework7 (Toolbar, FAB, Infinite Scroll) sind weiterhin im Repo historisiert (`docs/prp/tenant-app-specs/pages-ui-legacy.md`). Für Vergleiche bei Regressionen oder Migrationen bitte dort nachsehen.
|
||||
|
||||
124
docs/archive/prp/tenant-app-specs/settings-config.md
Normal file
124
docs/archive/prp/tenant-app-specs/settings-config.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Settings und Konfiguration für die Tenant Admin App
|
||||
|
||||
## Status
|
||||
- **Version**: 1.0.0 (2025-09-13)
|
||||
- **Fokus**: App-interne und tenant-spezifische Einstellungen; Integration mit Capacitor-Plugins.
|
||||
|
||||
## App-interne Settings
|
||||
Diese Settings werden lokal in der App gespeichert (via Capacitor Preferences oder IndexedDB) und beeinflussen das Verhalten der App:
|
||||
|
||||
### Core App Settings
|
||||
- **language**: String (default: 'de') – UI-Sprache; Sync mit User-Profile und i18next (react-i18next); Fallback 'en'; URL-Prefix (/de/, /en/) für persistente Wechsel.
|
||||
- **themeMode**: String ('system' | 'light' | 'dark') – Dark Mode-Präferenz; Framework7-Theming.
|
||||
- **offlineMode**: Boolean (default: true) – Aktiviert Offline-Caching und Background-Sync.
|
||||
- **pushNotifications**: Boolean (default: true) – Erlaubt Push-Registrierung.
|
||||
- **autoSyncInterval**: Number (default: 300) – Sekunden bis automatischer Sync (min: 60).
|
||||
- **maxPhotoCache**: Number (default: 100) – Anzahl gecachter Photos pro Event.
|
||||
|
||||
### User-spezifische Settings
|
||||
- **notificationPreferences**: Object
|
||||
- `newPhotos`: Boolean – Benachrichtigung bei neuen Uploads.
|
||||
- `eventUpdates`: Boolean – Bei Event-Änderungen (z.B. Veröffentlicht).
|
||||
- `lowCredits`: Boolean – Warnung bei < 2 Credits.
|
||||
- **privacySettings**: Object
|
||||
- `shareAnalytics`: Boolean (default: false) – Anonyme Stats teilen.
|
||||
- `dataRetention`: Number (default: 30) – Tage bis Auto-Delete von Cache.
|
||||
|
||||
**Speicherung**: Capacitor Preferences API (`@capacitor/preferences`) für sichere, persistente Speicherung. Bei Web-Fallback: localStorage mit Encryption (via Crypto API).
|
||||
|
||||
## Tenant-spezifische Optionen
|
||||
Diese werden über API (`GET/PATCH /tenant/settings`) geladen und überschreiben globale Defaults. Sie definieren das Verhalten für den Tenant und seine Events:
|
||||
|
||||
### Theme & Branding
|
||||
- **primaryColor**: String (default: '#007AFF') – Hauptfarbe (iOS-Blau); verwendet in Framework7 CSS-Vars (`--f7-primary`).
|
||||
- **secondaryColor**: String (default: '#5856D6') – Sekundärfarbe für Buttons/Accents.
|
||||
- **logoUrl**: String – Custom Logo für App-Banner (URL zu S3/CDN).
|
||||
- **tenantName**: String – Anzeigename (z.B. "Müller Hochzeit"); in Dashboard-Banner.
|
||||
|
||||
### Event Limits & Features
|
||||
- **maxEventsPerMonth**: Number (default: 5) – Limit pro Monat (enforced via Credits).
|
||||
- **maxPhotosPerEvent**: Number (default: 1000) – Soft-Limit; Warnung bei Überschreitung.
|
||||
- **enableTasks**: Boolean (default: true) – Tasks-System aktivieren.
|
||||
- **enableEmotions**: Boolean (default: true) – Emotions/Reactions erlauben.
|
||||
- **autoApprovePhotos**: Boolean (default: false) – Neue Uploads sofort freigeben (Risiko: Spam).
|
||||
|
||||
### Legal & Compliance
|
||||
- **legalPages**: Object (von Backend geladen, siehe docs/prp/02-tenancy.md)
|
||||
- `impressumUrl`: String – Pflicht in DE; Customizable Link.
|
||||
- `privacyUrl`: String – Datenschutzerklärung.
|
||||
- `agbUrl`: String – Allgemeine Geschäftsbedingungen.
|
||||
- **gdprRetentionDays**: Number (default: 30) – Automatische Löschung alter Photos/Events.
|
||||
- **contactEmail**: String – Support-Email für In-App-Feedback.
|
||||
|
||||
### Notifications & Integration
|
||||
- **pushEnabled**: Boolean – Tenant-weit Push aktivieren (erfordert Plugin-Registrierung).
|
||||
- **stripePublicKey**: String – Für Client-side Checkout (aus Env, nicht persistent).
|
||||
- **eventJoinDomain**: String (default: 'events.fotospiel.app') – Custom Domain für Guest-PWA.
|
||||
|
||||
**API-Handling**: Laden bei Login; Cache mit ETag; Update triggert UI-Refresh (z.B. Theme-Wechsel). Validation: Backend enforct Limits (z.B. Colors als HEX).
|
||||
|
||||
## Capacitor-Plugins
|
||||
Die App integriert folgende Plugins für native Features. Installation via `npx cap add` und Sync nach Build.
|
||||
|
||||
### Essentielle Plugins
|
||||
1. **@capacitor/preferences** (v6+)
|
||||
- **Zweck**: Sichere Speicherung von App- und User-Settings.
|
||||
- **Usage**: `Preferences.set({ key: 'themeMode', value: 'dark' })`; Migrate von localStorage.
|
||||
- **iOS/Android**: Keychain/Keystore für Encryption.
|
||||
|
||||
2. **@capacitor/push-notifications** (v6+)
|
||||
- **Zweck**: Native Push für neue Photos, Event-Updates, Low-Credits.
|
||||
- **Setup**: Registrierung bei App-Start (`PushNotifications.register()`); Token an Backend senden (`POST /tenant/devices`).
|
||||
- **Events**: `registrationToken` (senden), `pushNotificationReceived` (In-App-Handling), `pushNotificationActionPerformed` (Tap-Actions).
|
||||
- **Permissions**: Request bei erstem Login; Fallback zu Web Push in PWA/TWA.
|
||||
|
||||
3. **@capacitor/storage** (v6+)
|
||||
- **Zweck**: Offline-Caching von Events/Photos (als Alternative zu IndexedDB).
|
||||
- **Usage**: `Storage.set({ key: 'event-123', value: JSON.stringify(data) })`; Size-Limit beachten (50MB).
|
||||
- **Fallback**: Für Web; Sync mit Background-Sync.
|
||||
|
||||
4. **@capacitor/camera** (v6+)
|
||||
- **Zweck**: Direkte Kamera-Zugriff für Event-QR-Scans oder schnelle Photo-Uploads (Admin-Selfies).
|
||||
- **Usage**: `Camera.getPhoto({ quality: 90, allowEditing: true })`; Upload via signed URL.
|
||||
- **Permissions**: Camera/Mic; Privacy-Manifest für App Store.
|
||||
|
||||
5. **@capacitor/network** (v6+)
|
||||
- **Zweck**: Connectivity-Status überwachen; Offline-Modus triggern.
|
||||
- **Usage**: `Network.addListener('networkStatusChange', handleOffline)`; UI-Update bei 'offline'.
|
||||
- **Integration**: Deaktiviere Sync-Buttons; Zeige Cached-Data.
|
||||
|
||||
### Optionale Plugins
|
||||
- **@capacitor/haptics** (v6+): Vibration-Feedback bei Actions (z.B. Photo-Approve).
|
||||
- **@capacitor/share** (v6+): Teilen von Event-QR-Codes via Native Share-Sheet.
|
||||
- **@capacitor/device** (v6+): Geräte-Info (Model, OS) für Analytics; Token zu Backend.
|
||||
|
||||
### Plugin-Konfiguration (capacitor.config.ts)
|
||||
```typescript
|
||||
import { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.fotospiel.tenantadmin',
|
||||
appName: 'Event Photo Admin',
|
||||
webDir: 'dist',
|
||||
bundledWebRuntime: false,
|
||||
plugins: {
|
||||
PushNotifications: {
|
||||
presentationOptions: ["badge", "sound", "alert"]
|
||||
},
|
||||
Camera: {
|
||||
permissions: {
|
||||
camera: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Security & Privacy
|
||||
- **Plugin-Permissions**: Explizite Requests; Erkläre Zweck im Onboarding (z.B. "Push für neue Photos").
|
||||
- **Data Storage**: Kein PII in Preferences; Tokens encrypted (siehe Auth-Specs).
|
||||
- **App Store Compliance**: Privacy Manifest mit Usage-Descriptions (z.B. NSPhotoLibraryUsageDescription).
|
||||
|
||||
Für detailliertes Packaging siehe capacitor-setup.md; UI-Integration in pages-ui.md.
|
||||
Reference in New Issue
Block a user