// Absolute deep links to App Store Connect and Play Console pages. Every message that sends the owner (or an agent) to a // console page goes through here: "App Information > Age Rating: https://appstoreconnect.apple.com/apps//...", // never plain directions alone (pitfall 71). With unknown ids the link falls back to the best page that exists (the // app list, the developer chooser), so a message always carries an absolute https URL. // // Ids: apple.appId, play.developerId and play.appId from the config (loadConfig calls setConsoleIds), or explicit ids. // URL shapes reused from console-forms / console-plan (app-dashboard, app-content/*, create-new-app, app-list) are // verified; the others follow the consoles' current routes (see UNVERIFIED in the tests' page list). export const ASC = 'https://appstoreconnect.apple.com'; export const PLAY_CONSOLE = 'https://play.google.com/console'; export interface ConsoleIds { appleAppId?: string | null; playDeveloperId?: string | null; playAppId?: string | null } let current: ConsoleIds = {}; /** Remember the loaded config's ids so every later message deep-links to this app. */ export function setConsoleIds(ids: ConsoleIds): void { current = { ...ids }; } export function consoleIdsFromConfig(c: { apple?: { appId?: string }; play?: { developerId?: string; appId?: string } } | null | undefined): ConsoleIds { return { appleAppId: c?.apple?.appId, playDeveloperId: c?.play?.developerId, playAppId: c?.play?.appId }; } type AppleScope = 'app' | 'account'; type PlayScope = 'app' | 'developer'; interface ApplePage { store: 'apple'; label: string; scope: AppleScope; path: string } interface PlayPage { store: 'play'; label: string; scope: PlayScope; path: string } /** Every console page storebound points to: readable path + URL path under the app (or account). */ export const CONSOLE_PAGES = { // App Store Connect, per app (/apps//...) 'asc.apps': { store: 'apple', label: 'App Store Connect > Apps', scope: 'account', path: '/apps' }, 'asc.new-app': { store: 'apple', label: 'App Store Connect > Apps > + > New App', scope: 'account', path: '/apps' }, 'asc.app-info': { store: 'apple', label: 'App Store Connect > App Information', scope: 'app', path: '/distribution/info' }, 'asc.age-rating': { store: 'apple', label: 'App Store Connect > App Information > Age Rating', scope: 'app', path: '/distribution/info' }, 'asc.encryption': { store: 'apple', label: 'App Store Connect > App Information > App Encryption Documentation', scope: 'app', path: '/distribution/info' }, 'asc.app-privacy': { store: 'apple', label: 'App Store Connect > App Privacy', scope: 'app', path: '/distribution/privacy' }, 'asc.pricing': { store: 'apple', label: 'App Store Connect > Pricing and Availability', scope: 'app', path: '/distribution/pricing' }, 'asc.version': { store: 'apple', label: 'App Store Connect > the iOS version page', scope: 'app', path: '/distribution/ios/version/inflight' }, 'asc.app-review': { store: 'apple', label: 'App Store Connect > App Review', scope: 'app', path: '/distribution/reviewsubmissions' }, 'asc.testflight': { store: 'apple', label: 'App Store Connect > TestFlight', scope: 'app', path: '/testflight/ios' }, 'asc.iaps': { store: 'apple', label: 'App Store Connect > In-App Purchases', scope: 'app', path: '/distribution/iaps' }, 'asc.subscriptions': { store: 'apple', label: 'App Store Connect > Subscriptions', scope: 'app', path: '/distribution/subscriptions' }, 'asc.ppo': { store: 'apple', label: 'App Store Connect > Product Page Optimization', scope: 'app', path: '/distribution/product-page-optimization' }, // App Store Connect, account 'asc.payments': { store: 'apple', label: 'App Store Connect > Payments and Financial Reports', scope: 'account', path: '/itc/payments_and_financial_reports' }, 'asc.users': { store: 'apple', label: 'App Store Connect > Users and Access', scope: 'account', path: '/access/users' }, 'asc.keys': { store: 'apple', label: 'App Store Connect > Users and Access > Integrations > App Store Connect API', scope: 'account', path: '/access/integrations/api' }, // Play Console, per app (/u/0/developers//app//...) 'play.dashboard': { store: 'play', label: 'Play Console > Dashboard', scope: 'app', path: 'app-dashboard' }, 'play.app-content': { store: 'play', label: 'Play Console > Policy and programs > App content', scope: 'app', path: 'app-content/overview' }, 'play.data-safety': { store: 'play', label: 'Play Console > App content > Data safety', scope: 'app', path: 'app-content/data-privacy-security' }, 'play.content-rating': { store: 'play', label: 'Play Console > App content > Content ratings', scope: 'app', path: 'app-content/content-rating-overview' }, 'play.target-audience': { store: 'play', label: 'Play Console > App content > Target audience and content', scope: 'app', path: 'app-content/target-audience-content' }, 'play.ads': { store: 'play', label: 'Play Console > App content > Ads', scope: 'app', path: 'app-content/ads-declaration' }, 'play.app-access': { store: 'play', label: 'Play Console > App content > App access', scope: 'app', path: 'app-content/testing-credentials' }, 'play.foreground-service': { store: 'play', label: 'Play Console > App content > Foreground service permissions', scope: 'app', path: 'app-content/foreground-services' }, 'play.sensitive-permissions': { store: 'play', label: 'Play Console > App content > Sensitive app permissions', scope: 'app', path: 'app-content/overview' }, 'play.health-apps': { store: 'play', label: 'Play Console > App content > Health apps', scope: 'app', path: 'app-content/overview' }, 'play.full-screen-intent': { store: 'play', label: 'Play Console > App content > Full-screen intent', scope: 'app', path: 'app-content/overview' }, 'play.store-listing': { store: 'play', label: 'Play Console > Store presence > Main store listing', scope: 'app', path: 'main-store-listing' }, 'play.store-settings': { store: 'play', label: 'Play Console > Store presence > Store settings', scope: 'app', path: 'store-settings' }, 'play.listing-experiments': { store: 'play', label: 'Play Console > Grow users > Store listing experiments', scope: 'app', path: 'store-listing-experiments' }, 'play.releases': { store: 'play', label: 'Play Console > Test and release', scope: 'app', path: 'releases/overview' }, 'play.production': { store: 'play', label: 'Play Console > Test and release > Production', scope: 'app', path: 'tracks/production' }, 'play.internal-testing': { store: 'play', label: 'Play Console > Test and release > Internal testing', scope: 'app', path: 'tracks/internal-testing' }, 'play.publishing': { store: 'play', label: 'Play Console > Publishing overview', scope: 'app', path: 'publishing' }, 'play.app-signing': { store: 'play', label: 'Play Console > Test and release > App integrity > Play app signing', scope: 'app', path: 'keymanagement' }, 'play.games-services': { store: 'play', label: 'Play Console > Grow users > Play Games Services > Setup and management > Configuration', scope: 'app', path: 'play-games-services/setup' }, 'play.bundle-explorer': { store: 'play', label: 'Play Console > Test and release > App bundle explorer', scope: 'app', path: 'bundle-explorer-selector' }, 'play.products': { store: 'play', label: 'Play Console > Monetize with Play > Products > In-app products', scope: 'app', path: 'managed-products' }, 'play.subscriptions': { store: 'play', label: 'Play Console > Monetize with Play > Products > Subscriptions', scope: 'app', path: 'subscriptions' }, 'play.promo-codes': { store: 'play', label: 'Play Console > Monetize with Play > Promo codes', scope: 'app', path: 'promotions' }, 'play.vitals': { store: 'play', label: 'Play Console > Monitor and improve > Android vitals', scope: 'app', path: 'vitals/crashes' }, 'play.policy-status': { store: 'play', label: 'Play Console > Policy and programs > Policy status', scope: 'app', path: 'policy-center/issues' }, 'play.pre-registration': { store: 'play', label: 'Play Console > Test and release > Production > Countries / regions (pre-registration)', scope: 'app', path: 'tracks/production' }, // Play Console, developer account (/u/0/developers//...) 'play.app-list': { store: 'play', label: 'Play Console > Home (All apps)', scope: 'developer', path: 'app-list' }, 'play.create-app': { store: 'play', label: 'Play Console > Home > Create app', scope: 'developer', path: 'create-new-app' }, 'play.reports-statistics': { store: 'play', label: 'Play Console > Download reports > Statistics', scope: 'developer', path: 'download-reports/statistics' }, 'play.reports-financial': { store: 'play', label: 'Play Console > Download reports > Financial', scope: 'developer', path: 'download-reports/financial' }, 'play.users': { store: 'play', label: 'Play Console > Users and permissions', scope: 'developer', path: 'users-and-permissions' }, 'play.api-access': { store: 'play', label: 'Play Console > Setup > API access', scope: 'developer', path: 'api-access' }, 'play.internal-sharing': { store: 'play', label: 'Play Console > Internal app sharing', scope: 'developer', path: 'internal-app-sharing' }, } as const satisfies Record; export type ConsolePage = keyof typeof CONSOLE_PAGES; /** The absolute URL of a console page. Unknown ids: the best page that exists (app list / developer chooser). */ export function consoleUrl(page: ConsolePage, ids: ConsoleIds = current): string { const p: ApplePage | PlayPage = CONSOLE_PAGES[page]; const id = (v: string | null | undefined) => v && /^\d+$/.test(v) ? v : null; if (p.store === 'apple') { if (p.scope === 'account') return `${ASC}${p.path}`; const app = id(ids.appleAppId ?? current.appleAppId); return app ? `${ASC}/apps/${app}${p.path}` : `${ASC}/apps`; } const dev = id(ids.playDeveloperId ?? current.playDeveloperId), app = id(ids.playAppId ?? current.playAppId); if (!dev) return PLAY_CONSOLE; const base = `${PLAY_CONSOLE}/u/0/developers/${dev}`; if (p.scope === 'developer') return `${base}/${p.path}`; return app ? `${base}/app/${app}/${p.path}` : `${base}/app-list`; } /** True when the URL is the page itself (ids known), false when it is a fallback (app list, chooser). */ export function consoleUrlExact(page: ConsolePage, ids: ConsoleIds = current): boolean { const u = consoleUrl(page, ids); return u !== PLAY_CONSOLE && !(CONSOLE_PAGES[page].scope === 'app' && (u === `${ASC}/apps` || u.endsWith('/app-list'))); } /** "Readable > path: https://..." (the path suffix, e.g. "> Copy Cloud Storage URI", is appended to the label). */ export function consoleLink(page: ConsolePage, suffix = '', ids: ConsoleIds = current): string { return `${CONSOLE_PAGES[page].label}${suffix ? ` ${suffix}` : ''}: ${consoleUrl(page, ids)}`; } /** Play track page: production / internal have their own page; other tracks open Test and release. */ export function playTrackPage(track: string | undefined): ConsolePage { return track === 'production' ? 'play.production' : track === 'internal' ? 'play.internal-testing' : 'play.releases'; }