Open in Google Play Console deep-links the app: Play developer id per credential profile (accounts play-developer, desktop Accounts), shared playAppUrl builder for desktop, CLI and watch; developer chooser instead of the console root
This commit is contained in:
parent
7c769c18f4
commit
4a74989a95
33 changed files with 311 additions and 40 deletions
|
|
@ -46,6 +46,8 @@ function AccountRow({ a, busy, setBusy, onError }: { a: Account; busy: string |
|
|||
const [edit, setEdit] = useState(false);
|
||||
const [label, setLabel] = useState(a.label);
|
||||
const [studio, setStudio] = useState(a.studio ?? '');
|
||||
const [dev, setDev] = useState(a.developerId ?? '');
|
||||
const devOk = !dev.trim() || /^\d{1,25}$/.test(dev.trim());
|
||||
const h = d.accountHealth[a.id];
|
||||
const list = d.storeApps[a.id];
|
||||
const ignored = d.overrides.ignored.includes(`account:${a.id}`);
|
||||
|
|
@ -54,7 +56,7 @@ function AccountRow({ a, busy, setBusy, onError }: { a: Account; busy: string |
|
|||
setBusy(key); onError(null);
|
||||
try { await fn(); } catch (e) { onError(errorText(e)); } finally { setBusy(null); }
|
||||
};
|
||||
const save = (e: FormEvent) => { e.preventDefault(); run(`ed-${a.id}`, async () => { await api.updateAccount(a.id, label, studio || null); await d.reload(); setEdit(false); }); };
|
||||
const save = (e: FormEvent) => { e.preventDefault(); run(`ed-${a.id}`, async () => { await api.updateAccount(a.id, label, studio || null); if (a.kind === 'play' && a.profile && dev.trim() !== (a.developerId ?? '')) await api.setPlayDeveloper(a.id, dev.trim() || null); await d.reload(); setEdit(false); }); };
|
||||
const check = h?.check;
|
||||
const cm = useContextMenu();
|
||||
return (
|
||||
|
|
@ -81,6 +83,7 @@ function AccountRow({ a, busy, setBusy, onError }: { a: Account; busy: string |
|
|||
{' · '}{a.profile ? <span className="mono-id selectable" title="~/.storebound">{t('acct.profile', { name: a.profile })}</span> : t('acct.noProfile')}
|
||||
{' · '}{list ? t('acct.appsRead', { count: list.value.apps.length, when: ago(list.at) }) : t('acct.notImported')}
|
||||
{used ? ` · ${t('acct.folders', { count: used })}` : ''}
|
||||
{a.kind === 'play' && (a.developerId ? <> · <span className="mono-id selectable" title={t('acct.devIdHint')}>{t('acct.devId', { id: a.developerId })}</span></> : <> · <span title={t('acct.devIdHint')}>{t('acct.noDevId')}</span></>)}
|
||||
{check?.principal ? <> · <span className="mono-id selectable">{check.principal}</span></> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -100,7 +103,10 @@ function AccountRow({ a, busy, setBusy, onError }: { a: Account; busy: string |
|
|||
<option value="">{t('studio.none')}</option>
|
||||
{d.studios.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||
</select></div>
|
||||
<div className="actions"><button className="btn btn-primary btn-sm" type="submit" disabled={!label.trim() || !!busy}><Icon name="check" />{t('common.save')}</button><button type="button" className="btn btn-sm btn-ghost" onClick={() => setEdit(false)}>{t('common.cancel')}</button></div>
|
||||
{a.kind === 'play' && a.profile && <div className="field"><label htmlFor={`ad-${a.id}`}>{t('acct.devIdLabel')}</label>
|
||||
<input id={`ad-${a.id}`} className="input mono" inputMode="numeric" value={dev} onChange={e => setDev(e.target.value)} placeholder="5550000000000000001" aria-invalid={!devOk} aria-describedby={`adh-${a.id}`} />
|
||||
<small id={`adh-${a.id}`} className="muted">{t('acct.devIdHint')}</small></div>}
|
||||
<div className="actions"><button className="btn btn-primary btn-sm" type="submit" disabled={!label.trim() || !devOk || !!busy}><Icon name="check" />{t('common.save')}</button><button type="button" className="btn btn-sm btn-ghost" onClick={() => setEdit(false)}>{t('common.cancel')}</button></div>
|
||||
</form>
|
||||
)}
|
||||
{check && check.status !== 'ok' && check.fix && (
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// 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 } from '../ui/app-menu';
|
||||
import { appMenu, playUrlFor } from '../ui/app-menu';
|
||||
import { useShowIgnored } from '../lib/show-ignored';
|
||||
import { useState } from 'react';
|
||||
import { api, errorText } from '../api';
|
||||
|
|
@ -34,7 +34,6 @@ 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');
|
||||
export const PLAY_CONSOLE = 'https://play.google.com/console/';
|
||||
|
||||
const PAIRING: Record<UnifiedApp['pairing'], Key> = { folder: 'pair.folder', manual: 'pair.manual', id: 'pair.id', close: 'pair.close', name: 'pair.name', single: 'pair.single' };
|
||||
|
||||
|
|
@ -135,7 +134,7 @@ export function AppView({ app, tab }: { app: UnifiedApp; tab: AppTab }) {
|
|||
</>}
|
||||
links={(ios || android) && <>
|
||||
{ios && <button className="btn btn-sm btn-ghost" onClick={() => open(ascUrl(ios.ascAppId))} title={t('app.consoleHint')}><Icon name="external" />App Store Connect</button>}
|
||||
{android && <button className="btn btn-sm btn-ghost" onClick={() => open(PLAY_CONSOLE)} title={t('app.consoleHint')}><Icon name="external" />Play Console</button>}
|
||||
{android && <button className="btn btn-sm btn-ghost" onClick={() => open(playUrlFor(d, app))} title={t('app.consoleHint')}><Icon name="external" />Play Console</button>}
|
||||
</>}
|
||||
all={all} ios={!!ios} android={!!android} next={l?.release.next?.version ?? null}
|
||||
cta={nextStep({ project: !!app.project, att, all, next: l?.release.next?.version ?? null })} onCta={setTab} />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Catalog: products and subscriptions defined once in the config against both stores (catalog diff): a parity
|
||||
// matrix (present, mismatched, missing, not sold there, not read), and price gaps between the stores per region.
|
||||
import { useContextMenu } from '../../ui/ContextMenu';
|
||||
import { copyToast, projectConsoles } from '../../ui/app-menu';
|
||||
import { copyToast, playUrlFor, projectConsoles } from '../../ui/app-menu';
|
||||
import '../../styles/catalog.css';
|
||||
import type { Project } from '../../api';
|
||||
import { type Key, t } from '../../i18n';
|
||||
|
|
@ -110,7 +110,7 @@ export function CatalogTab({ project }: { project: Project }) {
|
|||
{ label: t('cm.copyProductId'), icon: 'copy', shortcut: r.id, onSelect: () => copyToast(d, r.id) },
|
||||
{ label: t('cm.showPrices'), icon: 'coins', onSelect: () => nav.go({ screen: 'app', appId: project.id, tab: 'prices' }) },
|
||||
'sep',
|
||||
...projectConsoles(d, plat?.ios?.ascAppId, !!plat?.ios, !!plat?.android),
|
||||
...projectConsoles(d, plat?.ios?.ascAppId, !!plat?.ios, !!plat?.android, playUrlFor(d, d.appByKey(project.id))),
|
||||
])}>
|
||||
<div className="cat-card-h">
|
||||
<span className="cat-ic" aria-hidden="true"><Icon name={r.kind === 'subscription' ? 'refresh' : 'tag'} /></span>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import '../../styles/growth.css';
|
|||
import { LineChart } from '../../ui/LineChart';
|
||||
import { RankChart } from '../../ui/RankChart';
|
||||
import { WriteButton } from '../../ui/WriteButton';
|
||||
import { ascUrl, PLAY_CONSOLE } from '../AppView';
|
||||
import { ascUrl } from '../AppView';
|
||||
import { playUrlFor } from '../../ui/app-menu';
|
||||
|
||||
const DONE = ['COMPLETED', 'STOPPED'];
|
||||
const started = (x: PpoTest) => !!x.startDate && !x.endDate && !DONE.includes(x.state);
|
||||
|
|
@ -243,10 +244,11 @@ function Experiments({ app }: { app: UnifiedApp }) {
|
|||
}
|
||||
|
||||
function PlayExperiments({ app }: { app: UnifiedApp }) {
|
||||
const d = useData();
|
||||
const nav = useNav();
|
||||
return (
|
||||
<Section title={t('gr.playExp')} icon="flask" sub={<span className="row" style={{ gap: 6 }}><StoreGlyph store="android" className="ic13" />{t('gr.playExpSub')}</span>}
|
||||
actions={<button className="btn btn-sm" onClick={() => api.openExternal(PLAY_CONSOLE).catch(() => {})}><Icon name="external" />Play Console</button>}>
|
||||
actions={<button className="btn btn-sm" onClick={() => api.openExternal(playUrlFor(d, app)).catch(() => {})}><Icon name="external" />Play Console</button>}>
|
||||
<div className="gr-body">
|
||||
<p className="gr-text">{t('gr.playExpText')}</p>
|
||||
{app.project && <div><button className="btn btn-sm" onClick={() => nav.go({ screen: 'app', appId: app.key, tab: 'art' })}><Icon name="grid" />{t('gr.seeVariants')}</button></div>}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ 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, PLAY_CONSOLE } from '../AppView';
|
||||
import { ascUrl } from '../AppView';
|
||||
import { playUrlFor } from '../../ui/app-menu';
|
||||
import '../../styles/submit.css';
|
||||
|
||||
const SUB_KIND: Record<SubmissionKind, ['ok' | 'warn' | 'bad' | 'info' | 'neutral', Key]> = {
|
||||
|
|
@ -219,7 +220,7 @@ export function PlayProductionCard({ app, project }: { app: UnifiedApp; project:
|
|||
sub={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={<>
|
||||
<ReadControl r={r} label={t('sub.read')} />
|
||||
<button className="btn btn-sm btn-ghost" onClick={() => api.openExternal(PLAY_CONSOLE).catch(() => {})}><Icon name="external" />Play Console</button>
|
||||
<button className="btn btn-sm btn-ghost" onClick={() => api.openExternal(playUrlFor(d, app)).catch(() => {})}><Icon name="external" />Play Console</button>
|
||||
</>} />
|
||||
<div className="sub-body">
|
||||
{r.error && <ErrorNotice error={r.error} keep={!!r.data} />}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue