pabal-store-api-mcp 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Screenshot upload helper utilities
3
+ * Shared logic for Google Play and App Store screenshot operations
4
+ */
5
+ /**
6
+ * Device type mappings for App Store screenshots
7
+ */
8
+ export declare const APP_STORE_DEVICE_TYPES: {
9
+ readonly iphone67: "APP_IPHONE_67";
10
+ readonly iphone65: "APP_IPHONE_65";
11
+ readonly iphone61: "APP_IPHONE_61";
12
+ readonly iphone58: "APP_IPHONE_58";
13
+ readonly iphone55: "APP_IPHONE_55";
14
+ readonly iphone47: "APP_IPHONE_47";
15
+ readonly iphone40: "APP_IPHONE_40";
16
+ readonly ipadPro129: "APP_IPAD_PRO_3GEN_129";
17
+ readonly ipadPro11: "APP_IPAD_PRO_11";
18
+ readonly ipad105: "APP_IPAD_105";
19
+ readonly ipad97: "APP_IPAD_97";
20
+ };
21
+ export type AppStoreDeviceType = keyof typeof APP_STORE_DEVICE_TYPES;
22
+ export type AppStoreScreenshotDisplayType = (typeof APP_STORE_DEVICE_TYPES)[AppStoreDeviceType];
23
+ /**
24
+ * Screenshot file info parsed from filename
25
+ */
26
+ export interface ParsedScreenshotFile {
27
+ filename: string;
28
+ path: string;
29
+ deviceType: string;
30
+ order: number;
31
+ }
32
+ /**
33
+ * Parse App Store screenshot filename
34
+ * @param filename e.g., "iphone65-1.png"
35
+ * @returns Parsed info or null if invalid
36
+ */
37
+ export declare function parseAppStoreScreenshotFilename(filename: string, basePath: string): ParsedScreenshotFile | null;
38
+ /**
39
+ * Group screenshots by device type with ordering
40
+ */
41
+ export declare function groupScreenshotsByDeviceType(screenshots: ParsedScreenshotFile[]): Record<string, ParsedScreenshotFile[]>;
42
+ /**
43
+ * Get screenshot files for a locale
44
+ * @returns Array of PNG files in the directory, or empty array if directory doesn't exist
45
+ */
46
+ export declare function getLocaleScreenshotFiles(screenshotsBaseDir: string, locale: string): string[];
47
+ /**
48
+ * Check if locale has any screenshot files
49
+ */
50
+ export declare function hasScreenshots(screenshotsBaseDir: string, locale: string): boolean;
51
+ /**
52
+ * Parse and group App Store screenshots for a locale
53
+ */
54
+ export declare function parseAppStoreScreenshots(screenshotsBaseDir: string, locale: string): {
55
+ valid: Record<string, ParsedScreenshotFile[]>;
56
+ invalid: string[];
57
+ unknown: string[];
58
+ };
59
+ /**
60
+ * Parse Google Play screenshot filenames
61
+ * Supports: phone-*.png, tablet7-*.png, tablet10-*.png, feature-graphic.png
62
+ */
63
+ export interface GooglePlayScreenshotFiles {
64
+ phone: string[];
65
+ tablet7: string[];
66
+ tablet10: string[];
67
+ featureGraphic: string | null;
68
+ }
69
+ export declare function parseGooglePlayScreenshots(screenshotsBaseDir: string, locale: string): GooglePlayScreenshotFiles;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Screenshot upload helper utilities
3
+ * Shared logic for Google Play and App Store screenshot operations
4
+ */
5
+ import { existsSync, readdirSync } from "node:fs";
6
+ /**
7
+ * Device type mappings for App Store screenshots
8
+ */
9
+ export const APP_STORE_DEVICE_TYPES = {
10
+ iphone67: "APP_IPHONE_67",
11
+ iphone65: "APP_IPHONE_65",
12
+ iphone61: "APP_IPHONE_61",
13
+ iphone58: "APP_IPHONE_58",
14
+ iphone55: "APP_IPHONE_55",
15
+ iphone47: "APP_IPHONE_47",
16
+ iphone40: "APP_IPHONE_40",
17
+ ipadPro129: "APP_IPAD_PRO_3GEN_129",
18
+ ipadPro11: "APP_IPAD_PRO_11",
19
+ ipad105: "APP_IPAD_105",
20
+ ipad97: "APP_IPAD_97",
21
+ };
22
+ /**
23
+ * Parse App Store screenshot filename
24
+ * @param filename e.g., "iphone65-1.png"
25
+ * @returns Parsed info or null if invalid
26
+ */
27
+ export function parseAppStoreScreenshotFilename(filename, basePath) {
28
+ const match = filename.match(/^([a-zA-Z0-9]+)-(\d+)\.png$/);
29
+ if (!match)
30
+ return null;
31
+ const [, deviceType, orderStr] = match;
32
+ const order = parseInt(orderStr, 10);
33
+ if (!APP_STORE_DEVICE_TYPES[deviceType]) {
34
+ return null;
35
+ }
36
+ return {
37
+ filename,
38
+ path: `${basePath}/${filename}`,
39
+ deviceType: APP_STORE_DEVICE_TYPES[deviceType],
40
+ order,
41
+ };
42
+ }
43
+ /**
44
+ * Group screenshots by device type with ordering
45
+ */
46
+ export function groupScreenshotsByDeviceType(screenshots) {
47
+ const grouped = {};
48
+ for (const screenshot of screenshots) {
49
+ if (!grouped[screenshot.deviceType]) {
50
+ grouped[screenshot.deviceType] = [];
51
+ }
52
+ grouped[screenshot.deviceType].push(screenshot);
53
+ }
54
+ // Sort each group by order
55
+ for (const deviceType in grouped) {
56
+ grouped[deviceType].sort((a, b) => a.order - b.order);
57
+ }
58
+ return grouped;
59
+ }
60
+ /**
61
+ * Get screenshot files for a locale
62
+ * @returns Array of PNG files in the directory, or empty array if directory doesn't exist
63
+ */
64
+ export function getLocaleScreenshotFiles(screenshotsBaseDir, locale) {
65
+ const localeDir = `${screenshotsBaseDir}/${locale}`;
66
+ if (!existsSync(localeDir)) {
67
+ return [];
68
+ }
69
+ return readdirSync(localeDir).filter((file) => file.endsWith(".png"));
70
+ }
71
+ /**
72
+ * Check if locale has any screenshot files
73
+ */
74
+ export function hasScreenshots(screenshotsBaseDir, locale) {
75
+ const files = getLocaleScreenshotFiles(screenshotsBaseDir, locale);
76
+ return files.length > 0;
77
+ }
78
+ /**
79
+ * Parse and group App Store screenshots for a locale
80
+ */
81
+ export function parseAppStoreScreenshots(screenshotsBaseDir, locale) {
82
+ const localeDir = `${screenshotsBaseDir}/${locale}`;
83
+ const files = getLocaleScreenshotFiles(screenshotsBaseDir, locale);
84
+ const validScreenshots = [];
85
+ const invalidFilenames = [];
86
+ const unknownDeviceTypes = [];
87
+ for (const file of files) {
88
+ const parsed = parseAppStoreScreenshotFilename(file, localeDir);
89
+ if (!parsed) {
90
+ invalidFilenames.push(file);
91
+ continue;
92
+ }
93
+ if (!parsed.deviceType) {
94
+ unknownDeviceTypes.push(file);
95
+ continue;
96
+ }
97
+ validScreenshots.push(parsed);
98
+ }
99
+ return {
100
+ valid: groupScreenshotsByDeviceType(validScreenshots),
101
+ invalid: invalidFilenames,
102
+ unknown: unknownDeviceTypes,
103
+ };
104
+ }
105
+ export function parseGooglePlayScreenshots(screenshotsBaseDir, locale) {
106
+ const localeDir = `${screenshotsBaseDir}/${locale}`;
107
+ const files = getLocaleScreenshotFiles(screenshotsBaseDir, locale);
108
+ return {
109
+ phone: files
110
+ .filter((f) => f.startsWith("phone-") && f.endsWith(".png"))
111
+ .map((f) => `${localeDir}/${f}`),
112
+ tablet7: files
113
+ .filter((f) => f.startsWith("tablet7-") && f.endsWith(".png"))
114
+ .map((f) => `${localeDir}/${f}`),
115
+ tablet10: files
116
+ .filter((f) => f.startsWith("tablet10-") && f.endsWith(".png"))
117
+ .map((f) => `${localeDir}/${f}`),
118
+ featureGraphic: files.includes("feature-graphic.png")
119
+ ? `${localeDir}/feature-graphic.png`
120
+ : null,
121
+ };
122
+ }
@@ -27,11 +27,13 @@ export declare class AppStoreService {
27
27
  updateReleaseNotes(bundleId: string, releaseNotes: Record<string, string>, versionId?: string, supportedLocales?: string[]): Promise<ServiceResult<UpdatedReleaseNotesResult>>;
28
28
  pullReleaseNotes(bundleId: string): Promise<ServiceResult<AppStoreReleaseNote[]>>;
29
29
  createVersion(bundleId: string, versionString: string, autoIncrement?: boolean): Promise<ServiceResult<CreatedAppStoreVersion>>;
30
- pushAsoData({ config, bundleId, localAsoData, appStoreDataPath, }: {
30
+ pushAsoData({ config, bundleId, localAsoData, appStoreDataPath, uploadImages, slug, }: {
31
31
  config: EnvConfig;
32
32
  bundleId?: string;
33
33
  localAsoData: PreparedAsoData;
34
34
  appStoreDataPath: string;
35
+ uploadImages?: boolean;
36
+ slug?: string;
35
37
  }): Promise<PushAsoResult>;
36
38
  verifyAuth(expirationSeconds?: number): Promise<VerifyAuthResult<{
37
39
  header: Record<string, unknown>;
@@ -161,7 +161,7 @@ export class AppStoreService {
161
161
  return serviceFailure(AppError.wrap(error, HTTP_STATUS.INTERNAL_SERVER_ERROR, ERROR_CODES.APP_STORE_CREATE_VERSION_FAILED, "Failed to create App Store version"));
162
162
  }
163
163
  }
164
- async pushAsoData({ config, bundleId, localAsoData, appStoreDataPath, }) {
164
+ async pushAsoData({ config, bundleId, localAsoData, appStoreDataPath, uploadImages = false, slug, }) {
165
165
  const skip = checkPushPrerequisites({
166
166
  storeLabel: "App Store",
167
167
  configured: Boolean(config.appStore),
@@ -194,6 +194,80 @@ export class AppStoreService {
194
194
  console.error(`[AppStore] ✅ ${locale} uploaded successfully`);
195
195
  }
196
196
  }
197
+ // Upload screenshots if enabled
198
+ if (uploadImages && slug) {
199
+ console.error(`[AppStore] 📤 Uploading screenshots...`);
200
+ const { getAsoPushDir } = await import("../../packages/configs/aso-config/utils.js");
201
+ const { parseAppStoreScreenshots, hasScreenshots } = await import("../../core/helpers/screenshot-helpers.js");
202
+ const pushDataDir = getAsoPushDir();
203
+ const screenshotsBaseDir = `${pushDataDir}/products/${slug}/store/app-store/screenshots`;
204
+ const uploadedLocales = [];
205
+ const skippedLocales = [];
206
+ const failedLocales = [];
207
+ for (const locale of localesToPush) {
208
+ try {
209
+ if (!hasScreenshots(screenshotsBaseDir, locale)) {
210
+ console.error(`[AppStore] ⏭️ Skipping ${locale} - no screenshots directory`);
211
+ skippedLocales.push(locale);
212
+ continue;
213
+ }
214
+ const result = parseAppStoreScreenshots(screenshotsBaseDir, locale);
215
+ // Check if there are any valid screenshots
216
+ const totalScreenshots = Object.values(result.valid).reduce((sum, screenshots) => sum + screenshots.length, 0);
217
+ if (totalScreenshots === 0) {
218
+ console.error(`[AppStore] ⚠️ Skipping ${locale} - no valid screenshots found`);
219
+ skippedLocales.push(locale);
220
+ continue;
221
+ }
222
+ console.error(`[AppStore] 📤 Uploading screenshots for ${locale}...`);
223
+ // Report parsing issues
224
+ if (result.invalid.length > 0) {
225
+ console.error(`[AppStore] ⚠️ Invalid filenames: ${result.invalid.join(", ")}`);
226
+ }
227
+ if (result.unknown.length > 0) {
228
+ console.error(`[AppStore] ⚠️ Unknown device types: ${result.unknown.join(", ")}`);
229
+ }
230
+ // Upload screenshots for each device type
231
+ for (const [displayType, screenshots] of Object.entries(result.valid)) {
232
+ if (screenshots.length === 0)
233
+ continue;
234
+ console.error(`[AppStore] 📱 Uploading ${screenshots.length} screenshots for ${displayType}...`);
235
+ for (const screenshot of screenshots) {
236
+ try {
237
+ await client.uploadScreenshot({
238
+ imagePath: screenshot.path,
239
+ screenshotDisplayType: displayType,
240
+ locale,
241
+ });
242
+ console.error(`[AppStore] ✅ ${screenshot.filename}`);
243
+ }
244
+ catch (uploadError) {
245
+ const msg = uploadError instanceof Error
246
+ ? uploadError.message
247
+ : String(uploadError);
248
+ console.error(`[AppStore] ❌ ${screenshot.filename}: ${msg}`);
249
+ }
250
+ }
251
+ }
252
+ uploadedLocales.push(locale);
253
+ console.error(`[AppStore] ✅ Screenshots uploaded for ${locale}`);
254
+ }
255
+ catch (error) {
256
+ console.error(`[AppStore] ❌ Failed to upload screenshots for ${locale}: ${error instanceof Error ? error.message : String(error)}`);
257
+ failedLocales.push(locale);
258
+ }
259
+ }
260
+ console.error(`[AppStore] 📊 Screenshot upload summary: ${uploadedLocales.length} succeeded, ${skippedLocales.length} skipped, ${failedLocales.length} failed`);
261
+ if (uploadedLocales.length > 0) {
262
+ console.error(`[AppStore] ✅ Uploaded: ${uploadedLocales.join(", ")}`);
263
+ }
264
+ if (skippedLocales.length > 0) {
265
+ console.error(`[AppStore] ⏭️ Skipped: ${skippedLocales.join(", ")}`);
266
+ }
267
+ if (failedLocales.length > 0) {
268
+ console.error(`[AppStore] ❌ Failed: ${failedLocales.join(", ")}`);
269
+ }
270
+ }
197
271
  try {
198
272
  const updated = updateRegisteredLocales(ensuredBundleId, "appStore", localesToPush);
199
273
  if (updated) {
@@ -22,11 +22,13 @@ export declare class GooglePlayService {
22
22
  updateReleaseNotes(packageName: string, releaseNotes: Record<string, string>, track?: string, supportedLocales?: string[]): Promise<ServiceResult<UpdatedReleaseNotesResult>>;
23
23
  pullReleaseNotes(packageName: string): Promise<ServiceResult<GooglePlayReleaseNote[]>>;
24
24
  createVersion(packageName: string, versionString: string, versionCodes: number[]): Promise<ServiceResult<CreatedGooglePlayVersion>>;
25
- pushAsoData({ config, packageName, localAsoData, googlePlayDataPath, }: {
25
+ pushAsoData({ config, packageName, localAsoData, googlePlayDataPath, uploadImages, slug, }: {
26
26
  config: EnvConfig;
27
27
  packageName?: string;
28
28
  localAsoData: PreparedAsoData;
29
29
  googlePlayDataPath: string;
30
+ uploadImages?: boolean;
31
+ slug?: string;
30
32
  }): Promise<PushAsoResult>;
31
33
  verifyAuth(): Promise<VerifyAuthResult<{
32
34
  client_email: string;
@@ -146,7 +146,7 @@ export class GooglePlayService {
146
146
  return serviceFailure(AppError.wrap(error, HTTP_STATUS.INTERNAL_SERVER_ERROR, ERROR_CODES.GOOGLE_PLAY_CREATE_VERSION_FAILED, "Failed to create Google Play version"));
147
147
  }
148
148
  }
149
- async pushAsoData({ config, packageName, localAsoData, googlePlayDataPath, }) {
149
+ async pushAsoData({ config, packageName, localAsoData, googlePlayDataPath, uploadImages = false, slug, }) {
150
150
  const skip = checkPushPrerequisites({
151
151
  storeLabel: "Google Play",
152
152
  configured: Boolean(config.playStore),
@@ -168,6 +168,99 @@ export class GooglePlayService {
168
168
  console.error(`[GooglePlay] 📤 Preparing locale: ${locale}`);
169
169
  }
170
170
  await client.pushMultilingualAsoData(googlePlayData);
171
+ // Push app-level contact information
172
+ if (googlePlayData.contactEmail || googlePlayData.contactWebsite) {
173
+ console.error(`[GooglePlay] 📤 Pushing app details...`);
174
+ await client.pushAppDetails({
175
+ contactEmail: googlePlayData.contactEmail,
176
+ contactWebsite: googlePlayData.contactWebsite,
177
+ });
178
+ console.error(`[GooglePlay] ✅ App details uploaded successfully`);
179
+ }
180
+ // Note: YouTube URL is pushed as part of listing data for each locale
181
+ // Upload screenshots if enabled
182
+ if (uploadImages && slug) {
183
+ console.error(`[GooglePlay] 📤 Uploading screenshots...`);
184
+ const { getAsoPushDir } = await import("../../packages/configs/aso-config/utils.js");
185
+ const { parseGooglePlayScreenshots, hasScreenshots } = await import("../../core/helpers/screenshot-helpers.js");
186
+ const pushDataDir = getAsoPushDir();
187
+ const screenshotsBaseDir = `${pushDataDir}/products/${slug}/store/google-play/screenshots`;
188
+ const uploadedLocales = [];
189
+ const skippedLocales = [];
190
+ const failedLocales = [];
191
+ for (const locale of localesToPush) {
192
+ try {
193
+ if (!hasScreenshots(screenshotsBaseDir, locale)) {
194
+ console.error(`[GooglePlay] ⏭️ Skipping ${locale} - no screenshots directory`);
195
+ skippedLocales.push(locale);
196
+ continue;
197
+ }
198
+ const screenshots = parseGooglePlayScreenshots(screenshotsBaseDir, locale);
199
+ // Google Play requires minimum 2 phone screenshots
200
+ const phoneCount = screenshots.phone.length + screenshots.tablet7.length;
201
+ if (phoneCount < 2) {
202
+ console.error(`[GooglePlay] ⚠️ Skipping ${locale} - needs at least 2 phone/tablet7 screenshots (found ${phoneCount})`);
203
+ skippedLocales.push(locale);
204
+ continue;
205
+ }
206
+ console.error(`[GooglePlay] 📤 Uploading screenshots for ${locale}...`);
207
+ // Upload phone screenshots (phone-*.png)
208
+ for (const imagePath of screenshots.phone) {
209
+ await client.uploadScreenshot({
210
+ imagePath,
211
+ imageType: "phoneScreenshots",
212
+ language: locale,
213
+ });
214
+ console.error(`[GooglePlay] ✅ ${imagePath.split("/").pop()}`);
215
+ }
216
+ // Upload 7-inch tablet screenshots as phone
217
+ for (const imagePath of screenshots.tablet7) {
218
+ await client.uploadScreenshot({
219
+ imagePath,
220
+ imageType: "phoneScreenshots",
221
+ language: locale,
222
+ });
223
+ console.error(`[GooglePlay] ✅ ${imagePath.split("/").pop()} (as phone)`);
224
+ }
225
+ // Upload 10-inch tablet screenshots as tablet (optional)
226
+ if (screenshots.tablet10.length > 0) {
227
+ for (const imagePath of screenshots.tablet10) {
228
+ await client.uploadScreenshot({
229
+ imagePath,
230
+ imageType: "tenInchScreenshots",
231
+ language: locale,
232
+ });
233
+ console.error(`[GooglePlay] ✅ ${imagePath.split("/").pop()} (as tablet)`);
234
+ }
235
+ }
236
+ // Upload feature graphic (optional)
237
+ if (screenshots.featureGraphic) {
238
+ await client.uploadScreenshot({
239
+ imagePath: screenshots.featureGraphic,
240
+ imageType: "featureGraphic",
241
+ language: locale,
242
+ });
243
+ console.error(`[GooglePlay] ✅ feature-graphic.png`);
244
+ }
245
+ uploadedLocales.push(locale);
246
+ console.error(`[GooglePlay] ✅ Screenshots uploaded for ${locale}`);
247
+ }
248
+ catch (error) {
249
+ console.error(`[GooglePlay] ❌ Failed to upload screenshots for ${locale}: ${error instanceof Error ? error.message : String(error)}`);
250
+ failedLocales.push(locale);
251
+ }
252
+ }
253
+ console.error(`[GooglePlay] 📊 Screenshot upload summary: ${uploadedLocales.length} succeeded, ${skippedLocales.length} skipped, ${failedLocales.length} failed`);
254
+ if (uploadedLocales.length > 0) {
255
+ console.error(`[GooglePlay] ✅ Uploaded: ${uploadedLocales.join(", ")}`);
256
+ }
257
+ if (skippedLocales.length > 0) {
258
+ console.error(`[GooglePlay] ⏭️ Skipped: ${skippedLocales.join(", ")}`);
259
+ }
260
+ if (failedLocales.length > 0) {
261
+ console.error(`[GooglePlay] ❌ Failed: ${failedLocales.join(", ")}`);
262
+ }
263
+ }
171
264
  try {
172
265
  const updated = updateRegisteredLocales(ensuredPackage, "googlePlay", localesToPush);
173
266
  if (updated) {
@@ -17,6 +17,7 @@ export interface GooglePlayAsoData {
17
17
  screenshots: GooglePlayScreenshots;
18
18
  featureGraphic?: string;
19
19
  promoGraphic?: string;
20
+ video?: string;
20
21
  category?: string;
21
22
  contentRating?: string;
22
23
  keywords?: string[];
@@ -29,6 +30,9 @@ export interface GooglePlayAsoData {
29
30
  export interface GooglePlayMultilingualAsoData {
30
31
  locales: Record<string, GooglePlayAsoData>;
31
32
  defaultLocale?: string;
33
+ contactEmail?: string;
34
+ contactWebsite?: string;
35
+ youtubeUrl?: string;
32
36
  }
33
37
  export interface GooglePlayReleaseNote {
34
38
  versionCode: number;
@@ -72,6 +76,11 @@ export interface AppStoreAsoData {
72
76
  export interface AppStoreMultilingualAsoData {
73
77
  locales: Record<string, AppStoreAsoData>;
74
78
  defaultLocale?: string;
79
+ contactEmail?: string;
80
+ supportUrl?: string;
81
+ marketingUrl?: string;
82
+ privacyPolicyUrl?: string;
83
+ termsUrl?: string;
75
84
  }
76
85
  export interface AppStoreReleaseNote {
77
86
  versionString: string;
@@ -72,12 +72,18 @@ export function prepareAsoDataForPush(slug, configData, options) {
72
72
  const locales = isGooglePlayMultilingual(googlePlayData)
73
73
  ? googlePlayData.locales
74
74
  : { [googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData };
75
+ // Get app-level youtubeUrl if available
76
+ const appLevelYoutubeUrl = isGooglePlayMultilingual(googlePlayData)
77
+ ? googlePlayData.youtubeUrl
78
+ : undefined;
75
79
  const cleanedLocales = {};
76
80
  for (const [locale, localeData] of Object.entries(locales)) {
77
81
  const { screenshots, featureGraphic, ...rest } = localeData;
78
82
  cleanedLocales[locale] = {
79
83
  ...rest,
80
84
  contactWebsite: detailPageUrl,
85
+ // Apply app-level youtubeUrl to all locales if not already set
86
+ video: rest.video || appLevelYoutubeUrl,
81
87
  };
82
88
  }
83
89
  const gpDefaultLocale = isGooglePlayMultilingual(googlePlayData)
@@ -86,6 +92,11 @@ export function prepareAsoDataForPush(slug, configData, options) {
86
92
  storeData.googlePlay = {
87
93
  locales: cleanedLocales,
88
94
  defaultLocale: gpDefaultLocale || DEFAULT_LOCALE,
95
+ contactEmail: isGooglePlayMultilingual(googlePlayData)
96
+ ? googlePlayData.contactEmail
97
+ : undefined,
98
+ contactWebsite: detailPageUrl,
99
+ youtubeUrl: appLevelYoutubeUrl,
89
100
  };
90
101
  }
91
102
  if (configData.appStore) {
@@ -70,6 +70,35 @@ export declare class AppStoreClient {
70
70
  private createAppStoreVersionLocalization;
71
71
  private listScreenshotSets;
72
72
  private listScreenshots;
73
+ /**
74
+ * Upload a screenshot to App Store Connect
75
+ * Full implementation with 4-step process:
76
+ * 1. Find or create Screenshot Set for display type and locale
77
+ * 2. Create AppScreenshot with upload operation
78
+ * 3. Upload file to reserved URL
79
+ * 4. Commit upload operation
80
+ */
81
+ uploadScreenshot(options: {
82
+ imagePath: string;
83
+ screenshotDisplayType: string;
84
+ locale: string;
85
+ }): Promise<void>;
86
+ /**
87
+ * Find or create Screenshot Set for a specific display type
88
+ */
89
+ private findOrCreateScreenshotSet;
90
+ /**
91
+ * Create AppScreenshot with upload operation
92
+ */
93
+ private createAppScreenshot;
94
+ /**
95
+ * Upload file to reserved URL
96
+ */
97
+ private uploadFileToUrl;
98
+ /**
99
+ * Commit AppScreenshot after upload
100
+ */
101
+ private commitAppScreenshot;
73
102
  private getApi;
74
103
  private normalizeEndpoint;
75
104
  private requestCollection;
@@ -5,7 +5,7 @@
5
5
  * API Documentation: https://developer.apple.com/documentation/appstoreconnectapi
6
6
  */
7
7
  import { AppStoreConnectAPI } from "appstore-connect-sdk";
8
- import { AppsApi, AppInfosApi, AppInfoLocalizationsApi, AppScreenshotSetsApi, AppStoreVersionLocalizationsApi, AppStoreVersionsApi, ResponseError, } from "appstore-connect-sdk/openapi";
8
+ import { AppsApi, AppInfosApi, AppInfoLocalizationsApi, AppScreenshotSetsApi, AppScreenshotsApi, AppStoreVersionLocalizationsApi, AppStoreVersionsApi, ResponseError, } from "appstore-connect-sdk/openapi";
9
9
  import { DEFAULT_LOCALE } from "../../../packages/configs/aso-config/constants.js";
10
10
  import { convertToAsoData, convertToMultilingualAsoData, convertToReleaseNote, fetchScreenshotsForLocalization, mapLocalizationsByLocale, selectEnglishAppName, sortReleaseNotes, sortVersions, } from "./api-converters.js";
11
11
  import { APP_STORE_API_BASE_URL, APP_STORE_PLATFORM, DEFAULT_APP_LIST_LIMIT, DEFAULT_VERSIONS_FETCH_LIMIT, } from "./constants.js";
@@ -565,6 +565,194 @@ export class AppStoreClient {
565
565
  return await this.handleSdkError(error);
566
566
  }
567
567
  }
568
+ /**
569
+ * Upload a screenshot to App Store Connect
570
+ * Full implementation with 4-step process:
571
+ * 1. Find or create Screenshot Set for display type and locale
572
+ * 2. Create AppScreenshot with upload operation
573
+ * 3. Upload file to reserved URL
574
+ * 4. Commit upload operation
575
+ */
576
+ async uploadScreenshot(options) {
577
+ const { imagePath, screenshotDisplayType, locale } = options;
578
+ const { readFileSync, statSync } = await import("node:fs");
579
+ const { basename } = await import("node:path");
580
+ try {
581
+ // Get app and version info
582
+ const appId = await this.findAppId();
583
+ const versionsResponse = await this.listAppStoreVersions(appId, {
584
+ platform: APP_STORE_PLATFORM,
585
+ limit: DEFAULT_VERSIONS_FETCH_LIMIT,
586
+ });
587
+ const version = sortVersions(versionsResponse.data || [])[0];
588
+ if (!version)
589
+ throw new Error("App Store version not found.");
590
+ // Find localization for this locale
591
+ const localizationsResponse = await this.listAppStoreVersionLocalizations(version.id, locale);
592
+ let localizationId;
593
+ if (localizationsResponse.data?.[0]) {
594
+ localizationId = localizationsResponse.data[0].id;
595
+ }
596
+ else {
597
+ // Create localization if it doesn't exist
598
+ const createResponse = await this.createAppStoreVersionLocalization(version.id, locale, {});
599
+ localizationId = createResponse.data.id;
600
+ }
601
+ // Step 1: Find or create Screenshot Set
602
+ const screenshotSetId = await this.findOrCreateScreenshotSet(localizationId, screenshotDisplayType);
603
+ // Get file info
604
+ const fileBuffer = readFileSync(imagePath);
605
+ const fileSize = statSync(imagePath).size;
606
+ const fileName = basename(imagePath);
607
+ // Step 2: Create AppScreenshot with upload operation
608
+ const screenshot = await this.createAppScreenshot(screenshotSetId, fileName, fileSize);
609
+ // Step 3: Upload file to reserved URL
610
+ if (screenshot.uploadOperations &&
611
+ screenshot.uploadOperations.length > 0) {
612
+ const uploadOp = screenshot.uploadOperations[0];
613
+ await this.uploadFileToUrl(uploadOp.url, fileBuffer, uploadOp.method);
614
+ }
615
+ // Step 4: Commit screenshot
616
+ await this.commitAppScreenshot(screenshot.id);
617
+ }
618
+ catch (error) {
619
+ const msg = error instanceof Error ? error.message : String(error);
620
+ console.error(`[AppStore] ❌ Screenshot upload failed for ${basename(imagePath)}: ${msg}`);
621
+ throw error;
622
+ }
623
+ }
624
+ /**
625
+ * Find or create Screenshot Set for a specific display type
626
+ */
627
+ async findOrCreateScreenshotSet(localizationId, screenshotDisplayType) {
628
+ // List existing screenshot sets
629
+ const setsResponse = await this.listScreenshotSets(localizationId);
630
+ const existingSet = (setsResponse.data || []).find((set) => set.attributes?.screenshotDisplayType === screenshotDisplayType);
631
+ if (existingSet) {
632
+ return existingSet.id;
633
+ }
634
+ // Create new screenshot set
635
+ const appScreenshotSetsApi = await this.getApi(AppScreenshotSetsApi);
636
+ try {
637
+ const response = await appScreenshotSetsApi.appScreenshotSetsCreateInstance({
638
+ appScreenshotSetCreateRequest: {
639
+ data: {
640
+ type: "appScreenshotSets",
641
+ attributes: {
642
+ screenshotDisplayType: screenshotDisplayType,
643
+ },
644
+ relationships: {
645
+ appStoreVersionLocalization: {
646
+ data: {
647
+ type: "appStoreVersionLocalizations",
648
+ id: localizationId,
649
+ },
650
+ },
651
+ },
652
+ },
653
+ },
654
+ });
655
+ return response.data.id;
656
+ }
657
+ catch (error) {
658
+ return await this.handleSdkError(error);
659
+ }
660
+ }
661
+ /**
662
+ * Create AppScreenshot with upload operation
663
+ */
664
+ async createAppScreenshot(screenshotSetId, fileName, fileSize) {
665
+ const appScreenshotsApi = await this.getApi(AppScreenshotsApi);
666
+ try {
667
+ const response = await appScreenshotsApi.appScreenshotsCreateInstance({
668
+ appScreenshotCreateRequest: {
669
+ data: {
670
+ type: "appScreenshots",
671
+ attributes: {
672
+ fileName,
673
+ fileSize,
674
+ },
675
+ relationships: {
676
+ appScreenshotSet: {
677
+ data: {
678
+ type: "appScreenshotSets",
679
+ id: screenshotSetId,
680
+ },
681
+ },
682
+ },
683
+ },
684
+ },
685
+ });
686
+ const screenshot = response.data;
687
+ const uploadOps = screenshot.attributes?.uploadOperations || [];
688
+ return {
689
+ id: screenshot.id,
690
+ uploadOperations: uploadOps.map((op) => ({
691
+ url: op.url,
692
+ method: op.method || "PUT",
693
+ length: op.length,
694
+ offset: op.offset,
695
+ })),
696
+ };
697
+ }
698
+ catch (error) {
699
+ return await this.handleSdkError(error);
700
+ }
701
+ }
702
+ /**
703
+ * Upload file to reserved URL
704
+ */
705
+ async uploadFileToUrl(url, fileBuffer, method = "PUT") {
706
+ const https = await import("node:https");
707
+ const { URL } = await import("node:url");
708
+ return new Promise((resolve, reject) => {
709
+ const parsedUrl = new URL(url);
710
+ const options = {
711
+ hostname: parsedUrl.hostname,
712
+ port: parsedUrl.port,
713
+ path: parsedUrl.pathname + parsedUrl.search,
714
+ method,
715
+ headers: {
716
+ "Content-Type": "image/png",
717
+ "Content-Length": fileBuffer.length,
718
+ },
719
+ };
720
+ const req = https.request(options, (res) => {
721
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
722
+ resolve();
723
+ }
724
+ else {
725
+ reject(new Error(`Upload failed with status ${res.statusCode}`));
726
+ }
727
+ });
728
+ req.on("error", reject);
729
+ req.write(fileBuffer);
730
+ req.end();
731
+ });
732
+ }
733
+ /**
734
+ * Commit AppScreenshot after upload
735
+ */
736
+ async commitAppScreenshot(screenshotId) {
737
+ const appScreenshotsApi = await this.getApi(AppScreenshotsApi);
738
+ try {
739
+ await appScreenshotsApi.appScreenshotsUpdateInstance({
740
+ id: screenshotId,
741
+ appScreenshotUpdateRequest: {
742
+ data: {
743
+ type: "appScreenshots",
744
+ id: screenshotId,
745
+ attributes: {
746
+ uploaded: true,
747
+ },
748
+ },
749
+ },
750
+ });
751
+ }
752
+ catch (error) {
753
+ return await this.handleSdkError(error);
754
+ }
755
+ }
568
756
  async getApi(apiClass) {
569
757
  if (!this.apiCache.has(apiClass)) {
570
758
  this.apiCache.set(apiClass, this.sdk.create(apiClass));
@@ -31,6 +31,7 @@ export declare function buildListingRequestBody(data: {
31
31
  title?: string;
32
32
  shortDescription?: string;
33
33
  fullDescription?: string;
34
+ video?: string;
34
35
  }): ListingUpdateAttributes;
35
36
  /**
36
37
  * Build request body for app details update (only defined values)
@@ -121,6 +121,8 @@ export function buildListingRequestBody(data) {
121
121
  body.shortDescription = data.shortDescription;
122
122
  if (data.fullDescription)
123
123
  body.fullDescription = data.fullDescription;
124
+ if (data.video)
125
+ body.video = data.video;
124
126
  return body;
125
127
  }
126
128
  /**
@@ -138,11 +138,15 @@ export class GooglePlayClient {
138
138
  };
139
139
  try {
140
140
  const language = data.defaultLanguage || DEFAULT_LANGUAGE;
141
- if (data.title || data.shortDescription || data.fullDescription) {
141
+ if (data.title ||
142
+ data.shortDescription ||
143
+ data.fullDescription ||
144
+ data.video) {
142
145
  const listingBody = buildListingRequestBody({
143
146
  title: data.title,
144
147
  shortDescription: data.shortDescription,
145
148
  fullDescription: data.fullDescription,
149
+ video: data.video,
146
150
  });
147
151
  console.error(`[GooglePlayClient] Updating listing for ${language}:`, JSON.stringify(listingBody, null, 2));
148
152
  try {
@@ -216,11 +220,13 @@ export class GooglePlayClient {
216
220
  for (const [language, localeData] of Object.entries(data.locales)) {
217
221
  if (localeData.title ||
218
222
  localeData.shortDescription ||
219
- localeData.fullDescription) {
223
+ localeData.fullDescription ||
224
+ localeData.video) {
220
225
  const listingBody = buildListingRequestBody({
221
226
  title: localeData.title,
222
227
  shortDescription: localeData.shortDescription,
223
228
  fullDescription: localeData.fullDescription,
229
+ video: localeData.video,
224
230
  });
225
231
  console.error(`[GooglePlayClient] Updating listing for ${language}...`);
226
232
  try {
@@ -295,6 +301,8 @@ export class GooglePlayClient {
295
301
  console.error(`[GooglePlayClient] No app details to update, skipping`);
296
302
  return;
297
303
  }
304
+ // Note: youtubeUrl is not part of AppDetails API
305
+ // YouTube URLs are managed at listing level (see pushMultilingualAsoData)
298
306
  const authClient = await this.auth.getClient();
299
307
  const editResponse = await this.createEdit(authClient, this.packageName);
300
308
  const editId = editResponse.data.id;
@@ -120,6 +120,7 @@ export interface UpdateReleaseNotesOptions {
120
120
  * App Details Data
121
121
  * Internal type for app details update operations
122
122
  * Based on AppDetails but with optional fields for partial updates
123
+ * Note: YouTube URLs are managed at listing level, not app details level
123
124
  */
124
125
  export interface AppDetailsData {
125
126
  contactEmail?: NonNullable<AppDetails["contactEmail"]>;
@@ -159,6 +159,8 @@ export async function handleAsoPush(options) {
159
159
  packageName,
160
160
  localAsoData,
161
161
  googlePlayDataPath,
162
+ uploadImages,
163
+ slug,
162
164
  });
163
165
  results.push(formatPushResult("Google Play", result));
164
166
  }
@@ -174,6 +176,8 @@ export async function handleAsoPush(options) {
174
176
  bundleId,
175
177
  localAsoData,
176
178
  appStoreDataPath,
179
+ uploadImages,
180
+ slug,
177
181
  });
178
182
  results.push(formatPushResult("App Store", appStoreResult));
179
183
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-store-api-mcp",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "MCP server for App Store / Play Store ASO workflows",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",