Earnings — coach-side of money. Hero shows Available (withdrawable) and Pending (released 24h after each session).
Weekly auto-payout only (Monday sweep). Coach does nothing, money lands. No manual withdraw in v1 — removes fee complexity and keeps cashflow consolidation on our side.
Future paid add-on: 1% fee charged to coach, money arrives within 30 min via Stripe Instant Payouts. Previewed in the + Instant Payout state so devs can see the intended UI and wire backend support, but not shipped in v1. Decision on monetization (per-transfer fee vs subscription bundle) deferred.
Funds enter Pending when the session is marked complete. After 24h (no refund/no-show), they move to Available. This window protects us against corrections.
Auto-payout fires only when Available ≥ €20. Below threshold — rolls to next week.
MVP: hero with Next payout only, no Withdraw pill.
+ Instant Payout: Withdraw now pill + Premium tag + notice banner explaining this is post-MVP.
Zero: neutral hero, no Withdraw, empty-state copy.
Needs payout method: hero dimmed, lock plate with CTA. Pending earnings listed but not withdrawable.
Stripe preview card reflects Stripe account status inline — "Connected · Default" (teal dot) normally, or "Action required · Tap to resolve" (yellow dot) when requirements.currently_due is non-empty. No separate banner on Earnings — the card itself is the signal, and tapping pushes to Stripe detail where the full warn-banner lives.
Tap on the Pending hero label (or its info icon) opens a bottom sheet explaining the 24-hour hold window + visual timeline (session complete → +24h available → Monday payout). Addresses the "why is my money stuck?" support ticket pre-emptively.
Earnings screen shows 4 most recent transactions. Devs: GET /coach/transactions/?limit=4. "View all ›" pushes to full Transactions screen which has its own pagination.
GET /coach/balance/ — { available, pending, currency, next_payout_at }GET /coach/transactions/?limit=4 — preview for Earnings screenGET /coach/transactions/?cursor=... — full ledger with paginationPOST /coach/payout/instant/ — triggers Stripe Instant Payout (post-MVP)
Currently coach balance is computed on-the-fly from completed events. Needs real coach_balance table + append-only coach_transactions ledger. See project_coach_balance_decisions.md.
Full chronological ledger: earnings, payouts, refunds. Grouped by month. Filter chips at top.
Chips: All / Earnings / Payouts / Refunds. Single-select. Default = All.
Infinite scroll by month. First fetch = current + previous month. Scrolling to bottom loads older months.
Shown when /coach/transactions/ returns 0 items AND filter = All. With filter active, empty copy should adapt: "No payouts yet", etc.
Skeleton rows during first fetch. For infinite scroll — inline spinner at the bottom of the list (not skeleton).
Drill-down on a single earning. Amount up top with status pill (Pending / Available / Paid out).
Status pill: Pending (gray) → Available (teal) → Paid out (blue). When included in a payout batch, shows link to that payout.
Batched payout — shows all included earnings. Amount neutral (not minus red; payout isn't "bad").
For Instant payouts: fee row shows 1% deduction. For weekly batch — fee = €0.
Managing connected payout providers. Default receives weekly batch (and future Instant) payouts. Others are backup.
Coach has no payout accounts connected yet — reached from Earnings Needs payout method CTA. Each provider shows inline "Connect" action. Tapping Connect on Stripe launches the StripeConnect embedded component (native iOS SDK, not WebView) — form lives in-app, validation + doc upload + selfie verification all native.
After Stripe webhook fires account.updated with charges_enabled & payouts_enabled → backend creates PayoutAccount with is_default=true → user returns to Single (MVP) state. If webhook reports requirements.currently_due non-empty → Stripe screen shows Action required.
Only Stripe connected, no radio (default is implicit), ⋯ menu has only Disconnect. Revolut is a "Coming soon" placeholder to show provider abstraction intent.
2+ methods connected. Each card has an iOS-style radio on the left. Tapping empty radio = instant switch (toast "X is now your default"). Tapping filled radio does nothing. "Set as default" in ⋯ menu is hidden when the method is already default.
Backup exists: soft confirm sheet — "Your next payout will go to {fallback}". Destructive red CTA.
Only method: warning sheet — yellow triangle + "Payouts will be paused until you add another method — your earnings will stay on hold". Explicit tradeoff. User can still proceed (not blocked). On confirm → falls to Needs-payout-method state on Earnings screen.
GET /coach/payout-methods/ — list (returns {} for empty state)POST /coach/payout-methods/stripe/session/ — returns {client_secret} for embedded onboarding componentPATCH /coach/payout-methods/{id}/ {is_default: true} — switch defaultDELETE /coach/payout-methods/{id}/ — disconnect (backend must re-route default to next available; pause payouts if none left)
Per-provider detail screen. Four states reflect the Stripe Connect account lifecycle.
iOS native SDK: uses StripeConnect module — not WebView. Nativecontroller rendered in-app. Backend needs to expose account_session client_secret.
StripeAPI.shared.publishableKey = ...let mgr = EmbeddedComponentManager(fetchClientSecret: ...)let vc = mgr.createAccountOnboarding(...)
POST /coach/stripe/account-session/ — returns { client_secret } for embedded components. Replaces current /coach/stripe-onboarding redirect URL.
Stripe webhooks → update PayoutAccount.status: none → pending → done. Action-required triggered by requirements.currently_due becoming non-empty. Earnings screen's Stripe preview card mirrors this automatically.
Coach can reach Disconnect either via ⋯ menu on the method card in Payout methods list, OR via the destructive button on the Connected state of this Stripe detail. Both open the same warning sheet — backup-variant if other methods exist, only-variant if Stripe is the single connected method.