// TestFlight: beta groups (internal / external, testers, builds, public link) and the recent builds with their // processing and beta review states. import { Fragment, useState } from 'react'; import type { UnifiedApp } from '../../lib/identity'; import { WriteButton } from '../../ui/WriteButton'; import { t } from '../../i18n'; import { date, num } from '../../lib/format'; import type { TestFlightStatus, TfBuild, TfGroup, Tone } from '../../lib/model'; import { useData, useRead, useWrite } from '../../store'; import { Icon } from '../../ui/icons'; import { Card, Chip, Empty, Notice, ReadControl, Skeleton } from '../../ui/kit'; /** Beta state word -> tone and a short label. */ export function betaState(s: string | null): { tone: Tone; text: string } { if (!s) return { tone: 'neutral', text: '–' }; if (/IN_BETA_TESTING|BETA_APPROVED|READY_FOR_BETA_TESTING|VALID/.test(s)) return { tone: 'ok', text: s.replace(/_/g, ' ').toLowerCase() }; if (/WAITING|IN_BETA_REVIEW|PROCESSING|READY_FOR_BETA_SUBMISSION/.test(s)) return { tone: 'warn', text: s.replace(/_/g, ' ').toLowerCase() }; if (/EXPIRED/.test(s)) return { tone: 'neutral', text: s.replace(/_/g, ' ').toLowerCase() }; if (/REJECTED|FAILED|INVALID|MISSING_EXPORT_COMPLIANCE/.test(s)) return { tone: 'bad', text: s.replace(/_/g, ' ').toLowerCase() }; return { tone: 'neutral', text: s.replace(/_/g, ' ').toLowerCase() }; } function State({ s }: { s: string | null }) { const b = betaState(s); return s ? {b.text} : –; } /** One build's "What to Test" and its internal groups: confirm-gated writes (dry run, dialog, apply). */ function BuildEditor({ b, groups, w, mode, onDone }: { b: TfBuild; groups: TfGroup[]; w: ReturnType; mode: 'what' | 'groups'; onDone: () => void }) { const [text, setText] = useState(''); const internal = groups.filter(g => g.internal); const [picked, setPicked] = useState(internal.map(g => g.name)); const base = { build: b.build ?? '', ...(b.version ? { version: b.version } : {}) }; if (mode === 'what') { return (