// Releases > send for review: the owner's buttons, each a plan first (ui/OwnerAction.tsx), then a native dialog, then // the host-minted token. App Store: the submission state from `apple status` (waiting for / in review, back from App // Review with UNRESOLVED_ISSUES and its items, empty drafts labelled harmless), the uploaded builds to pick by number // (never "the latest"), Submit for review (`apple submit --build n`), Resubmit after a rejection (`apple resubmit // --build n [--resolve-iaps]`) and in-app purchases to review (`apple iap-submit [--with-version]`). Google Play: the // versionCodes on Play to pick by number and Send to production (`play release --version-code n --production ...`), // the managed-publishing warning, and the console steps Play has no API for (production countries, "Send changes for // review" in Publishing overview) as console plans with a deep link. // v2: each store is a numbered checklist (ready / blocked / to do per step) that ends in its one primary button. import { type ReactNode, useState } from 'react'; import { api, type Project } from '../../api'; import { type Key, t } from '../../i18n'; import { ago, date } from '../../lib/format'; import { type AppleStatusData, appleSubmitState, platformFlag, platformLabel, type PlayTrackData, playSubmitState, type SubmissionKind } from '../../lib/submit'; import type { UnifiedApp } from '../../lib/identity'; import { useData, useRead } from '../../store'; import { ConsolePlanButton } from '../../ui/ConsolePlan'; import { Icon, StoreGlyph } from '../../ui/icons'; import { Chip, ErrorNotice, Notice, ReadControl, Seg, Skeleton } from '../../ui/kit'; import { EmptyState, StatusPill } from '../../ui/ds'; import { OwnerAction } from '../../ui/OwnerAction'; import { ascUrl } from '../AppView'; import { playUrlFor } from '../../ui/app-menu'; import '../../styles/submit.css'; const SUB_KIND: Record = { waiting: ['info', 'sub.kWaiting'], inReview: ['info', 'sub.kInReview'], unresolved: ['bad', 'sub.kUnresolved'], draft: ['neutral', 'sub.kDraft'], emptyDraft: ['neutral', 'sub.kEmptyDraft'], done: ['ok', 'sub.kDone'], other: ['neutral', 'sub.kOther'], }; type StepState = 'ready' | 'blocked' | 'todo' | 'warn'; const STEP_PILL: Record = { ready: ['ok', 'v2rel.ready'], blocked: ['bad', 'v2rel.blocked'], todo: ['neutral', 'v2rel.todo'], warn: ['warn', 'v2rel.attention'], }; /** One numbered step of a send-for-review checklist: node (number, check or alert), title, state pill, body. */ function Step({ n, state: st, title, children }: { n: number; state: StepState | undefined; title: string; children?: ReactNode }) { const state = st ?? 'todo'; const [tone, key] = STEP_PILL[state]; return (
  • {title}{t(key)}
    {children}
  • ); } /** The head of a store's send card: store mark, title, read state line, ready count, read / console buttons. */ function SubHead({ store, title, sub, actions, done, total }: { store: 'ios' | 'android'; title: string; sub: ReactNode; actions: ReactNode; done: number | null; total: number }) { return (

    {title}

    {sub}
    {done !== null && ( {t('v2rel.readyCount', { done, total })} )}
    {actions}
    ); } /** The App Store card: submission state, build picker, Submit / Resubmit / in-app purchases. */ export function AppleSubmitCard({ app, project }: { app: UnifiedApp; project: Project }) { const d = useData(); const r = useRead(project.id, 'apple.status', { network: true, title: t('sub.appleRead'), appName: app.name }); const s = r.data ? appleSubmitState(r.data) : null; const [build, setBuild] = useState(''); const [resolveIaps, setResolveIaps] = useState(false); const [withVersion, setWithVersion] = useState(false); const summary = d.statusOf(project)?.summary; const iaps = summary?.catalog.ios.length ?? 0; const picked = s?.builds.find(b => b.number === build && b.valid) ?? null; const version = s?.unresolved ? s.versions.find(v => s.unresolved!.items.some(i => i.label === `app version ${v.version} (${platformLabel(v.platform)})`)) ?? s.editable : s?.editable ?? null; const rejectedIap = s?.unresolved?.items.some(i => i.rejected && i.kind !== 'appStoreVersion') ?? false; const args = { build, ...(version ? { version: version.version, platform: platformFlag(version.platform) } : {}), ...(resolveIaps ? { resolve_iaps: true } : {}) }; const refresh = () => { r.refresh(); }; const open = s ? s.submissions.filter(x => x.kind !== 'done') : []; const states: StepState[] = s ? [ 'ready', s.busy ? 'blocked' : s.unresolved ? 'warn' : 'ready', picked ? 'ready' : s.builds.length ? 'todo' : 'blocked', version ? 'ready' : 'blocked', picked && version && !s.busy ? 'ready' : 'todo', ] : []; const done = s ? states.filter(x => x === 'ready').length : null; return (
    refresh() }} label={t('sub.read')} /> } />
    {r.error && } {!s ? (r.loading && r.data === null && !r.error ? : {t('sub.notRead')} {t('sub.notReadText')}) : (
      {open.length > 0 ? (
      {open.slice(0, 6).map(x => { const [tone, key] = SUB_KIND[x.kind]; return (
      {t(key)} {x.id} · {x.state}{x.date ? ` · ${date(x.date)}` : ''}{x.kind === 'emptyDraft' ? ` · ${t('sub.harmless')}` : ''} {x.items.length > 0 && (
        {x.items.map(i => (
      • {i.label}: {i.state}{i.rejected ? ` · ${i.kind === 'appStoreVersion' ? t('sub.markVersion') : t('sub.markIap')}` : ''}
      • ))}
      )}
      {x.state}
      ); })}
      ) : {t('sub.noOpen')}}
      {s.busy && {t('sub.busy', { state: s.busy.state })}} {!s.busy && s.unresolved && {t('sub.unresolvedText')}} {!s.busy && ( <> {s.builds.length ? t('sub.buildHint') : t('sub.noBuilds')}
      {t('sub.version')} {version ? <>{version.version}{platformLabel(version.platform)} · {version.state} : {t('sub.noVersion')}}
      {s.unresolved ? ( <>
      ) : (
      )}
      )}
    )} {s && !s.busy && iaps > 0 && (
    {t('sub.iapTitle', { count: iaps })}
    {t('sub.iapText')}
    )}
    ); } const SHARES = [1, 2, 5, 10, 20, 50]; /** The Google Play card: versionCode picker, Send to production, managed publishing, the console steps without an API. */ export function PlayProductionCard({ app, project }: { app: UnifiedApp; project: Project }) { const d = useData(); const r = useRead<{ tracks?: PlayTrackData[] }>(project.id, 'play.status', { network: true, title: t('sub.playRead'), appName: app.name }); const s = r.data ? playSubmitState(r.data) : null; const pr = d.statusOf(project)?.summary?.playRelease ?? null; const [code, setCode] = useState(''); const [status, setStatus] = useState<'completed' | 'inProgress' | 'draft'>('completed'); const [share, setShare] = useState('10'); const picked = s?.codes.find(c => c.code === code) ?? null; const args = { version_code: code, status, ...(status === 'inProgress' ? { percent: Number(share) } : {}) }; const managed = pr && pr.managedPublishing !== true ? {pr.managedPublishing === false ? t('sub.managedOff') : t('sub.managedUnset')} : pr && pr.managedPublishing === true ? {t('sub.managedOn')} : null; const states: StepState[] = s ? [ pr && pr.managedPublishing !== true ? 'warn' : 'ready', picked ? 'ready' : s.codes.length ? 'todo' : 'blocked', 'ready', picked ? 'ready' : 'todo', ] : []; const done = s ? states.filter(x => x === 'ready').length : null; return (
    } />
    {r.error && } {!s ? ( <> {managed} {r.loading && r.data === null && !r.error ? : {t('sub.notRead')} {t('sub.playNotReadText')}} ) : (
      {managed} {s.production.length > 0 && ( {t('sub.prodNow')}: {s.production.map(p => `${p.name ?? p.codes.join(', ')} · ${p.status}${p.fraction !== null ? ` · ${Math.round(p.fraction * 100)} %` : ''}`).join('; ')} )} {s.codes.length ? t('sub.codeHint') : t('sub.noCodes')}
      {status === 'inProgress' && ( )}
      { r.refresh(); }} />
    )}
    {t('sub.consoleStepTitle')}
    {t('sub.consoleStepText', { countries: pr?.countries ? t('sub.countriesN', { count: pr.countries }) : t('sub.countriesAll') })}
    ); }