pabal-store-api-mcp 1.2.2 → 1.3.0
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.
- package/dist/src/core/helpers/screenshot-helpers.d.ts +69 -0
- package/dist/src/core/helpers/screenshot-helpers.js +122 -0
- package/dist/src/core/services/app-store-service.d.ts +3 -1
- package/dist/src/core/services/app-store-service.js +45 -1
- package/dist/src/core/services/google-play-service.d.ts +3 -1
- package/dist/src/core/services/google-play-service.js +63 -1
- package/dist/src/packages/configs/aso-config/types.d.ts +7 -0
- package/dist/src/packages/stores/app-store/client.d.ts +29 -0
- package/dist/src/packages/stores/app-store/client.js +189 -1
- package/dist/src/tools/aso/push.js +4 -0
- package/package.json +1 -1
|
@@ -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,50 @@ 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
|
+
for (const locale of localesToPush) {
|
|
205
|
+
if (!hasScreenshots(screenshotsBaseDir, locale)) {
|
|
206
|
+
console.error(`[AppStore] ⏭️ Skipping ${locale} - no screenshots directory`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
console.error(`[AppStore] 📤 Uploading screenshots for ${locale}...`);
|
|
210
|
+
const result = parseAppStoreScreenshots(screenshotsBaseDir, locale);
|
|
211
|
+
// Report parsing issues
|
|
212
|
+
if (result.invalid.length > 0) {
|
|
213
|
+
console.error(`[AppStore] ⚠️ Invalid filenames: ${result.invalid.join(", ")}`);
|
|
214
|
+
}
|
|
215
|
+
if (result.unknown.length > 0) {
|
|
216
|
+
console.error(`[AppStore] ⚠️ Unknown device types: ${result.unknown.join(", ")}`);
|
|
217
|
+
}
|
|
218
|
+
// Upload screenshots for each device type
|
|
219
|
+
for (const [displayType, screenshots] of Object.entries(result.valid)) {
|
|
220
|
+
console.error(`[AppStore] 📱 Uploading ${screenshots.length} screenshots for ${displayType}...`);
|
|
221
|
+
for (const screenshot of screenshots) {
|
|
222
|
+
try {
|
|
223
|
+
await client.uploadScreenshot({
|
|
224
|
+
imagePath: screenshot.path,
|
|
225
|
+
screenshotDisplayType: displayType,
|
|
226
|
+
locale,
|
|
227
|
+
});
|
|
228
|
+
console.error(`[AppStore] ✅ ${screenshot.filename}`);
|
|
229
|
+
}
|
|
230
|
+
catch (uploadError) {
|
|
231
|
+
const msg = uploadError instanceof Error
|
|
232
|
+
? uploadError.message
|
|
233
|
+
: String(uploadError);
|
|
234
|
+
console.error(`[AppStore] ❌ ${screenshot.filename}: ${msg}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
console.error(`[AppStore] ✅ Screenshots uploaded for ${locale}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
197
241
|
try {
|
|
198
242
|
const updated = updateRegisteredLocales(ensuredBundleId, "appStore", localesToPush);
|
|
199
243
|
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,68 @@ 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
|
+
// Upload screenshots if enabled
|
|
181
|
+
if (uploadImages && slug) {
|
|
182
|
+
console.error(`[GooglePlay] 📤 Uploading screenshots...`);
|
|
183
|
+
const { getAsoPushDir } = await import("../../packages/configs/aso-config/utils.js");
|
|
184
|
+
const { parseGooglePlayScreenshots, hasScreenshots } = await import("../../core/helpers/screenshot-helpers.js");
|
|
185
|
+
const pushDataDir = getAsoPushDir();
|
|
186
|
+
const screenshotsBaseDir = `${pushDataDir}/products/${slug}/store/google-play/screenshots`;
|
|
187
|
+
for (const locale of localesToPush) {
|
|
188
|
+
if (!hasScreenshots(screenshotsBaseDir, locale)) {
|
|
189
|
+
console.error(`[GooglePlay] ⏭️ Skipping ${locale} - no screenshots directory`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
console.error(`[GooglePlay] 📤 Uploading screenshots for ${locale}...`);
|
|
193
|
+
const screenshots = parseGooglePlayScreenshots(screenshotsBaseDir, locale);
|
|
194
|
+
// Upload phone screenshots (phone-*.png)
|
|
195
|
+
for (const imagePath of screenshots.phone) {
|
|
196
|
+
await client.uploadScreenshot({
|
|
197
|
+
imagePath,
|
|
198
|
+
imageType: "phoneScreenshots",
|
|
199
|
+
language: locale,
|
|
200
|
+
});
|
|
201
|
+
console.error(`[GooglePlay] ✅ ${imagePath.split("/").pop()}`);
|
|
202
|
+
}
|
|
203
|
+
// Upload 7-inch tablet screenshots as phone
|
|
204
|
+
for (const imagePath of screenshots.tablet7) {
|
|
205
|
+
await client.uploadScreenshot({
|
|
206
|
+
imagePath,
|
|
207
|
+
imageType: "phoneScreenshots",
|
|
208
|
+
language: locale,
|
|
209
|
+
});
|
|
210
|
+
console.error(`[GooglePlay] ✅ ${imagePath.split("/").pop()} (as phone)`);
|
|
211
|
+
}
|
|
212
|
+
// Upload 10-inch tablet screenshots as tablet
|
|
213
|
+
for (const imagePath of screenshots.tablet10) {
|
|
214
|
+
await client.uploadScreenshot({
|
|
215
|
+
imagePath,
|
|
216
|
+
imageType: "tenInchScreenshots",
|
|
217
|
+
language: locale,
|
|
218
|
+
});
|
|
219
|
+
console.error(`[GooglePlay] ✅ ${imagePath.split("/").pop()} (as tablet)`);
|
|
220
|
+
}
|
|
221
|
+
// Upload feature graphic
|
|
222
|
+
if (screenshots.featureGraphic) {
|
|
223
|
+
await client.uploadScreenshot({
|
|
224
|
+
imagePath: screenshots.featureGraphic,
|
|
225
|
+
imageType: "featureGraphic",
|
|
226
|
+
language: locale,
|
|
227
|
+
});
|
|
228
|
+
console.error(`[GooglePlay] ✅ feature-graphic.png`);
|
|
229
|
+
}
|
|
230
|
+
console.error(`[GooglePlay] ✅ Screenshots uploaded for ${locale}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
171
233
|
try {
|
|
172
234
|
const updated = updateRegisteredLocales(ensuredPackage, "googlePlay", localesToPush);
|
|
173
235
|
if (updated) {
|
|
@@ -29,6 +29,8 @@ export interface GooglePlayAsoData {
|
|
|
29
29
|
export interface GooglePlayMultilingualAsoData {
|
|
30
30
|
locales: Record<string, GooglePlayAsoData>;
|
|
31
31
|
defaultLocale?: string;
|
|
32
|
+
contactEmail?: string;
|
|
33
|
+
contactWebsite?: string;
|
|
32
34
|
}
|
|
33
35
|
export interface GooglePlayReleaseNote {
|
|
34
36
|
versionCode: number;
|
|
@@ -72,6 +74,11 @@ export interface AppStoreAsoData {
|
|
|
72
74
|
export interface AppStoreMultilingualAsoData {
|
|
73
75
|
locales: Record<string, AppStoreAsoData>;
|
|
74
76
|
defaultLocale?: string;
|
|
77
|
+
contactEmail?: string;
|
|
78
|
+
supportUrl?: string;
|
|
79
|
+
marketingUrl?: string;
|
|
80
|
+
privacyPolicyUrl?: string;
|
|
81
|
+
termsUrl?: string;
|
|
75
82
|
}
|
|
76
83
|
export interface AppStoreReleaseNote {
|
|
77
84
|
versionString: string;
|
|
@@ -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));
|
|
@@ -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
|
}
|