eas-cli 18.4.0 → 18.5.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 +92 -90
- package/build/build/android/prepareJob.js +2 -2
- package/build/build/ios/prepareJob.js +2 -2
- package/build/build/metadata.js +2 -1
- package/build/commandUtils/EasCommand.js +23 -2
- package/build/commandUtils/context/contextUtils/getProjectIdAsync.js +2 -0
- package/build/commandUtils/workflow/fetchLogs.js +11 -2
- package/build/commandUtils/workflow/types.d.ts +5 -1
- package/build/commandUtils/workflow/utils.js +22 -16
- package/build/commands/metadata/pull.d.ts +1 -0
- package/build/commands/metadata/pull.js +11 -4
- package/build/commands/metadata/push.d.ts +1 -0
- package/build/commands/metadata/push.js +11 -4
- package/build/commands/project/onboarding.js +3 -0
- package/build/commands/workflow/logs.js +12 -12
- package/build/graphql/generated.d.ts +673 -46
- package/build/graphql/generated.js +58 -20
- package/build/graphql/queries/UserQuery.js +3 -0
- package/build/graphql/types/Update.js +3 -0
- package/build/metadata/apple/config/reader.d.ts +5 -1
- package/build/metadata/apple/config/reader.js +8 -0
- package/build/metadata/apple/config/writer.d.ts +5 -1
- package/build/metadata/apple/config/writer.js +13 -0
- package/build/metadata/apple/data.d.ts +6 -2
- package/build/metadata/apple/rules/infoRestrictedWords.js +6 -1
- package/build/metadata/apple/tasks/age-rating.d.ts +1 -1
- package/build/metadata/apple/tasks/age-rating.js +19 -3
- package/build/metadata/apple/tasks/app-review-detail.js +7 -2
- package/build/metadata/apple/tasks/index.js +4 -0
- package/build/metadata/apple/tasks/previews.d.ts +18 -0
- package/build/metadata/apple/tasks/previews.js +208 -0
- package/build/metadata/apple/tasks/screenshots.d.ts +18 -0
- package/build/metadata/apple/tasks/screenshots.js +224 -0
- package/build/metadata/apple/types.d.ts +34 -1
- package/build/metadata/auth.d.ts +11 -1
- package/build/metadata/auth.js +96 -2
- package/build/metadata/download.d.ts +5 -1
- package/build/metadata/download.js +16 -8
- package/build/metadata/upload.d.ts +5 -1
- package/build/metadata/upload.js +11 -4
- package/build/project/projectUtils.d.ts +0 -2
- package/build/project/projectUtils.js +0 -12
- package/build/project/workflow.js +1 -1
- package/build/sentry.d.ts +2 -0
- package/build/sentry.js +22 -0
- package/build/update/utils.d.ts +2 -2
- package/build/update/utils.js +1 -0
- package/build/user/User.d.ts +2 -2
- package/build/user/User.js +3 -0
- package/build/user/expoBrowserAuthFlowLauncher.js +70 -13
- package/oclif.manifest.json +13 -1
- package/package.json +12 -11
- package/schema/metadata-0.json +36 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PreviewsTask = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const apple_utils_1 = require("@expo/apple-utils");
|
|
6
|
+
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
7
|
+
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
8
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
9
|
+
const fetch_1 = tslib_1.__importDefault(require("../../../fetch"));
|
|
10
|
+
const log_1 = tslib_1.__importDefault(require("../../../log"));
|
|
11
|
+
const log_2 = require("../../utils/log");
|
|
12
|
+
const task_1 = require("../task");
|
|
13
|
+
/**
|
|
14
|
+
* Normalize preview config to always return an object with path and optional previewFrameTimeCode.
|
|
15
|
+
*/
|
|
16
|
+
function normalizePreviewConfig(config) {
|
|
17
|
+
if (typeof config === 'string') {
|
|
18
|
+
return { path: config };
|
|
19
|
+
}
|
|
20
|
+
return config;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Task for managing App Store video previews.
|
|
24
|
+
* Downloads existing previews and uploads new ones based on store configuration.
|
|
25
|
+
*/
|
|
26
|
+
class PreviewsTask extends task_1.AppleTask {
|
|
27
|
+
name = () => 'video previews';
|
|
28
|
+
async prepareAsync({ context }) {
|
|
29
|
+
// Initialize the preview sets map
|
|
30
|
+
context.previewSets = new Map();
|
|
31
|
+
if (!context.versionLocales) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Fetch preview sets for all locales in parallel
|
|
35
|
+
await Promise.all(context.versionLocales.map(async (locale) => {
|
|
36
|
+
const sets = await locale.getAppPreviewSetsAsync();
|
|
37
|
+
const previewTypeMap = new Map();
|
|
38
|
+
for (const set of sets) {
|
|
39
|
+
previewTypeMap.set(set.attributes.previewType, set);
|
|
40
|
+
}
|
|
41
|
+
context.previewSets.set(locale.attributes.locale, previewTypeMap);
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
async downloadAsync({ config, context }) {
|
|
45
|
+
if (!context.previewSets || !context.versionLocales) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
for (const locale of context.versionLocales) {
|
|
49
|
+
const localeCode = locale.attributes.locale;
|
|
50
|
+
const previewTypeMap = context.previewSets.get(localeCode);
|
|
51
|
+
if (!previewTypeMap || previewTypeMap.size === 0) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const previews = {};
|
|
55
|
+
for (const [previewType, set] of previewTypeMap) {
|
|
56
|
+
const previewModels = set.attributes.appPreviews;
|
|
57
|
+
if (!previewModels || previewModels.length === 0) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// For now, we only handle the first preview per set (App Store allows up to 3)
|
|
61
|
+
// We can extend this later to support multiple previews
|
|
62
|
+
const preview = previewModels[0];
|
|
63
|
+
const relativePath = await downloadPreviewAsync(context.projectDir, localeCode, previewType, preview, 0);
|
|
64
|
+
if (relativePath) {
|
|
65
|
+
// Include preview frame time code if available
|
|
66
|
+
if (preview.attributes.previewFrameTimeCode) {
|
|
67
|
+
previews[previewType] = {
|
|
68
|
+
path: relativePath,
|
|
69
|
+
previewFrameTimeCode: preview.attributes.previewFrameTimeCode,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
previews[previewType] = relativePath;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (Object.keys(previews).length > 0) {
|
|
78
|
+
config.setPreviews(localeCode, previews);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async uploadAsync({ config, context }) {
|
|
83
|
+
if (!context.previewSets || !context.versionLocales) {
|
|
84
|
+
log_1.default.log((0, chalk_1.default) `{dim - Skipped video previews, no version available}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const locales = config.getLocales();
|
|
88
|
+
if (locales.length <= 0) {
|
|
89
|
+
log_1.default.log((0, chalk_1.default) `{dim - Skipped video previews, no locales configured}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const localeCode of locales) {
|
|
93
|
+
const previews = config.getPreviews(localeCode);
|
|
94
|
+
if (!previews || Object.keys(previews).length === 0) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
|
|
98
|
+
if (!localization) {
|
|
99
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Skipping video previews for ${localeCode} - locale not found}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (const [previewType, previewConfig] of Object.entries(previews)) {
|
|
103
|
+
if (!previewConfig) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
await syncPreviewSetAsync(context.projectDir, localization, previewType, normalizePreviewConfig(previewConfig), context.previewSets.get(localeCode));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.PreviewsTask = PreviewsTask;
|
|
112
|
+
/**
|
|
113
|
+
* Sync a preview set - upload new preview, delete old one if changed.
|
|
114
|
+
*/
|
|
115
|
+
async function syncPreviewSetAsync(projectDir, localization, previewType, previewConfig, existingSets) {
|
|
116
|
+
const locale = localization.attributes.locale;
|
|
117
|
+
const absolutePath = path_1.default.resolve(projectDir, previewConfig.path);
|
|
118
|
+
const fileName = path_1.default.basename(absolutePath);
|
|
119
|
+
if (!fs_1.default.existsSync(absolutePath)) {
|
|
120
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Video preview not found: ${absolutePath}}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Get or create the preview set
|
|
124
|
+
let previewSet = existingSets?.get(previewType);
|
|
125
|
+
if (!previewSet) {
|
|
126
|
+
previewSet = await (0, log_2.logAsync)(() => localization.createAppPreviewSetAsync({
|
|
127
|
+
previewType,
|
|
128
|
+
}), {
|
|
129
|
+
pending: `Creating preview set for ${chalk_1.default.bold(previewType)} (${locale})...`,
|
|
130
|
+
success: `Created preview set for ${chalk_1.default.bold(previewType)} (${locale})`,
|
|
131
|
+
failure: `Failed creating preview set for ${chalk_1.default.bold(previewType)} (${locale})`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const existingPreviews = previewSet.attributes.appPreviews || [];
|
|
135
|
+
// Check if we need to update (different filename, size, or no existing preview)
|
|
136
|
+
const existingPreview = existingPreviews.find(p => p.attributes.fileName === fileName);
|
|
137
|
+
const localSize = fs_1.default.statSync(absolutePath).size;
|
|
138
|
+
if (existingPreview &&
|
|
139
|
+
existingPreview.isComplete() &&
|
|
140
|
+
existingPreview.attributes.fileSize === localSize) {
|
|
141
|
+
// Preview with same filename exists, check if we need to update preview frame time code
|
|
142
|
+
if (previewConfig.previewFrameTimeCode &&
|
|
143
|
+
existingPreview.attributes.previewFrameTimeCode !== previewConfig.previewFrameTimeCode) {
|
|
144
|
+
await (0, log_2.logAsync)(() => existingPreview.updateAsync({
|
|
145
|
+
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
|
146
|
+
}), {
|
|
147
|
+
pending: `Updating preview frame time code for ${chalk_1.default.bold(fileName)} (${locale})...`,
|
|
148
|
+
success: `Updated preview frame time code for ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
149
|
+
failure: `Failed updating preview frame time code for ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
log_1.default.log((0, chalk_1.default) `{dim Preview ${fileName} already exists, skipping upload}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Delete existing previews that don't match
|
|
156
|
+
for (const preview of existingPreviews) {
|
|
157
|
+
if (preview.attributes.fileName !== fileName) {
|
|
158
|
+
await (0, log_2.logAsync)(() => preview.deleteAsync(), {
|
|
159
|
+
pending: `Deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})...`,
|
|
160
|
+
success: `Deleted old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
|
|
161
|
+
failure: `Failed deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Upload new preview
|
|
166
|
+
await (0, log_2.logAsync)(() => apple_utils_1.AppPreview.uploadAsync(localization.context, {
|
|
167
|
+
id: previewSet.id,
|
|
168
|
+
filePath: absolutePath,
|
|
169
|
+
waitForProcessing: true,
|
|
170
|
+
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
|
171
|
+
}), {
|
|
172
|
+
pending: `Uploading video preview ${chalk_1.default.bold(fileName)} (${locale})...`,
|
|
173
|
+
success: `Uploaded video preview ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
174
|
+
failure: `Failed uploading video preview ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Download a video preview to the local filesystem.
|
|
179
|
+
* Returns the relative path to the downloaded file.
|
|
180
|
+
*/
|
|
181
|
+
async function downloadPreviewAsync(projectDir, locale, previewType, preview, index) {
|
|
182
|
+
const videoUrl = preview.getVideoUrl();
|
|
183
|
+
if (!videoUrl) {
|
|
184
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Could not get download URL for preview ${preview.attributes.fileName}}`);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
// Create directory structure: store/apple/preview/{locale}/{previewType}/
|
|
188
|
+
const previewsDir = path_1.default.join(projectDir, 'store', 'apple', 'preview', locale, previewType);
|
|
189
|
+
await fs_1.default.promises.mkdir(previewsDir, { recursive: true });
|
|
190
|
+
// Use original filename for matching during sync
|
|
191
|
+
const fileName = preview.attributes.fileName || `${String(index + 1).padStart(2, '0')}.mp4`;
|
|
192
|
+
const outputPath = path_1.default.join(previewsDir, fileName);
|
|
193
|
+
const relativePath = path_1.default.relative(projectDir, outputPath);
|
|
194
|
+
try {
|
|
195
|
+
const response = await (0, fetch_1.default)(videoUrl);
|
|
196
|
+
if (!response.ok) {
|
|
197
|
+
throw new Error(`HTTP ${response.status}`);
|
|
198
|
+
}
|
|
199
|
+
const buffer = await response.buffer();
|
|
200
|
+
await fs_1.default.promises.writeFile(outputPath, buffer);
|
|
201
|
+
log_1.default.log((0, chalk_1.default) `{dim Downloaded video preview: ${relativePath}}`);
|
|
202
|
+
return relativePath;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Failed to download video preview ${fileName}: ${error.message}}`);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AppScreenshotSet, ScreenshotDisplayType } from '@expo/apple-utils';
|
|
2
|
+
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
|
3
|
+
/** Locale -> ScreenshotDisplayType -> AppScreenshotSet */
|
|
4
|
+
export type ScreenshotSetsMap = Map<string, Map<ScreenshotDisplayType, AppScreenshotSet>>;
|
|
5
|
+
export type ScreenshotsData = {
|
|
6
|
+
/** Map of locales to their screenshot sets */
|
|
7
|
+
screenshotSets: ScreenshotSetsMap;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Task for managing App Store screenshots.
|
|
11
|
+
* Downloads existing screenshots and uploads new ones based on store configuration.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ScreenshotsTask extends AppleTask {
|
|
14
|
+
name: () => string;
|
|
15
|
+
prepareAsync({ context }: TaskPrepareOptions): Promise<void>;
|
|
16
|
+
downloadAsync({ config, context }: TaskDownloadOptions): Promise<void>;
|
|
17
|
+
uploadAsync({ config, context }: TaskUploadOptions): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ScreenshotsTask = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const apple_utils_1 = require("@expo/apple-utils");
|
|
6
|
+
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
7
|
+
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
8
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
9
|
+
const fetch_1 = tslib_1.__importDefault(require("../../../fetch"));
|
|
10
|
+
const log_1 = tslib_1.__importDefault(require("../../../log"));
|
|
11
|
+
const log_2 = require("../../utils/log");
|
|
12
|
+
const task_1 = require("../task");
|
|
13
|
+
/**
|
|
14
|
+
* Task for managing App Store screenshots.
|
|
15
|
+
* Downloads existing screenshots and uploads new ones based on store configuration.
|
|
16
|
+
*/
|
|
17
|
+
class ScreenshotsTask extends task_1.AppleTask {
|
|
18
|
+
name = () => 'screenshots';
|
|
19
|
+
async prepareAsync({ context }) {
|
|
20
|
+
// Initialize the screenshot sets map
|
|
21
|
+
context.screenshotSets = new Map();
|
|
22
|
+
if (!context.versionLocales) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Fetch screenshot sets for all locales in parallel
|
|
26
|
+
await Promise.all(context.versionLocales.map(async (locale) => {
|
|
27
|
+
const sets = await locale.getAppScreenshotSetsAsync();
|
|
28
|
+
const displayTypeMap = new Map();
|
|
29
|
+
for (const set of sets) {
|
|
30
|
+
displayTypeMap.set(set.attributes.screenshotDisplayType, set);
|
|
31
|
+
}
|
|
32
|
+
context.screenshotSets.set(locale.attributes.locale, displayTypeMap);
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
async downloadAsync({ config, context }) {
|
|
36
|
+
if (!context.screenshotSets || !context.versionLocales) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const locale of context.versionLocales) {
|
|
40
|
+
const localeCode = locale.attributes.locale;
|
|
41
|
+
const displayTypeMap = context.screenshotSets.get(localeCode);
|
|
42
|
+
if (!displayTypeMap || displayTypeMap.size === 0) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const screenshots = {};
|
|
46
|
+
for (const [displayType, set] of displayTypeMap) {
|
|
47
|
+
const screenshotModels = set.attributes.appScreenshots;
|
|
48
|
+
if (!screenshotModels || screenshotModels.length === 0) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Download screenshots and save to local filesystem
|
|
52
|
+
const paths = [];
|
|
53
|
+
for (let i = 0; i < screenshotModels.length; i++) {
|
|
54
|
+
const screenshot = screenshotModels[i];
|
|
55
|
+
const relativePath = await downloadScreenshotAsync(context.projectDir, localeCode, displayType, screenshot, i);
|
|
56
|
+
if (relativePath) {
|
|
57
|
+
paths.push(relativePath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (paths.length > 0) {
|
|
61
|
+
screenshots[displayType] = paths;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (Object.keys(screenshots).length > 0) {
|
|
65
|
+
config.setScreenshots(localeCode, screenshots);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async uploadAsync({ config, context }) {
|
|
70
|
+
if (!context.screenshotSets || !context.versionLocales) {
|
|
71
|
+
log_1.default.log((0, chalk_1.default) `{dim - Skipped screenshots, no version available}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const locales = config.getLocales();
|
|
75
|
+
if (locales.length <= 0) {
|
|
76
|
+
log_1.default.log((0, chalk_1.default) `{dim - Skipped screenshots, no locales configured}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (const localeCode of locales) {
|
|
80
|
+
const screenshots = config.getScreenshots(localeCode);
|
|
81
|
+
if (!screenshots || Object.keys(screenshots).length === 0) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
|
|
85
|
+
if (!localization) {
|
|
86
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Skipping screenshots for ${localeCode} - locale not found}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
for (const [displayType, paths] of Object.entries(screenshots)) {
|
|
90
|
+
if (!paths || paths.length === 0) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
await syncScreenshotSetAsync(context.projectDir, localization, displayType, paths, context.screenshotSets.get(localeCode));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.ScreenshotsTask = ScreenshotsTask;
|
|
99
|
+
/**
|
|
100
|
+
* Sync a screenshot set - upload new screenshots, delete removed ones, reorder if needed.
|
|
101
|
+
*/
|
|
102
|
+
async function syncScreenshotSetAsync(projectDir, localization, displayType, paths, existingSets) {
|
|
103
|
+
const locale = localization.attributes.locale;
|
|
104
|
+
// Get or create the screenshot set
|
|
105
|
+
let screenshotSet = existingSets?.get(displayType);
|
|
106
|
+
if (!screenshotSet) {
|
|
107
|
+
screenshotSet = await (0, log_2.logAsync)(() => localization.createAppScreenshotSetAsync({
|
|
108
|
+
screenshotDisplayType: displayType,
|
|
109
|
+
}), {
|
|
110
|
+
pending: `Creating screenshot set for ${chalk_1.default.bold(displayType)} (${locale})...`,
|
|
111
|
+
success: `Created screenshot set for ${chalk_1.default.bold(displayType)} (${locale})`,
|
|
112
|
+
failure: `Failed creating screenshot set for ${chalk_1.default.bold(displayType)} (${locale})`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const existingScreenshots = screenshotSet.attributes.appScreenshots || [];
|
|
116
|
+
// Build a map of existing screenshots by filename for comparison
|
|
117
|
+
const existingByFilename = new Map();
|
|
118
|
+
for (const screenshot of existingScreenshots) {
|
|
119
|
+
existingByFilename.set(screenshot.attributes.fileName, screenshot);
|
|
120
|
+
}
|
|
121
|
+
// Track which screenshots to keep, upload, and delete
|
|
122
|
+
const screenshotIdsToKeep = [];
|
|
123
|
+
const pathsToUpload = [];
|
|
124
|
+
for (const relativePath of paths) {
|
|
125
|
+
const absolutePath = path_1.default.resolve(projectDir, relativePath);
|
|
126
|
+
const fileName = path_1.default.basename(absolutePath);
|
|
127
|
+
// Check if screenshot already exists with same name and file size
|
|
128
|
+
const existing = existingByFilename.get(fileName);
|
|
129
|
+
const localSize = fs_1.default.existsSync(absolutePath) ? fs_1.default.statSync(absolutePath).size : null;
|
|
130
|
+
if (existing &&
|
|
131
|
+
existing.isComplete() &&
|
|
132
|
+
(localSize === null || existing.attributes.fileSize === localSize)) {
|
|
133
|
+
screenshotIdsToKeep.push(existing.id);
|
|
134
|
+
existingByFilename.delete(fileName);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
pathsToUpload.push(absolutePath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Delete screenshots that are no longer in config
|
|
141
|
+
for (const screenshot of existingByFilename.values()) {
|
|
142
|
+
await (0, log_2.logAsync)(() => screenshot.deleteAsync(), {
|
|
143
|
+
pending: `Deleting screenshot ${chalk_1.default.bold(screenshot.attributes.fileName)} (${locale})...`,
|
|
144
|
+
success: `Deleted screenshot ${chalk_1.default.bold(screenshot.attributes.fileName)} (${locale})`,
|
|
145
|
+
failure: `Failed deleting screenshot ${chalk_1.default.bold(screenshot.attributes.fileName)} (${locale})`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// Upload new screenshots
|
|
149
|
+
for (const absolutePath of pathsToUpload) {
|
|
150
|
+
const fileName = path_1.default.basename(absolutePath);
|
|
151
|
+
if (!fs_1.default.existsSync(absolutePath)) {
|
|
152
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Screenshot not found: ${absolutePath}}`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const newScreenshot = await (0, log_2.logAsync)(() => apple_utils_1.AppScreenshot.uploadAsync(localization.context, {
|
|
156
|
+
id: screenshotSet.id,
|
|
157
|
+
filePath: absolutePath,
|
|
158
|
+
waitForProcessing: true,
|
|
159
|
+
}), {
|
|
160
|
+
pending: `Uploading screenshot ${chalk_1.default.bold(fileName)} (${locale})...`,
|
|
161
|
+
success: `Uploaded screenshot ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
162
|
+
failure: `Failed uploading screenshot ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
163
|
+
});
|
|
164
|
+
screenshotIdsToKeep.push(newScreenshot.id);
|
|
165
|
+
}
|
|
166
|
+
// Reorder screenshots to match config order
|
|
167
|
+
if (screenshotIdsToKeep.length > 0) {
|
|
168
|
+
const refreshedSet = await apple_utils_1.AppScreenshotSet.infoAsync(localization.context, {
|
|
169
|
+
id: screenshotSet.id,
|
|
170
|
+
});
|
|
171
|
+
const refreshedScreenshots = refreshedSet.attributes.appScreenshots || [];
|
|
172
|
+
const screenshotsByFilename = new Map();
|
|
173
|
+
for (const s of refreshedScreenshots) {
|
|
174
|
+
screenshotsByFilename.set(s.attributes.fileName, s);
|
|
175
|
+
}
|
|
176
|
+
// Build the desired order based on config paths
|
|
177
|
+
const orderedIds = [];
|
|
178
|
+
for (const relativePath of paths) {
|
|
179
|
+
const fileName = path_1.default.basename(relativePath);
|
|
180
|
+
const screenshot = screenshotsByFilename.get(fileName);
|
|
181
|
+
if (screenshot) {
|
|
182
|
+
orderedIds.push(screenshot.id);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Only call reorder if the order actually differs from current
|
|
186
|
+
const currentIds = refreshedScreenshots.map(s => s.id);
|
|
187
|
+
if (orderedIds.length > 0 &&
|
|
188
|
+
(orderedIds.length !== currentIds.length || orderedIds.some((id, i) => id !== currentIds[i]))) {
|
|
189
|
+
await screenshotSet.reorderScreenshotsAsync({ appScreenshots: orderedIds });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Download a screenshot to the local filesystem.
|
|
195
|
+
* Returns the relative path to the downloaded file.
|
|
196
|
+
*/
|
|
197
|
+
async function downloadScreenshotAsync(projectDir, locale, displayType, screenshot, index) {
|
|
198
|
+
const imageUrl = screenshot.getImageAssetUrl({ type: 'png' });
|
|
199
|
+
if (!imageUrl) {
|
|
200
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Could not get download URL for screenshot ${screenshot.attributes.fileName}}`);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
// Create directory structure: store/apple/screenshot/{locale}/{displayType}/
|
|
204
|
+
const screenshotsDir = path_1.default.join(projectDir, 'store', 'apple', 'screenshot', locale, displayType);
|
|
205
|
+
await fs_1.default.promises.mkdir(screenshotsDir, { recursive: true });
|
|
206
|
+
// Use original filename for matching during sync
|
|
207
|
+
const fileName = screenshot.attributes.fileName || `${String(index + 1).padStart(2, '0')}.png`;
|
|
208
|
+
const outputPath = path_1.default.join(screenshotsDir, fileName);
|
|
209
|
+
const relativePath = path_1.default.relative(projectDir, outputPath);
|
|
210
|
+
try {
|
|
211
|
+
const response = await (0, fetch_1.default)(imageUrl);
|
|
212
|
+
if (!response.ok) {
|
|
213
|
+
throw new Error(`HTTP ${response.status}`);
|
|
214
|
+
}
|
|
215
|
+
const buffer = await response.buffer();
|
|
216
|
+
await fs_1.default.promises.writeFile(outputPath, buffer);
|
|
217
|
+
log_1.default.log((0, chalk_1.default) `{dim Downloaded screenshot: ${relativePath}}`);
|
|
218
|
+
return relativePath;
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Failed to download screenshot ${fileName}: ${error.message}}`);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -1,5 +1,33 @@
|
|
|
1
|
-
import type { AgeRatingDeclarationProps } from '@expo/apple-utils';
|
|
1
|
+
import type { AgeRatingDeclarationProps, PreviewType, ScreenshotDisplayType } from '@expo/apple-utils';
|
|
2
2
|
export type AppleLocale = string;
|
|
3
|
+
/** Screenshot display type enum values from App Store Connect API */
|
|
4
|
+
export type AppleScreenshotDisplayType = `${ScreenshotDisplayType}`;
|
|
5
|
+
/** Preview display type enum values from App Store Connect API */
|
|
6
|
+
export type ApplePreviewType = `${PreviewType}`;
|
|
7
|
+
/**
|
|
8
|
+
* Screenshots organized by display type.
|
|
9
|
+
* Key is the display type (e.g., 'APP_IPHONE_67'), value is array of file paths.
|
|
10
|
+
* @example { "APP_IPHONE_67": ["./screenshots/home.png", "./screenshots/profile.png"] }
|
|
11
|
+
*/
|
|
12
|
+
export type AppleScreenshots = Partial<Record<AppleScreenshotDisplayType, string[]>>;
|
|
13
|
+
/**
|
|
14
|
+
* Video preview configuration - either a simple path string or an object with options.
|
|
15
|
+
* @example "./previews/demo.mp4"
|
|
16
|
+
* @example { path: "./previews/demo.mp4", previewFrameTimeCode: "00:05:00" }
|
|
17
|
+
*/
|
|
18
|
+
export type ApplePreviewConfig = string | {
|
|
19
|
+
/** Video file path (relative to project root) */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Optional preview frame time code (e.g., '00:05:00' for 5 seconds) */
|
|
22
|
+
previewFrameTimeCode?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Video previews organized by display type.
|
|
26
|
+
* Key is the display type (e.g., 'IPHONE_67'), value is the preview config.
|
|
27
|
+
* @example { "IPHONE_67": "./previews/demo.mp4" }
|
|
28
|
+
* @example { "IPHONE_67": { path: "./previews/demo.mp4", previewFrameTimeCode: "00:05:00" } }
|
|
29
|
+
*/
|
|
30
|
+
export type ApplePreviews = Partial<Record<ApplePreviewType, ApplePreviewConfig>>;
|
|
3
31
|
export interface AppleMetadata {
|
|
4
32
|
version?: string;
|
|
5
33
|
copyright?: string;
|
|
@@ -7,6 +35,7 @@ export interface AppleMetadata {
|
|
|
7
35
|
categories?: AppleCategory;
|
|
8
36
|
release?: AppleRelease;
|
|
9
37
|
advisory?: AppleAdvisory;
|
|
38
|
+
/** @deprecated Use screenshots/previews in AppleInfo instead */
|
|
10
39
|
preview?: Record<string, string[]>;
|
|
11
40
|
review?: AppleReview;
|
|
12
41
|
}
|
|
@@ -30,6 +59,10 @@ export interface AppleInfo {
|
|
|
30
59
|
privacyPolicyText?: string;
|
|
31
60
|
privacyChoicesUrl?: string;
|
|
32
61
|
supportUrl?: string;
|
|
62
|
+
/** Screenshots for this locale, organized by display type */
|
|
63
|
+
screenshots?: AppleScreenshots;
|
|
64
|
+
/** Video previews for this locale, organized by display type */
|
|
65
|
+
previews?: ApplePreviews;
|
|
33
66
|
}
|
|
34
67
|
export interface AppleReview {
|
|
35
68
|
firstName: string;
|
package/build/metadata/auth.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { App, Session } from '@expo/apple-utils';
|
|
2
2
|
import { ExpoConfig } from '@expo/config';
|
|
3
3
|
import { SubmitProfile } from '@expo/eas-json';
|
|
4
|
+
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
|
4
5
|
import { CredentialsContext } from '../credentials/context';
|
|
5
6
|
export type MetadataAppStoreAuthentication = {
|
|
6
7
|
/** The root entity of the App store */
|
|
@@ -11,10 +12,19 @@ export type MetadataAppStoreAuthentication = {
|
|
|
11
12
|
/**
|
|
12
13
|
* To start syncing ASC entities, we need access to the apple utils App instance.
|
|
13
14
|
* This resolves both the authentication and that App instance.
|
|
15
|
+
*
|
|
16
|
+
* Resolution order for authentication:
|
|
17
|
+
* 1. ASC API key from environment variables (EXPO_ASC_API_KEY_PATH, etc.)
|
|
18
|
+
* 2. ASC API key from submit profile (ascApiKeyPath, etc. in eas.json)
|
|
19
|
+
* 3. ASC API key from EAS credentials service
|
|
20
|
+
* 4. Interactive cookie auth (only when not in non-interactive mode)
|
|
14
21
|
*/
|
|
15
|
-
export declare function getAppStoreAuthAsync({ projectDir, profile, exp, credentialsCtx, }: {
|
|
22
|
+
export declare function getAppStoreAuthAsync({ projectDir, profile, exp, credentialsCtx, nonInteractive, graphqlClient, projectId, }: {
|
|
16
23
|
projectDir: string;
|
|
17
24
|
profile: SubmitProfile;
|
|
18
25
|
exp: ExpoConfig;
|
|
19
26
|
credentialsCtx: CredentialsContext;
|
|
27
|
+
nonInteractive: boolean;
|
|
28
|
+
graphqlClient: ExpoGraphqlClient;
|
|
29
|
+
projectId: string;
|
|
20
30
|
}): Promise<MetadataAppStoreAuthentication>;
|
package/build/metadata/auth.js
CHANGED
|
@@ -4,8 +4,14 @@ exports.getAppStoreAuthAsync = getAppStoreAuthAsync;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const apple_utils_1 = require("@expo/apple-utils");
|
|
6
6
|
const assert_1 = tslib_1.__importDefault(require("assert"));
|
|
7
|
+
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
7
8
|
const authenticate_1 = require("../credentials/ios/appstore/authenticate");
|
|
9
|
+
const authenticateTypes_1 = require("../credentials/ios/appstore/authenticateTypes");
|
|
10
|
+
const resolveCredentials_1 = require("../credentials/ios/appstore/resolveCredentials");
|
|
11
|
+
const AppStoreConnectApiKeyQuery_1 = require("../graphql/queries/AppStoreConnectApiKeyQuery");
|
|
12
|
+
const log_1 = tslib_1.__importDefault(require("../log"));
|
|
8
13
|
const bundleIdentifier_1 = require("../project/ios/bundleIdentifier");
|
|
14
|
+
const projectUtils_1 = require("../project/projectUtils");
|
|
9
15
|
/**
|
|
10
16
|
* Resolve the bundle identifier from the selected submit profile.
|
|
11
17
|
* This bundle identifier is used as target for the metadata submission.
|
|
@@ -16,15 +22,103 @@ async function resolveAppStoreBundleIdentifierAsync(projectDir, profile, exp, vc
|
|
|
16
22
|
}
|
|
17
23
|
return await (0, bundleIdentifier_1.getBundleIdentifierAsync)(projectDir, exp, vcsClient);
|
|
18
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Try to resolve an ASC API key from the submit profile or EAS credentials service.
|
|
27
|
+
* Returns null if no key is available from these sources.
|
|
28
|
+
*/
|
|
29
|
+
async function tryResolveAscApiKeyAsync({ profile, graphqlClient, projectId, exp, bundleId, }) {
|
|
30
|
+
// 1. Check submit profile for ASC API key fields
|
|
31
|
+
if ('ascApiKeyPath' in profile && 'ascApiKeyIssuerId' in profile && 'ascApiKeyId' in profile) {
|
|
32
|
+
const { ascApiKeyPath, ascApiKeyIssuerId, ascApiKeyId } = profile;
|
|
33
|
+
if (ascApiKeyPath && ascApiKeyIssuerId && ascApiKeyId) {
|
|
34
|
+
const keyP8 = await fs_1.default.promises.readFile(ascApiKeyPath, 'utf-8');
|
|
35
|
+
// Also try to get teamId from the profile if available
|
|
36
|
+
const teamId = 'appleTeamId' in profile ? profile.appleTeamId : undefined;
|
|
37
|
+
return {
|
|
38
|
+
ascApiKey: { keyP8, keyId: ascApiKeyId, issuerId: ascApiKeyIssuerId },
|
|
39
|
+
teamId,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 2. Look up stored credentials via EAS credentials service
|
|
44
|
+
try {
|
|
45
|
+
const account = await (0, projectUtils_1.getOwnerAccountForProjectIdAsync)(graphqlClient, projectId);
|
|
46
|
+
const appLookupParams = {
|
|
47
|
+
account,
|
|
48
|
+
projectName: exp.slug,
|
|
49
|
+
bundleIdentifier: bundleId,
|
|
50
|
+
};
|
|
51
|
+
// Import dynamically to avoid circular dependency issues
|
|
52
|
+
const { getAscApiKeyForAppSubmissionsAsync } = await Promise.resolve().then(() => tslib_1.__importStar(require('../credentials/ios/api/GraphqlClient')));
|
|
53
|
+
const ascKeyFragment = await getAscApiKeyForAppSubmissionsAsync(graphqlClient, appLookupParams);
|
|
54
|
+
if (ascKeyFragment) {
|
|
55
|
+
log_1.default.log('Using App Store Connect API Key from EAS credentials service.');
|
|
56
|
+
const fullKey = await AppStoreConnectApiKeyQuery_1.AppStoreConnectApiKeyQuery.getByIdAsync(graphqlClient, ascKeyFragment.id);
|
|
57
|
+
return {
|
|
58
|
+
ascApiKey: {
|
|
59
|
+
keyP8: fullKey.keyP8,
|
|
60
|
+
keyId: fullKey.keyIdentifier,
|
|
61
|
+
issuerId: fullKey.issuerIdentifier,
|
|
62
|
+
},
|
|
63
|
+
teamId: ascKeyFragment.appleTeam?.appleTeamIdentifier,
|
|
64
|
+
teamName: ascKeyFragment.appleTeam?.appleTeamName ?? undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
// If we can't look up credentials, that's fine — we'll fall back
|
|
70
|
+
log_1.default.warn(`Could not look up stored ASC API key: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
19
74
|
/**
|
|
20
75
|
* To start syncing ASC entities, we need access to the apple utils App instance.
|
|
21
76
|
* This resolves both the authentication and that App instance.
|
|
77
|
+
*
|
|
78
|
+
* Resolution order for authentication:
|
|
79
|
+
* 1. ASC API key from environment variables (EXPO_ASC_API_KEY_PATH, etc.)
|
|
80
|
+
* 2. ASC API key from submit profile (ascApiKeyPath, etc. in eas.json)
|
|
81
|
+
* 3. ASC API key from EAS credentials service
|
|
82
|
+
* 4. Interactive cookie auth (only when not in non-interactive mode)
|
|
22
83
|
*/
|
|
23
|
-
async function getAppStoreAuthAsync({ projectDir, profile, exp, credentialsCtx, }) {
|
|
84
|
+
async function getAppStoreAuthAsync({ projectDir, profile, exp, credentialsCtx, nonInteractive, graphqlClient, projectId, }) {
|
|
24
85
|
const bundleId = await resolveAppStoreBundleIdentifierAsync(projectDir, profile, exp, credentialsCtx.vcsClient);
|
|
86
|
+
// Try to resolve an ASC API key from profile or credentials service
|
|
87
|
+
const resolvedKey = await tryResolveAscApiKeyAsync({
|
|
88
|
+
profile,
|
|
89
|
+
graphqlClient,
|
|
90
|
+
projectId,
|
|
91
|
+
exp,
|
|
92
|
+
bundleId,
|
|
93
|
+
});
|
|
94
|
+
if (resolvedKey || (0, resolveCredentials_1.hasAscEnvVars)()) {
|
|
95
|
+
const authOptions = {
|
|
96
|
+
mode: authenticateTypes_1.AuthenticationMode.API_KEY,
|
|
97
|
+
...(resolvedKey
|
|
98
|
+
? {
|
|
99
|
+
ascApiKey: resolvedKey.ascApiKey,
|
|
100
|
+
teamId: resolvedKey.teamId,
|
|
101
|
+
teamName: resolvedKey.teamName,
|
|
102
|
+
// Default to COMPANY_OR_ORGANIZATION to avoid prompting for team type
|
|
103
|
+
teamType: authenticateTypes_1.AppleTeamType.COMPANY_OR_ORGANIZATION,
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
106
|
+
};
|
|
107
|
+
const authCtx = await credentialsCtx.appStore.ensureAuthenticatedAsync(authOptions);
|
|
108
|
+
(0, assert_1.default)(authCtx.authState, 'Failed to authenticate with App Store Connect');
|
|
109
|
+
const app = await apple_utils_1.App.findAsync((0, authenticate_1.getRequestContext)(authCtx), { bundleId });
|
|
110
|
+
(0, assert_1.default)(app, `Failed to load app "${bundleId}" from App Store Connect`);
|
|
111
|
+
return { app, auth: authCtx.authState };
|
|
112
|
+
}
|
|
113
|
+
if (nonInteractive) {
|
|
114
|
+
throw new Error('No App Store Connect API Key found. In non-interactive mode, provide one via:\n' +
|
|
115
|
+
' - Environment variables: EXPO_ASC_API_KEY_PATH, EXPO_ASC_KEY_ID, EXPO_ASC_ISSUER_ID\n' +
|
|
116
|
+
' - eas.json submit profile: ascApiKeyPath, ascApiKeyId, ascApiKeyIssuerId\n' +
|
|
117
|
+
' - EAS credentials service: run `eas credentials` to set up an API key');
|
|
118
|
+
}
|
|
119
|
+
// Fall back to interactive cookie auth
|
|
25
120
|
const authCtx = await credentialsCtx.appStore.ensureAuthenticatedAsync();
|
|
26
121
|
(0, assert_1.default)(authCtx.authState, 'Failed to authenticate with App Store Connect');
|
|
27
|
-
// TODO: improve error handling by mentioning possible configuration errors
|
|
28
122
|
const app = await apple_utils_1.App.findAsync((0, authenticate_1.getRequestContext)(authCtx), { bundleId });
|
|
29
123
|
(0, assert_1.default)(app, `Failed to load app "${bundleId}" from App Store Connect`);
|
|
30
124
|
return { app, auth: authCtx.authState };
|