// 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. import { 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, 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 { Card, Chip, Empty, ErrorNotice, Notice, ReadControl, Seg, Skeleton } from '../../ui/kit'; import { OwnerAction } from '../../ui/OwnerAction'; import { ascUrl, PLAY_CONSOLE } from '../AppView'; 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'], }; /** 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}`)) ?? 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 } : {}), ...(resolveIaps ? { resolve_iaps: true } : {}) }; const refresh = () => { r.refresh(); }; return ( {r.data ? (r.fromCache ? t('common.cachedFrom', { when: ago(r.at) }) : t('common.readWhen', { when: ago(r.at) })) : r.loading ? t('read.waiting') : t('sub.appleSub')}} actions={<> refresh() }} label={t('sub.read')} /> }> {r.error && } {!s ? (r.loading && r.data === null && !r.error ? : {t('sub.notReadText')}) : (
{s.submissions.length > 0 && (
{s.submissions.filter(x => x.kind !== 'done').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}
); })}
)} {!s.submissions.some(x => x.kind !== 'done') && {t('sub.noOpen')}} {s.busy && {t('sub.busy', { state: s.busy.state })}} {!s.busy && ( <>
{t('sub.version')} {version ? `${version.version} · ${version.state}` : {t('sub.noVersion')}}
{s.builds.length ? t('sub.buildHint') : t('sub.noBuilds')} {s.unresolved ? ( <> {t('sub.unresolvedText')}
) : (
)} {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) } : {}) }; return ( {r.data ? (r.fromCache ? t('common.cachedFrom', { when: ago(r.at) }) : t('common.readWhen', { when: ago(r.at) })) : r.loading ? t('read.waiting') : t('sub.playSub')}} actions={<> }> {r.error && }
{pr && pr.managedPublishing !== true && {pr.managedPublishing === false ? t('sub.managedOff') : t('sub.managedUnset')}} {pr && pr.managedPublishing === true && {t('sub.managedOn')}} {!s ? (r.loading && r.data === null && !r.error ? : {t('sub.playNotReadText')}) : ( <> {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('; ')} )}
{t('sub.as')}
{status === 'inProgress' && ( )}
{s.codes.length ? t('sub.codeHint') : t('sub.noCodes')}
{ r.refresh(); }} />
)}
}>
{t('sub.consoleStepTitle')} {t('sub.consoleStepText', { countries: pr?.countries ? t('sub.countriesN', { count: pr.countries }) : t('sub.countriesAll') })}
); }