pabal-store-api-mcp 1.1.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/README.md +95 -0
- package/bin/pabal-mcp.js +6 -0
- package/dist/src/core/clients/app-store-factory.d.ts +29 -0
- package/dist/src/core/clients/app-store-factory.js +72 -0
- package/dist/src/core/clients/client-factory-helpers.d.ts +7 -0
- package/dist/src/core/clients/client-factory-helpers.js +10 -0
- package/dist/src/core/clients/google-play-factory.d.ts +29 -0
- package/dist/src/core/clients/google-play-factory.js +72 -0
- package/dist/src/core/clients/types.d.ts +8 -0
- package/dist/src/core/clients/types.js +1 -0
- package/dist/src/core/helpers/formatters.d.ts +3 -0
- package/dist/src/core/helpers/formatters.js +38 -0
- package/dist/src/core/helpers/registration.d.ts +21 -0
- package/dist/src/core/helpers/registration.js +21 -0
- package/dist/src/core/helpers/translate-release-notes.d.ts +46 -0
- package/dist/src/core/helpers/translate-release-notes.js +87 -0
- package/dist/src/core/services/app-resolution-service.d.ts +14 -0
- package/dist/src/core/services/app-resolution-service.js +35 -0
- package/dist/src/core/services/app-store-service.d.ts +41 -0
- package/dist/src/core/services/app-store-service.js +266 -0
- package/dist/src/core/services/google-play-service.d.ts +36 -0
- package/dist/src/core/services/google-play-service.js +203 -0
- package/dist/src/core/services/service-helpers.d.ts +15 -0
- package/dist/src/core/services/service-helpers.js +31 -0
- package/dist/src/core/services/types.d.ts +81 -0
- package/dist/src/core/services/types.js +1 -0
- package/dist/src/core/workflows/version-info.d.ts +29 -0
- package/dist/src/core/workflows/version-info.js +100 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +279 -0
- package/dist/src/packages/common/errors/app-error.d.ts +39 -0
- package/dist/src/packages/common/errors/app-error.js +134 -0
- package/dist/src/packages/common/errors/error-codes.d.ts +63 -0
- package/dist/src/packages/common/errors/error-codes.js +71 -0
- package/dist/src/packages/common/errors/status-codes.d.ts +10 -0
- package/dist/src/packages/common/errors/status-codes.js +9 -0
- package/dist/src/packages/configs/aso-config/constants.d.ts +14 -0
- package/dist/src/packages/configs/aso-config/constants.js +102 -0
- package/dist/src/packages/configs/aso-config/locale-guards.d.ts +3 -0
- package/dist/src/packages/configs/aso-config/locale-guards.js +7 -0
- package/dist/src/packages/configs/aso-config/store.d.ts +11 -0
- package/dist/src/packages/configs/aso-config/store.js +11 -0
- package/dist/src/packages/configs/aso-config/types.d.ts +98 -0
- package/dist/src/packages/configs/aso-config/types.js +2 -0
- package/dist/src/packages/configs/aso-config/utils.d.ts +43 -0
- package/dist/src/packages/configs/aso-config/utils.js +223 -0
- package/dist/src/packages/configs/secrets-config/config.d.ts +12 -0
- package/dist/src/packages/configs/secrets-config/config.js +187 -0
- package/dist/src/packages/configs/secrets-config/constants.d.ts +1 -0
- package/dist/src/packages/configs/secrets-config/constants.js +1 -0
- package/dist/src/packages/configs/secrets-config/errors.d.ts +9 -0
- package/dist/src/packages/configs/secrets-config/errors.js +15 -0
- package/dist/src/packages/configs/secrets-config/registered-apps.d.ts +52 -0
- package/dist/src/packages/configs/secrets-config/registered-apps.js +108 -0
- package/dist/src/packages/configs/secrets-config/schemas.d.ts +21 -0
- package/dist/src/packages/configs/secrets-config/schemas.js +9 -0
- package/dist/src/packages/configs/secrets-config/types.d.ts +8 -0
- package/dist/src/packages/configs/secrets-config/types.js +1 -0
- package/dist/src/packages/stores/app-store/api-converters.d.ts +26 -0
- package/dist/src/packages/stores/app-store/api-converters.js +131 -0
- package/dist/src/packages/stores/app-store/api-endpoints.d.ts +33 -0
- package/dist/src/packages/stores/app-store/api-endpoints.js +157 -0
- package/dist/src/packages/stores/app-store/auth.d.ts +12 -0
- package/dist/src/packages/stores/app-store/auth.js +36 -0
- package/dist/src/packages/stores/app-store/client.d.ts +78 -0
- package/dist/src/packages/stores/app-store/client.js +637 -0
- package/dist/src/packages/stores/app-store/constants.d.ts +11 -0
- package/dist/src/packages/stores/app-store/constants.js +38 -0
- package/dist/src/packages/stores/app-store/generated-types.d.ts +118537 -0
- package/dist/src/packages/stores/app-store/generated-types.js +5 -0
- package/dist/src/packages/stores/app-store/types.d.ts +39 -0
- package/dist/src/packages/stores/app-store/types.js +9 -0
- package/dist/src/packages/stores/app-store/verify-auth.d.ts +16 -0
- package/dist/src/packages/stores/app-store/verify-auth.js +34 -0
- package/dist/src/packages/stores/play-store/api-converters.d.ts +58 -0
- package/dist/src/packages/stores/play-store/api-converters.js +209 -0
- package/dist/src/packages/stores/play-store/api-endpoints.d.ts +68 -0
- package/dist/src/packages/stores/play-store/api-endpoints.js +145 -0
- package/dist/src/packages/stores/play-store/client.d.ts +55 -0
- package/dist/src/packages/stores/play-store/client.js +628 -0
- package/dist/src/packages/stores/play-store/constants.d.ts +10 -0
- package/dist/src/packages/stores/play-store/constants.js +17 -0
- package/dist/src/packages/stores/play-store/types.d.ts +146 -0
- package/dist/src/packages/stores/play-store/types.js +9 -0
- package/dist/src/packages/stores/play-store/verify-auth.d.ts +13 -0
- package/dist/src/packages/stores/play-store/verify-auth.js +31 -0
- package/dist/src/tools/apps/add.d.ts +28 -0
- package/dist/src/tools/apps/add.js +307 -0
- package/dist/src/tools/apps/init.d.ts +58 -0
- package/dist/src/tools/apps/init.js +390 -0
- package/dist/src/tools/apps/search.d.ts +33 -0
- package/dist/src/tools/apps/search.js +147 -0
- package/dist/src/tools/aso/pull.d.ts +22 -0
- package/dist/src/tools/aso/pull.js +264 -0
- package/dist/src/tools/aso/push.d.ts +23 -0
- package/dist/src/tools/aso/push.js +189 -0
- package/dist/src/tools/auth/app-store.d.ts +9 -0
- package/dist/src/tools/auth/app-store.js +34 -0
- package/dist/src/tools/auth/check.d.ts +14 -0
- package/dist/src/tools/auth/check.js +50 -0
- package/dist/src/tools/auth/play-store.d.ts +9 -0
- package/dist/src/tools/auth/play-store.js +30 -0
- package/dist/src/tools/release/check-versions.d.ts +14 -0
- package/dist/src/tools/release/check-versions.js +65 -0
- package/dist/src/tools/release/create.d.ts +23 -0
- package/dist/src/tools/release/create.js +128 -0
- package/dist/src/tools/release/pull-notes.d.ts +22 -0
- package/dist/src/tools/release/pull-notes.js +151 -0
- package/dist/src/tools/release/update-notes.d.ts +110 -0
- package/dist/src/tools/release/update-notes.js +537 -0
- package/package.json +71 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Play Console API Client
|
|
3
|
+
*
|
|
4
|
+
* Authentication: Service account JSON key file required
|
|
5
|
+
* API Documentation: https://developers.google.com/android-publisher
|
|
6
|
+
*/
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { google } from "googleapis";
|
|
10
|
+
import { DEFAULT_LOCALE } from "../../../packages/configs/aso-config/constants.js";
|
|
11
|
+
import { fetchScreenshotsAndFeatureGraphic, convertToAsoData, convertToLocaleAsoData, convertToMultilingualAsoData, buildListingRequestBody, buildDetailsRequestBody, convertToReleaseNote, convertReleaseNotesToApiFormat, extractLatestRelease, } from "./api-converters.js";
|
|
12
|
+
import { DEFAULT_TRACK, DEFAULT_LANGUAGE, DEFAULT_RELEASE_STATUS, } from "./constants.js";
|
|
13
|
+
export class GooglePlayClient {
|
|
14
|
+
auth;
|
|
15
|
+
androidPublisher;
|
|
16
|
+
packageName;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.packageName = config.packageName;
|
|
19
|
+
let key = config.serviceAccountKey;
|
|
20
|
+
if (config.serviceAccountKeyPath && !key) {
|
|
21
|
+
const content = readFileSync(config.serviceAccountKeyPath, "utf-8");
|
|
22
|
+
key = JSON.parse(content);
|
|
23
|
+
}
|
|
24
|
+
if (!key) {
|
|
25
|
+
throw new Error("Google Play service account key is required.");
|
|
26
|
+
}
|
|
27
|
+
this.auth = new google.auth.GoogleAuth({
|
|
28
|
+
credentials: key,
|
|
29
|
+
scopes: ["https://www.googleapis.com/auth/androidpublisher"],
|
|
30
|
+
});
|
|
31
|
+
this.androidPublisher = google.androidpublisher("v3");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Verify app access (returns app information)
|
|
35
|
+
* Google Play API does not support listing apps, so only verifies access by package name
|
|
36
|
+
*/
|
|
37
|
+
async verifyAppAccess() {
|
|
38
|
+
const authClient = await this.auth.getClient();
|
|
39
|
+
const appResponse = await this.createEdit(authClient, this.packageName);
|
|
40
|
+
const editId = appResponse.data.id;
|
|
41
|
+
const session = {
|
|
42
|
+
auth: authClient,
|
|
43
|
+
packageName: this.packageName,
|
|
44
|
+
editId,
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
const appDetails = await this.getAppDetails(session);
|
|
48
|
+
const listingsResponse = await this.listListings(session);
|
|
49
|
+
const listings = listingsResponse.data.listings || [];
|
|
50
|
+
const defaultListing = listings[0];
|
|
51
|
+
const supportedLocales = listings
|
|
52
|
+
.map((l) => l.language)
|
|
53
|
+
.filter((lang) => !!lang)
|
|
54
|
+
.sort();
|
|
55
|
+
return {
|
|
56
|
+
packageName: this.packageName,
|
|
57
|
+
title: defaultListing?.title ?? undefined,
|
|
58
|
+
defaultLanguage: appDetails.data.defaultLanguage ?? undefined,
|
|
59
|
+
supportedLocales,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
try {
|
|
64
|
+
await this.deleteEdit(session);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Ignore deletion failure
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async pullAllLanguagesAsoData() {
|
|
72
|
+
const authClient = await this.auth.getClient();
|
|
73
|
+
const appResponse = await this.createEdit(authClient, this.packageName);
|
|
74
|
+
const editId = appResponse.data.id;
|
|
75
|
+
const session = {
|
|
76
|
+
auth: authClient,
|
|
77
|
+
packageName: this.packageName,
|
|
78
|
+
editId,
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
const appDetails = await this.getAppDetails(session);
|
|
82
|
+
const listingsResponse = await this.listListings(session);
|
|
83
|
+
const allListings = listingsResponse.data.listings || [];
|
|
84
|
+
console.log(`🌍 Found ${allListings.length} Google Play languages: ${allListings
|
|
85
|
+
.map((l) => l.language)
|
|
86
|
+
.join(", ")}`);
|
|
87
|
+
const locales = {};
|
|
88
|
+
let defaultLanguage;
|
|
89
|
+
for (const listing of allListings) {
|
|
90
|
+
const language = listing.language;
|
|
91
|
+
if (!language) {
|
|
92
|
+
console.warn(`⚠️ Listing has no language`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const listingDetail = await this.getListing(session, language);
|
|
96
|
+
const { screenshots, featureGraphic } = await fetchScreenshotsAndFeatureGraphic((imageType) => this.listImages(session, language, imageType), language);
|
|
97
|
+
locales[language] = convertToLocaleAsoData(listingDetail.data, appDetails.data, screenshots, featureGraphic, this.packageName, language);
|
|
98
|
+
if (!defaultLanguage && language === DEFAULT_LOCALE) {
|
|
99
|
+
defaultLanguage = language;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return convertToMultilingualAsoData(locales, defaultLanguage);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await this.deleteEdit(session);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async pullAsoData() {
|
|
109
|
+
const authClient = await this.auth.getClient();
|
|
110
|
+
const appResponse = await this.createEdit(authClient, this.packageName);
|
|
111
|
+
const editId = appResponse.data.id;
|
|
112
|
+
const session = {
|
|
113
|
+
auth: authClient,
|
|
114
|
+
packageName: this.packageName,
|
|
115
|
+
editId,
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
const appDetails = await this.getAppDetails(session);
|
|
119
|
+
const listings = await this.listListings(session);
|
|
120
|
+
const defaultListing = listings.data.listings?.[0];
|
|
121
|
+
const defaultLanguage = defaultListing?.language || DEFAULT_LANGUAGE;
|
|
122
|
+
const listing = await this.getListing(session, defaultLanguage);
|
|
123
|
+
const { screenshots, featureGraphic } = await fetchScreenshotsAndFeatureGraphic((imageType) => this.listImages(session, defaultLanguage, imageType), defaultLanguage);
|
|
124
|
+
return convertToAsoData(listing.data, appDetails.data, screenshots, featureGraphic, this.packageName, defaultLanguage);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
await this.deleteEdit(session);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async pushAsoData(data) {
|
|
131
|
+
const authClient = await this.auth.getClient();
|
|
132
|
+
const editResponse = await this.createEdit(authClient, this.packageName);
|
|
133
|
+
const editId = editResponse.data.id;
|
|
134
|
+
const session = {
|
|
135
|
+
auth: authClient,
|
|
136
|
+
packageName: this.packageName,
|
|
137
|
+
editId,
|
|
138
|
+
};
|
|
139
|
+
try {
|
|
140
|
+
const language = data.defaultLanguage || DEFAULT_LANGUAGE;
|
|
141
|
+
if (data.title || data.shortDescription || data.fullDescription) {
|
|
142
|
+
const listingBody = buildListingRequestBody({
|
|
143
|
+
title: data.title,
|
|
144
|
+
shortDescription: data.shortDescription,
|
|
145
|
+
fullDescription: data.fullDescription,
|
|
146
|
+
});
|
|
147
|
+
console.error(`[GooglePlayClient] Updating listing for ${language}:`, JSON.stringify(listingBody, null, 2));
|
|
148
|
+
try {
|
|
149
|
+
await this.updateListing(session, language, listingBody);
|
|
150
|
+
console.error(`[GooglePlayClient] ✅ Listing updated for ${language}`);
|
|
151
|
+
}
|
|
152
|
+
catch (listingError) {
|
|
153
|
+
console.error(`[GooglePlayClient] ❌ Listing update failed for ${language}`);
|
|
154
|
+
console.error(`[GooglePlayClient] Error code:`, listingError.code);
|
|
155
|
+
console.error(`[GooglePlayClient] Error message:`, listingError.message);
|
|
156
|
+
if (listingError.errors) {
|
|
157
|
+
console.error(`[GooglePlayClient] Error details:`, JSON.stringify(listingError.errors, null, 2));
|
|
158
|
+
}
|
|
159
|
+
if (listingError.response?.data) {
|
|
160
|
+
console.error(`[GooglePlayClient] Response data:`, JSON.stringify(listingError.response.data, null, 2));
|
|
161
|
+
}
|
|
162
|
+
throw listingError;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (data.contactEmail || data.contactPhone || data.contactWebsite) {
|
|
166
|
+
const detailsBody = buildDetailsRequestBody({
|
|
167
|
+
contactEmail: data.contactEmail,
|
|
168
|
+
contactPhone: data.contactPhone,
|
|
169
|
+
contactWebsite: data.contactWebsite,
|
|
170
|
+
});
|
|
171
|
+
console.error(`[GooglePlayClient] Updating details:`, JSON.stringify(detailsBody, null, 2));
|
|
172
|
+
try {
|
|
173
|
+
await this.updateAppDetails(session, detailsBody);
|
|
174
|
+
console.error(`[GooglePlayClient] ✅ Details updated`);
|
|
175
|
+
}
|
|
176
|
+
catch (detailsError) {
|
|
177
|
+
console.error(`[GooglePlayClient] ❌ Details update failed`);
|
|
178
|
+
console.error(`[GooglePlayClient] Error code:`, detailsError.code);
|
|
179
|
+
console.error(`[GooglePlayClient] Error message:`, detailsError.message);
|
|
180
|
+
if (detailsError.errors) {
|
|
181
|
+
console.error(`[GooglePlayClient] Error details:`, JSON.stringify(detailsError.errors, null, 2));
|
|
182
|
+
}
|
|
183
|
+
if (detailsError.response?.data) {
|
|
184
|
+
console.error(`[GooglePlayClient] Response data:`, JSON.stringify(detailsError.response.data, null, 2));
|
|
185
|
+
}
|
|
186
|
+
throw detailsError;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
console.error(`[GooglePlayClient] Committing edit...`);
|
|
190
|
+
await this.commitEdit(session);
|
|
191
|
+
console.error(`[GooglePlayClient] ✅ Edit committed successfully`);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.error(`[GooglePlayClient] Rolling back edit due to error...`);
|
|
195
|
+
await this.deleteEdit(session);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Push multilingual ASO data in a single edit session
|
|
201
|
+
* This prevents backendError from rapid successive commits
|
|
202
|
+
*/
|
|
203
|
+
async pushMultilingualAsoData(data) {
|
|
204
|
+
const authClient = await this.auth.getClient();
|
|
205
|
+
const editResponse = await this.createEdit(authClient, this.packageName);
|
|
206
|
+
const editId = editResponse.data.id;
|
|
207
|
+
const session = {
|
|
208
|
+
auth: authClient,
|
|
209
|
+
packageName: this.packageName,
|
|
210
|
+
editId,
|
|
211
|
+
};
|
|
212
|
+
const locales = Object.keys(data.locales);
|
|
213
|
+
console.error(`[GooglePlayClient] Starting multilingual push for ${locales.length} locale(s): ${locales.join(", ")}`);
|
|
214
|
+
try {
|
|
215
|
+
// Update listings for all languages in a single edit session
|
|
216
|
+
for (const [language, localeData] of Object.entries(data.locales)) {
|
|
217
|
+
if (localeData.title ||
|
|
218
|
+
localeData.shortDescription ||
|
|
219
|
+
localeData.fullDescription) {
|
|
220
|
+
const listingBody = buildListingRequestBody({
|
|
221
|
+
title: localeData.title,
|
|
222
|
+
shortDescription: localeData.shortDescription,
|
|
223
|
+
fullDescription: localeData.fullDescription,
|
|
224
|
+
});
|
|
225
|
+
console.error(`[GooglePlayClient] Updating listing for ${language}...`);
|
|
226
|
+
try {
|
|
227
|
+
await this.updateListing(session, language, listingBody);
|
|
228
|
+
console.error(`[GooglePlayClient] ✅ Listing prepared for ${language}`);
|
|
229
|
+
}
|
|
230
|
+
catch (listingError) {
|
|
231
|
+
console.error(`[GooglePlayClient] ❌ Listing update failed for ${language}`);
|
|
232
|
+
console.error(`[GooglePlayClient] Error code:`, listingError.code);
|
|
233
|
+
console.error(`[GooglePlayClient] Error message:`, listingError.message);
|
|
234
|
+
if (listingError.errors) {
|
|
235
|
+
console.error(`[GooglePlayClient] Error details:`, JSON.stringify(listingError.errors, null, 2));
|
|
236
|
+
}
|
|
237
|
+
throw listingError;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// NOTE: App details (contactEmail, contactPhone, contactWebsite) must be updated
|
|
242
|
+
// in a separate edit session using pushAppDetails() method.
|
|
243
|
+
// Google Play API returns 500 Internal Server Error when updating details
|
|
244
|
+
// in the same edit session as listings.
|
|
245
|
+
// Commit all changes at once
|
|
246
|
+
console.error(`[GooglePlayClient] Committing all changes...`);
|
|
247
|
+
try {
|
|
248
|
+
await this.commitEdit(session);
|
|
249
|
+
console.error(`[GooglePlayClient] ✅ All ${locales.length} locale(s) committed successfully`);
|
|
250
|
+
}
|
|
251
|
+
catch (commitError) {
|
|
252
|
+
console.error(`[GooglePlayClient] ❌ Commit failed`);
|
|
253
|
+
console.error(`[GooglePlayClient] Error code:`, commitError.code);
|
|
254
|
+
console.error(`[GooglePlayClient] Error message:`, commitError.message);
|
|
255
|
+
console.error(`[GooglePlayClient] Error status:`, commitError.status);
|
|
256
|
+
if (commitError.errors) {
|
|
257
|
+
console.error(`[GooglePlayClient] Error details:`, JSON.stringify(commitError.errors, null, 2));
|
|
258
|
+
}
|
|
259
|
+
if (commitError.response?.data) {
|
|
260
|
+
console.error(`[GooglePlayClient] Response data:`, JSON.stringify(commitError.response.data, null, 2));
|
|
261
|
+
}
|
|
262
|
+
if (commitError.response?.status) {
|
|
263
|
+
console.error(`[GooglePlayClient] Response status:`, commitError.response.status);
|
|
264
|
+
}
|
|
265
|
+
if (commitError.response?.statusText) {
|
|
266
|
+
console.error(`[GooglePlayClient] Response statusText:`, commitError.response.statusText);
|
|
267
|
+
}
|
|
268
|
+
if (commitError.response?.headers) {
|
|
269
|
+
console.error(`[GooglePlayClient] Response headers:`, JSON.stringify(commitError.response.headers, null, 2));
|
|
270
|
+
}
|
|
271
|
+
throw commitError;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
console.error(`[GooglePlayClient] Rolling back edit due to error...`);
|
|
276
|
+
console.error(`[GooglePlayClient] Full error object:`, JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
|
|
277
|
+
try {
|
|
278
|
+
await this.deleteEdit(session);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// Ignore deletion failure
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Push app details (contactEmail, contactPhone, contactWebsite) in a separate edit session
|
|
288
|
+
* This must be called separately from pushMultilingualAsoData due to Google Play API limitations
|
|
289
|
+
* NOTE: defaultLanguage is required - it will be fetched from current app details if not provided
|
|
290
|
+
*/
|
|
291
|
+
async pushAppDetails(details) {
|
|
292
|
+
if (!details.contactEmail &&
|
|
293
|
+
!details.contactPhone &&
|
|
294
|
+
!details.contactWebsite) {
|
|
295
|
+
console.error(`[GooglePlayClient] No app details to update, skipping`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const authClient = await this.auth.getClient();
|
|
299
|
+
const editResponse = await this.createEdit(authClient, this.packageName);
|
|
300
|
+
const editId = editResponse.data.id;
|
|
301
|
+
const session = {
|
|
302
|
+
auth: authClient,
|
|
303
|
+
packageName: this.packageName,
|
|
304
|
+
editId,
|
|
305
|
+
};
|
|
306
|
+
console.error(`[GooglePlayClient] Starting app details update...`);
|
|
307
|
+
try {
|
|
308
|
+
// defaultLanguage is required for details.update API
|
|
309
|
+
// If not provided, fetch current value first
|
|
310
|
+
let defaultLanguage = details.defaultLanguage;
|
|
311
|
+
if (!defaultLanguage) {
|
|
312
|
+
console.error(`[GooglePlayClient] Fetching current defaultLanguage...`);
|
|
313
|
+
const currentDetails = await this.getAppDetails(session);
|
|
314
|
+
defaultLanguage =
|
|
315
|
+
currentDetails.data.defaultLanguage || DEFAULT_LANGUAGE;
|
|
316
|
+
console.error(`[GooglePlayClient] Current defaultLanguage: ${defaultLanguage}`);
|
|
317
|
+
}
|
|
318
|
+
const detailsBody = buildDetailsRequestBody({
|
|
319
|
+
defaultLanguage: defaultLanguage,
|
|
320
|
+
contactEmail: details.contactEmail,
|
|
321
|
+
contactPhone: details.contactPhone,
|
|
322
|
+
contactWebsite: details.contactWebsite,
|
|
323
|
+
});
|
|
324
|
+
console.error(`[GooglePlayClient] Updating details:`, JSON.stringify(detailsBody, null, 2));
|
|
325
|
+
await this.updateAppDetails(session, detailsBody);
|
|
326
|
+
console.error(`[GooglePlayClient] ✅ Details prepared`);
|
|
327
|
+
console.error(`[GooglePlayClient] Committing app details...`);
|
|
328
|
+
await this.commitEdit(session);
|
|
329
|
+
console.error(`[GooglePlayClient] ✅ App details committed successfully`);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
console.error(`[GooglePlayClient] Rolling back edit due to error...`);
|
|
333
|
+
console.error(`[GooglePlayClient] Error:`, error.message);
|
|
334
|
+
if (error.errors) {
|
|
335
|
+
console.error(`[GooglePlayClient] Error details:`, JSON.stringify(error.errors, null, 2));
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
await this.deleteEdit(session);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// Ignore deletion failure
|
|
342
|
+
}
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async uploadScreenshot(options) {
|
|
347
|
+
const { imagePath, imageType, language = DEFAULT_LANGUAGE } = options;
|
|
348
|
+
if (!existsSync(imagePath)) {
|
|
349
|
+
throw new Error(`Image file not found: ${imagePath}`);
|
|
350
|
+
}
|
|
351
|
+
const authClient = await this.auth.getClient();
|
|
352
|
+
const editResponse = await this.createEdit(authClient, this.packageName);
|
|
353
|
+
const editId = editResponse.data.id;
|
|
354
|
+
const session = {
|
|
355
|
+
auth: authClient,
|
|
356
|
+
packageName: this.packageName,
|
|
357
|
+
editId,
|
|
358
|
+
};
|
|
359
|
+
try {
|
|
360
|
+
const imageBuffer = readFileSync(imagePath);
|
|
361
|
+
await this.uploadImage(session, language, imageType, {
|
|
362
|
+
mimeType: "image/png",
|
|
363
|
+
body: imageBuffer,
|
|
364
|
+
});
|
|
365
|
+
await this.commitEdit(session);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
await this.deleteEdit(session);
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async getLatestProductionRelease() {
|
|
373
|
+
const authClient = await this.auth.getClient();
|
|
374
|
+
const editResponse = await this.createEdit(authClient, this.packageName);
|
|
375
|
+
const editId = editResponse.data.id;
|
|
376
|
+
const session = {
|
|
377
|
+
auth: authClient,
|
|
378
|
+
packageName: this.packageName,
|
|
379
|
+
editId,
|
|
380
|
+
};
|
|
381
|
+
try {
|
|
382
|
+
const trackResponse = await this.getTrack(session, DEFAULT_TRACK);
|
|
383
|
+
const releases = trackResponse.data.releases || [];
|
|
384
|
+
return extractLatestRelease(releases);
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
await this.deleteEdit(session);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async createProductionRelease(options) {
|
|
391
|
+
const { versionCodes, releaseName, status = DEFAULT_RELEASE_STATUS, } = options;
|
|
392
|
+
if (!versionCodes || versionCodes.length === 0) {
|
|
393
|
+
throw new Error("At least one versionCode is required to create a release.");
|
|
394
|
+
}
|
|
395
|
+
const authClient = await this.auth.getClient();
|
|
396
|
+
const editResponse = await this.createEdit(authClient, this.packageName);
|
|
397
|
+
const editId = editResponse.data.id;
|
|
398
|
+
const session = {
|
|
399
|
+
auth: authClient,
|
|
400
|
+
packageName: this.packageName,
|
|
401
|
+
editId,
|
|
402
|
+
};
|
|
403
|
+
try {
|
|
404
|
+
await this.updateTrack(session, DEFAULT_TRACK, {
|
|
405
|
+
track: DEFAULT_TRACK,
|
|
406
|
+
releases: [
|
|
407
|
+
{
|
|
408
|
+
name: releaseName,
|
|
409
|
+
status,
|
|
410
|
+
versionCodes: versionCodes.map(String),
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
});
|
|
414
|
+
await this.commitEdit(session);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
await this.deleteEdit(session);
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Update release notes for production track
|
|
423
|
+
*/
|
|
424
|
+
async updateReleaseNotes(options) {
|
|
425
|
+
const { releaseNotes, track = DEFAULT_TRACK } = options;
|
|
426
|
+
const authClient = await this.auth.getClient();
|
|
427
|
+
const editResponse = await this.createEdit(authClient, this.packageName);
|
|
428
|
+
const editId = editResponse.data.id;
|
|
429
|
+
const session = {
|
|
430
|
+
auth: authClient,
|
|
431
|
+
packageName: this.packageName,
|
|
432
|
+
editId,
|
|
433
|
+
};
|
|
434
|
+
try {
|
|
435
|
+
// Get current track information
|
|
436
|
+
const trackResponse = await this.getTrack(session, track);
|
|
437
|
+
const releases = trackResponse.data.releases || [];
|
|
438
|
+
if (releases.length === 0) {
|
|
439
|
+
throw new Error(`No releases found in ${track} track`);
|
|
440
|
+
}
|
|
441
|
+
// Add release notes to latest release
|
|
442
|
+
const latestRelease = releases[0];
|
|
443
|
+
const releaseNotesArray = convertReleaseNotesToApiFormat(releaseNotes);
|
|
444
|
+
// Update track
|
|
445
|
+
await this.updateTrack(session, track, {
|
|
446
|
+
track,
|
|
447
|
+
releases: [
|
|
448
|
+
{
|
|
449
|
+
...latestRelease,
|
|
450
|
+
releaseNotes: releaseNotesArray,
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
});
|
|
454
|
+
// Commit
|
|
455
|
+
await this.commitEdit(session);
|
|
456
|
+
return {
|
|
457
|
+
updated: Object.keys(releaseNotes),
|
|
458
|
+
failed: [],
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
// Rollback on failure
|
|
463
|
+
try {
|
|
464
|
+
await this.deleteEdit(session);
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// Ignore deletion failure
|
|
468
|
+
}
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async pullReleaseNotes() {
|
|
473
|
+
const authClient = await this.auth.getClient();
|
|
474
|
+
const editResponse = await this.createEdit(authClient, this.packageName);
|
|
475
|
+
const editId = editResponse.data.id;
|
|
476
|
+
const session = {
|
|
477
|
+
auth: authClient,
|
|
478
|
+
packageName: this.packageName,
|
|
479
|
+
editId,
|
|
480
|
+
};
|
|
481
|
+
try {
|
|
482
|
+
const tracksResponse = await this.listTracks(session);
|
|
483
|
+
const tracks = tracksResponse.data.tracks || [];
|
|
484
|
+
const releaseNotes = [];
|
|
485
|
+
for (const track of tracks) {
|
|
486
|
+
const trackName = track.track || DEFAULT_TRACK;
|
|
487
|
+
const trackResponse = await this.getTrack(session, trackName);
|
|
488
|
+
const releases = trackResponse.data.releases || [];
|
|
489
|
+
for (const release of releases) {
|
|
490
|
+
const versionCodes = release.versionCodes || [];
|
|
491
|
+
for (const versionCode of versionCodes) {
|
|
492
|
+
const releaseNote = convertToReleaseNote(release, versionCode, trackName);
|
|
493
|
+
if (releaseNote) {
|
|
494
|
+
releaseNotes.push(releaseNote);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return releaseNotes;
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
await this.deleteEdit(session);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async pullProductionReleaseNotes() {
|
|
506
|
+
const allReleaseNotes = await this.pullReleaseNotes();
|
|
507
|
+
return allReleaseNotes.filter((rn) => rn.track === DEFAULT_TRACK);
|
|
508
|
+
}
|
|
509
|
+
async createEdit(auth, packageName) {
|
|
510
|
+
const response = await this.androidPublisher.edits.insert({
|
|
511
|
+
auth,
|
|
512
|
+
packageName,
|
|
513
|
+
});
|
|
514
|
+
return { data: response.data };
|
|
515
|
+
}
|
|
516
|
+
async deleteEdit(session) {
|
|
517
|
+
await this.androidPublisher.edits.delete({
|
|
518
|
+
auth: session.auth,
|
|
519
|
+
packageName: session.packageName,
|
|
520
|
+
editId: session.editId,
|
|
521
|
+
});
|
|
522
|
+
return { data: undefined };
|
|
523
|
+
}
|
|
524
|
+
async commitEdit(session) {
|
|
525
|
+
const response = await this.androidPublisher.edits.commit({
|
|
526
|
+
auth: session.auth,
|
|
527
|
+
packageName: session.packageName,
|
|
528
|
+
editId: session.editId,
|
|
529
|
+
});
|
|
530
|
+
return { data: response.data };
|
|
531
|
+
}
|
|
532
|
+
async getAppDetails(session) {
|
|
533
|
+
const response = await this.androidPublisher.edits.details.get({
|
|
534
|
+
auth: session.auth,
|
|
535
|
+
packageName: session.packageName,
|
|
536
|
+
editId: session.editId,
|
|
537
|
+
});
|
|
538
|
+
return { data: response.data };
|
|
539
|
+
}
|
|
540
|
+
async updateAppDetails(session, requestBody) {
|
|
541
|
+
const response = await this.androidPublisher.edits.details.update({
|
|
542
|
+
auth: session.auth,
|
|
543
|
+
packageName: session.packageName,
|
|
544
|
+
editId: session.editId,
|
|
545
|
+
requestBody,
|
|
546
|
+
});
|
|
547
|
+
return { data: response.data };
|
|
548
|
+
}
|
|
549
|
+
async listListings(session) {
|
|
550
|
+
const response = await this.androidPublisher.edits.listings.list({
|
|
551
|
+
auth: session.auth,
|
|
552
|
+
packageName: session.packageName,
|
|
553
|
+
editId: session.editId,
|
|
554
|
+
});
|
|
555
|
+
return { data: response.data };
|
|
556
|
+
}
|
|
557
|
+
async getListing(session, language) {
|
|
558
|
+
const response = await this.androidPublisher.edits.listings.get({
|
|
559
|
+
auth: session.auth,
|
|
560
|
+
packageName: session.packageName,
|
|
561
|
+
editId: session.editId,
|
|
562
|
+
language,
|
|
563
|
+
});
|
|
564
|
+
return { data: response.data };
|
|
565
|
+
}
|
|
566
|
+
async updateListing(session, language, requestBody) {
|
|
567
|
+
const response = await this.androidPublisher.edits.listings.update({
|
|
568
|
+
auth: session.auth,
|
|
569
|
+
packageName: session.packageName,
|
|
570
|
+
editId: session.editId,
|
|
571
|
+
language,
|
|
572
|
+
requestBody,
|
|
573
|
+
});
|
|
574
|
+
return { data: response.data };
|
|
575
|
+
}
|
|
576
|
+
async listImages(session, language, imageType) {
|
|
577
|
+
const response = await this.androidPublisher.edits.images.list({
|
|
578
|
+
auth: session.auth,
|
|
579
|
+
packageName: session.packageName,
|
|
580
|
+
editId: session.editId,
|
|
581
|
+
language,
|
|
582
|
+
imageType,
|
|
583
|
+
});
|
|
584
|
+
return { data: response.data };
|
|
585
|
+
}
|
|
586
|
+
async uploadImage(session, language, imageType, media) {
|
|
587
|
+
const response = await this.androidPublisher.edits.images.upload({
|
|
588
|
+
auth: session.auth,
|
|
589
|
+
packageName: session.packageName,
|
|
590
|
+
editId: session.editId,
|
|
591
|
+
language,
|
|
592
|
+
imageType,
|
|
593
|
+
media,
|
|
594
|
+
});
|
|
595
|
+
const uploadData = response.data;
|
|
596
|
+
if (!uploadData?.image) {
|
|
597
|
+
throw new Error("Image upload failed: no image data returned");
|
|
598
|
+
}
|
|
599
|
+
return { data: uploadData.image };
|
|
600
|
+
}
|
|
601
|
+
async getTrack(session, track) {
|
|
602
|
+
const response = await this.androidPublisher.edits.tracks.get({
|
|
603
|
+
auth: session.auth,
|
|
604
|
+
packageName: session.packageName,
|
|
605
|
+
editId: session.editId,
|
|
606
|
+
track,
|
|
607
|
+
});
|
|
608
|
+
return { data: response.data };
|
|
609
|
+
}
|
|
610
|
+
async updateTrack(session, track, requestBody) {
|
|
611
|
+
const response = await this.androidPublisher.edits.tracks.update({
|
|
612
|
+
auth: session.auth,
|
|
613
|
+
packageName: session.packageName,
|
|
614
|
+
editId: session.editId,
|
|
615
|
+
track,
|
|
616
|
+
requestBody,
|
|
617
|
+
});
|
|
618
|
+
return { data: response.data };
|
|
619
|
+
}
|
|
620
|
+
async listTracks(session) {
|
|
621
|
+
const response = await this.androidPublisher.edits.tracks.list({
|
|
622
|
+
auth: session.auth,
|
|
623
|
+
packageName: session.packageName,
|
|
624
|
+
editId: session.editId,
|
|
625
|
+
});
|
|
626
|
+
return { data: response.data };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Play Console API Constants
|
|
3
|
+
*
|
|
4
|
+
* Constant values for Google Play Store operations
|
|
5
|
+
*/
|
|
6
|
+
import type { ImageType } from "./types.js";
|
|
7
|
+
export declare const IMAGE_TYPES: readonly ImageType[];
|
|
8
|
+
export declare const DEFAULT_TRACK = "production";
|
|
9
|
+
export declare const DEFAULT_LANGUAGE = "en-US";
|
|
10
|
+
export declare const DEFAULT_RELEASE_STATUS = "draft";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Play Console API Constants
|
|
3
|
+
*
|
|
4
|
+
* Constant values for Google Play Store operations
|
|
5
|
+
*/
|
|
6
|
+
// Subset of ImageType enum values that are commonly used for screenshots
|
|
7
|
+
export const IMAGE_TYPES = [
|
|
8
|
+
"phoneScreenshots",
|
|
9
|
+
"sevenInchScreenshots",
|
|
10
|
+
"tenInchScreenshots",
|
|
11
|
+
"tvScreenshots",
|
|
12
|
+
"wearScreenshots",
|
|
13
|
+
"featureGraphic",
|
|
14
|
+
];
|
|
15
|
+
export const DEFAULT_TRACK = "production";
|
|
16
|
+
export const DEFAULT_LANGUAGE = "en-US";
|
|
17
|
+
export const DEFAULT_RELEASE_STATUS = "draft";
|