// One app across both stores: header (icon, studio, ids, store state, pairing), then five sections by job (Release, // Store page, Revenue, Players, Health; docs IA), each with a header (what it is for, the one next step) and its sub views. A store-only app (no // app folder) shows its store reads (releases, health, reviews, TestFlight) read-only, with "Link a folder" and // "Start an app folder here" to make it a full app. import { useContextMenu, useMenuButton } from '../ui/ContextMenu'; import { appMenu, playUrlFor } from '../ui/app-menu'; import { useShowIgnored } from '../lib/show-ignored'; import { useState } from 'react'; import { GamesTab } from './tabs/Games'; import { SectionHead } from './SectionHead'; import { api, errorText } from '../api'; import { type Key, t } from '../i18n'; import { pairCandidates, type UnifiedApp, withLink, withPair, withStudio } from '../lib/identity'; import { attention, storeNow } from '../lib/model'; import { Hero, nextStep, Tabs } from '../ui/ds'; import { isGameSummary } from '../lib/model'; import { type AppSection, type AppTab, SECTION_ICON, SECTION_KEY, SECTION_TABS, sectionOf, sectionsFor, subsOf, useNav } from '../nav'; import { useData } from '../store'; import { Icon } from '../ui/icons'; import { ErrorBoundary } from '../ui/ErrorBoundary'; import { FolderMissing } from '../ui/FolderMissing'; import { Chip, Notice, PhaseChip, StoreBadge, StoreOnlyTag, StudioMark } from '../ui/kit'; import { AppInfoTab } from './tabs/AppInfo'; import { ArtTab } from './tabs/Art'; import { LiveArt } from './tabs/LiveArt'; import { CatalogTab } from './tabs/Catalog'; import { ChecksTab } from './tabs/Checks'; import { HealthTab } from './tabs/Health'; import { GrowthTab } from './tabs/Growth'; import { ListingsTab } from './tabs/Listings'; import { PricesTab } from './tabs/Prices'; import { ReleasesTab } from './tabs/Releases'; import { ReviewsTab } from './tabs/Reviews'; import { TestFlightTab } from './tabs/TestFlight'; import { ToolsTab } from './tabs/Tools'; import { EDIT_TABS, TabEditor, useUnsavedGuard } from './editors/TabEditor'; import { StoreListingEditor } from './editors/StoreListingEditor'; import { useEdits } from '../ui/Editor'; export const ascUrl = (appId: string | null | undefined): string => (appId ? `https://appstoreconnect.apple.com/apps/${encodeURIComponent(appId)}/distribution` : 'https://appstoreconnect.apple.com/apps'); const PAIRING: Record = { folder: 'pair.folder', manual: 'pair.manual', id: 'pair.id', close: 'pair.close', name: 'pair.name', single: 'pair.single' }; /** Pairing, studio and ignore controls of one app. */ function AppControls({ app }: { app: UnifiedApp }) { const d = useData(); const nav = useNav(); const cands = pairCandidates(app, d.apps); const more = useMenuButton(); const [showIgnored] = useShowIgnored(); return (
{t(PAIRING[app.pairing])} {cands.length > 0 && ( )}
); } function StoreOnlyBanner({ app }: { app: UnifiedApp }) { const d = useData(); const [busy, setBusy] = useState(false); const [profile, setProfile] = useState<'app' | 'game' | 'subscription-app'>('app'); const link = async (how: 'add' | 'init' | 'fastlane') => { setBusy(true); try { const p = how === 'init' ? await api.initProject(profile) : how === 'fastlane' ? await api.importFastlane(profile) : await api.addProject(); if (p) { await d.updateOverrides(o => withLink(o, app, p.id)); await d.reload(); d.toast({ kind: 'ok', title: t('app.linked', { name: p.name }), detail: how === 'init' ? t('folders.startedDetail') : how === 'fastlane' ? t('folders.importedDetail') : undefined }); } } catch (e) { d.toast({ kind: 'bad', title: t('app.linkFailed'), detail: errorText(e) }); } finally { setBusy(false); } }; return ( }> {t('app.storeOnlyTitle')} {t('app.storeOnlyText')} ); } export function AppView({ app, tab }: { app: UnifiedApp; tab: AppTab }) { const d = useData(); const nav = useNav(); const e = d.statusFor(app); const l = d.liveFor(app); const ios = e?.app?.platforms.ios ?? (app.ios ? { bundleId: app.ios.app.bundleId, ascAppId: app.ios.app.id } : null); const android = e?.app?.platforms.android ?? (app.android ? { packageName: app.android.app.id } : null); const name = (app.project && e?.app?.name) || app.name; const game = isGameSummary(e?.summary); const sections = sectionsFor(!!app.project, game); const section = sectionOf(tab); const ed = useEdits(); useUnsavedGuard(); const studio = d.studios.find(s => s.id === app.studio) ?? null; const setTab = (x: AppTab) => nav.go({ screen: 'app', appId: app.key, tab: x }); const setSection = (x: AppSection) => setTab(subsOf(x, !!app.project, game)[0] ?? SECTION_TABS[x][0]!); const editCount = (x: AppTab) => (EDIT_TABS[x] ? ed.count(app.project?.id ?? app.target, EDIT_TABS[x]) : 0); const all = l?.release.all ?? []; const iosNow = l ? storeNow(all, 'ios').now : null, androidNow = l ? storeNow(all, 'android').now : null; const att = attention(app.key, app.name, e, l); const open = (url: string) => { api.openExternal(url).catch(() => {}); }; const cm = useContextMenu(); const [showIgnored] = useShowIgnored(); return (
appMenu(app, d, nav, { showIgnored }), `app:${app.key}`)} badges={<> {e?.summary?.profile && app.project && {t(`profile.${e.summary.profile}` as 'profile.app')}} {!app.project && } } ids={<> {iosNow && <>{iosNow.version}} {androidNow && <>{androidNow.version}} } links={(ios || android) && <> {ios && } {android && } } all={all} ios={!!ios} android={!!android} next={l?.release.next?.version ?? null} compact={section !== 'release' || tab !== 'releases'} cta={nextStep({ project: !!app.project, att, all, next: l?.release.next?.version ?? null })} onCta={setTab} /> {!app.project && } {app.project?.storeboundPin && {t('app.pinNotice', { pin: app.project.storeboundPin, app: d.info?.version ?? '' })}} {app.project?.missing ? : <>
{ const n = subsOf(x, !!app.project, game).reduce((a, s2) => a + editCount(s2), 0); return { id: x, label: t(SECTION_KEY[x]), icon: SECTION_ICON[x], hint: String(i + 1), count: att.filter(a => a.tab && sectionOf(a.tab) === x && a.tone === 'bad').length, dot: n > 0, dotTitle: n ? t('ed.unsaved', { count: n }) : undefined }; })} />
a + editCount(s2), 0)} editCount={editCount} onTab={setTab} />
{tab === 'releases' && } {tab === 'catalog' && app.project && } {tab === 'prices' && app.project && } {tab === 'listings' && app.project && <>} {tab === 'listings' && !app.project && } {tab === 'art' && app.project && } {tab === 'art' && !app.project && } {tab === 'health' && } {tab === 'reviews' && } {tab === 'games' && app.project && } {tab === 'testflight' && } {tab === 'growth' && } {tab === 'appinfo' && app.project && } {tab === 'checks' && app.project && } {tab === 'tools' && app.project && } {tab !== 'listings' && app.project && !app.project.missing && EDIT_TABS[tab] && }
}
); }