eas-cli 18.4.0 → 18.6.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/pagination.d.ts +2 -1
- package/build/commandUtils/pagination.js +3 -2
- 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/deploy/index.js +18 -2
- package/build/commands/metadata/pull.d.ts +1 -0
- package/build/commands/metadata/pull.js +9 -4
- package/build/commands/metadata/push.d.ts +1 -0
- package/build/commands/metadata/push.js +9 -4
- package/build/commands/observe/events.d.ts +27 -0
- package/build/commands/observe/events.js +140 -0
- package/build/commands/observe/metrics.d.ts +21 -0
- package/build/commands/observe/metrics.js +111 -0
- package/build/commands/observe/versions.d.ts +19 -0
- package/build/commands/observe/versions.js +69 -0
- package/build/commands/project/onboarding.js +3 -0
- package/build/commands/workflow/logs.js +12 -12
- package/build/credentials/ios/IosCredentialsProvider.js +8 -4
- package/build/credentials/ios/utils/provisioningProfile.d.ts +1 -0
- package/build/credentials/ios/utils/provisioningProfile.js +15 -1
- package/build/graphql/generated.d.ts +1273 -73
- package/build/graphql/generated.js +59 -19
- package/build/graphql/queries/ObserveQuery.d.ts +35 -0
- package/build/graphql/queries/ObserveQuery.js +109 -0
- package/build/graphql/queries/UserQuery.js +3 -0
- package/build/graphql/types/Observe.d.ts +3 -0
- package/build/graphql/types/Observe.js +84 -0
- package/build/graphql/types/Update.js +3 -0
- package/build/metadata/apple/config/reader.d.ts +13 -1
- package/build/metadata/apple/config/reader.js +33 -0
- package/build/metadata/apple/config/writer.d.ts +11 -1
- package/build/metadata/apple/config/writer.js +57 -0
- package/build/metadata/apple/data.d.ts +7 -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-clip.d.ts +37 -0
- package/build/metadata/apple/tasks/app-clip.js +404 -0
- package/build/metadata/apple/tasks/app-review-detail.js +7 -2
- package/build/metadata/apple/tasks/index.js +6 -0
- package/build/metadata/apple/tasks/previews.d.ts +18 -0
- package/build/metadata/apple/tasks/previews.js +212 -0
- package/build/metadata/apple/tasks/screenshots.d.ts +18 -0
- package/build/metadata/apple/tasks/screenshots.js +235 -0
- package/build/metadata/apple/types.d.ts +61 -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/observe/fetchEvents.d.ts +27 -0
- package/build/observe/fetchEvents.js +83 -0
- package/build/observe/fetchMetrics.d.ts +11 -0
- package/build/observe/fetchMetrics.js +78 -0
- package/build/observe/fetchVersions.d.ts +7 -0
- package/build/observe/fetchVersions.js +31 -0
- package/build/observe/formatEvents.d.ts +31 -0
- package/build/observe/formatEvents.js +99 -0
- package/build/observe/formatMetrics.d.ts +38 -0
- package/build/observe/formatMetrics.js +206 -0
- package/build/observe/formatVersions.d.ts +32 -0
- package/build/observe/formatVersions.js +92 -0
- package/build/observe/metricNames.d.ts +4 -0
- package/build/observe/metricNames.js +33 -0
- package/build/observe/startAndEndTime.d.ts +18 -0
- package/build/observe/startAndEndTime.js +36 -0
- 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/SessionManager.js +11 -0
- package/build/user/User.d.ts +2 -2
- package/build/user/User.js +3 -0
- package/build/user/expoBrowserAuthFlowLauncher.js +70 -13
- package/build/worker/upload.js +15 -5
- package/oclif.manifest.json +1794 -1306
- package/package.json +15 -11
- package/schema/metadata-0.json +213 -0
|
@@ -0,0 +1,212 @@
|
|
|
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 downloaded = await downloadPreviewAsync(context.projectDir, localeCode, previewType, preview, 0);
|
|
64
|
+
// When the download succeeds, write the real path. When it fails
|
|
65
|
+
// (e.g. the preview is in a broken AWAITING_UPLOAD state with no
|
|
66
|
+
// rendered videoUrl), preserve the entry in config so the user can
|
|
67
|
+
// either drop in a replacement file or remove the entry to delete
|
|
68
|
+
// the broken ASC record.
|
|
69
|
+
const fileName = preview.attributes.fileName || 'preview.mp4';
|
|
70
|
+
const relativePath = downloaded || path_1.default.join('store', 'apple', 'preview', localeCode, previewType, fileName);
|
|
71
|
+
if (preview.attributes.previewFrameTimeCode) {
|
|
72
|
+
previews[previewType] = {
|
|
73
|
+
path: relativePath,
|
|
74
|
+
previewFrameTimeCode: preview.attributes.previewFrameTimeCode,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
previews[previewType] = relativePath;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (Object.keys(previews).length > 0) {
|
|
82
|
+
config.setPreviews(localeCode, previews);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async uploadAsync({ config, context }) {
|
|
87
|
+
if (!context.previewSets || !context.versionLocales) {
|
|
88
|
+
log_1.default.log((0, chalk_1.default) `{dim - Skipped video previews, no version available}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const locales = config.getLocales();
|
|
92
|
+
if (locales.length <= 0) {
|
|
93
|
+
log_1.default.log((0, chalk_1.default) `{dim - Skipped video previews, no locales configured}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (const localeCode of locales) {
|
|
97
|
+
const previews = config.getPreviews(localeCode);
|
|
98
|
+
if (!previews || Object.keys(previews).length === 0) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
|
|
102
|
+
if (!localization) {
|
|
103
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Skipping video previews for ${localeCode} - locale not found}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const [previewType, previewConfig] of Object.entries(previews)) {
|
|
107
|
+
if (!previewConfig) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
await syncPreviewSetAsync(context.projectDir, localization, previewType, normalizePreviewConfig(previewConfig), context.previewSets.get(localeCode));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.PreviewsTask = PreviewsTask;
|
|
116
|
+
/**
|
|
117
|
+
* Sync a preview set - upload new preview, delete old one if changed.
|
|
118
|
+
*/
|
|
119
|
+
async function syncPreviewSetAsync(projectDir, localization, previewType, previewConfig, existingSets) {
|
|
120
|
+
const locale = localization.attributes.locale;
|
|
121
|
+
const absolutePath = path_1.default.resolve(projectDir, previewConfig.path);
|
|
122
|
+
const fileName = path_1.default.basename(absolutePath);
|
|
123
|
+
if (!fs_1.default.existsSync(absolutePath)) {
|
|
124
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Video preview not found: ${absolutePath}}`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Get or create the preview set
|
|
128
|
+
let previewSet = existingSets?.get(previewType);
|
|
129
|
+
if (!previewSet) {
|
|
130
|
+
previewSet = await (0, log_2.logAsync)(() => localization.createAppPreviewSetAsync({
|
|
131
|
+
previewType,
|
|
132
|
+
}), {
|
|
133
|
+
pending: `Creating preview set for ${chalk_1.default.bold(previewType)} (${locale})...`,
|
|
134
|
+
success: `Created preview set for ${chalk_1.default.bold(previewType)} (${locale})`,
|
|
135
|
+
failure: `Failed creating preview set for ${chalk_1.default.bold(previewType)} (${locale})`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const existingPreviews = previewSet.attributes.appPreviews || [];
|
|
139
|
+
// Check if we need to update (different filename, size, or no existing preview)
|
|
140
|
+
const existingPreview = existingPreviews.find(p => p.attributes.fileName === fileName);
|
|
141
|
+
const localSize = fs_1.default.statSync(absolutePath).size;
|
|
142
|
+
if (existingPreview &&
|
|
143
|
+
existingPreview.isComplete() &&
|
|
144
|
+
existingPreview.attributes.fileSize === localSize) {
|
|
145
|
+
// Preview with same filename exists, check if we need to update preview frame time code
|
|
146
|
+
if (previewConfig.previewFrameTimeCode &&
|
|
147
|
+
existingPreview.attributes.previewFrameTimeCode !== previewConfig.previewFrameTimeCode) {
|
|
148
|
+
await (0, log_2.logAsync)(() => existingPreview.updateAsync({
|
|
149
|
+
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
|
150
|
+
}), {
|
|
151
|
+
pending: `Updating preview frame time code for ${chalk_1.default.bold(fileName)} (${locale})...`,
|
|
152
|
+
success: `Updated preview frame time code for ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
153
|
+
failure: `Failed updating preview frame time code for ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
log_1.default.log((0, chalk_1.default) `{dim Preview ${fileName} already exists, skipping upload}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Delete existing previews that don't match
|
|
160
|
+
for (const preview of existingPreviews) {
|
|
161
|
+
if (preview.attributes.fileName !== fileName) {
|
|
162
|
+
await (0, log_2.logAsync)(() => preview.deleteAsync(), {
|
|
163
|
+
pending: `Deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})...`,
|
|
164
|
+
success: `Deleted old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
|
|
165
|
+
failure: `Failed deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Upload new preview
|
|
170
|
+
await (0, log_2.logAsync)(() => apple_utils_1.AppPreview.uploadAsync(localization.context, {
|
|
171
|
+
id: previewSet.id,
|
|
172
|
+
filePath: absolutePath,
|
|
173
|
+
waitForProcessing: true,
|
|
174
|
+
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
|
175
|
+
}), {
|
|
176
|
+
pending: `Uploading video preview ${chalk_1.default.bold(fileName)} (${locale})...`,
|
|
177
|
+
success: `Uploaded video preview ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
178
|
+
failure: `Failed uploading video preview ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Download a video preview to the local filesystem.
|
|
183
|
+
* Returns the relative path to the downloaded file.
|
|
184
|
+
*/
|
|
185
|
+
async function downloadPreviewAsync(projectDir, locale, previewType, preview, index) {
|
|
186
|
+
const videoUrl = preview.getVideoUrl();
|
|
187
|
+
if (!videoUrl) {
|
|
188
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Could not get download URL for preview ${preview.attributes.fileName}}`);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
// Create directory structure: store/apple/preview/{locale}/{previewType}/
|
|
192
|
+
const previewsDir = path_1.default.join(projectDir, 'store', 'apple', 'preview', locale, previewType);
|
|
193
|
+
await fs_1.default.promises.mkdir(previewsDir, { recursive: true });
|
|
194
|
+
// Use original filename for matching during sync
|
|
195
|
+
const fileName = preview.attributes.fileName || `${String(index + 1).padStart(2, '0')}.mp4`;
|
|
196
|
+
const outputPath = path_1.default.join(previewsDir, fileName);
|
|
197
|
+
const relativePath = path_1.default.relative(projectDir, outputPath);
|
|
198
|
+
try {
|
|
199
|
+
const response = await (0, fetch_1.default)(videoUrl);
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
throw new Error(`HTTP ${response.status}`);
|
|
202
|
+
}
|
|
203
|
+
const buffer = await response.buffer();
|
|
204
|
+
await fs_1.default.promises.writeFile(outputPath, buffer);
|
|
205
|
+
log_1.default.log((0, chalk_1.default) `{dim Downloaded video preview: ${relativePath}}`);
|
|
206
|
+
return relativePath;
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Failed to download video preview ${fileName}: ${error.message}}`);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -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,235 @@
|
|
|
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. When a screenshot
|
|
52
|
+
// is in a broken state (AWAITING_UPLOAD with no rendered imageAsset)
|
|
53
|
+
// the download will fail, but we still preserve the entry pointing at
|
|
54
|
+
// its expected local path so users can either drop in a replacement
|
|
55
|
+
// file or remove the entry to delete the broken ASC record.
|
|
56
|
+
const paths = [];
|
|
57
|
+
for (let i = 0; i < screenshotModels.length; i++) {
|
|
58
|
+
const screenshot = screenshotModels[i];
|
|
59
|
+
const downloaded = await downloadScreenshotAsync(context.projectDir, localeCode, displayType, screenshot, i);
|
|
60
|
+
if (downloaded) {
|
|
61
|
+
paths.push(downloaded);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Fall back to a placeholder path so the entry isn't lost from
|
|
65
|
+
// config. Push will detect that the existing screenshot isn't
|
|
66
|
+
// complete and either re-upload (if a local file exists at this
|
|
67
|
+
// path) or warn and skip (if it doesn't).
|
|
68
|
+
const fileName = screenshot.attributes.fileName || `${String(i + 1).padStart(2, '0')}.png`;
|
|
69
|
+
paths.push(path_1.default.join('store', 'apple', 'screenshot', localeCode, displayType, fileName));
|
|
70
|
+
}
|
|
71
|
+
if (paths.length > 0) {
|
|
72
|
+
screenshots[displayType] = paths;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (Object.keys(screenshots).length > 0) {
|
|
76
|
+
config.setScreenshots(localeCode, screenshots);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async uploadAsync({ config, context }) {
|
|
81
|
+
if (!context.screenshotSets || !context.versionLocales) {
|
|
82
|
+
log_1.default.log((0, chalk_1.default) `{dim - Skipped screenshots, no version available}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const locales = config.getLocales();
|
|
86
|
+
if (locales.length <= 0) {
|
|
87
|
+
log_1.default.log((0, chalk_1.default) `{dim - Skipped screenshots, no locales configured}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const localeCode of locales) {
|
|
91
|
+
const screenshots = config.getScreenshots(localeCode);
|
|
92
|
+
if (!screenshots || Object.keys(screenshots).length === 0) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
|
|
96
|
+
if (!localization) {
|
|
97
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Skipping screenshots for ${localeCode} - locale not found}`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const [displayType, paths] of Object.entries(screenshots)) {
|
|
101
|
+
if (!paths || paths.length === 0) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
await syncScreenshotSetAsync(context.projectDir, localization, displayType, paths, context.screenshotSets.get(localeCode));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.ScreenshotsTask = ScreenshotsTask;
|
|
110
|
+
/**
|
|
111
|
+
* Sync a screenshot set - upload new screenshots, delete removed ones, reorder if needed.
|
|
112
|
+
*/
|
|
113
|
+
async function syncScreenshotSetAsync(projectDir, localization, displayType, paths, existingSets) {
|
|
114
|
+
const locale = localization.attributes.locale;
|
|
115
|
+
// Get or create the screenshot set
|
|
116
|
+
let screenshotSet = existingSets?.get(displayType);
|
|
117
|
+
if (!screenshotSet) {
|
|
118
|
+
screenshotSet = await (0, log_2.logAsync)(() => localization.createAppScreenshotSetAsync({
|
|
119
|
+
screenshotDisplayType: displayType,
|
|
120
|
+
}), {
|
|
121
|
+
pending: `Creating screenshot set for ${chalk_1.default.bold(displayType)} (${locale})...`,
|
|
122
|
+
success: `Created screenshot set for ${chalk_1.default.bold(displayType)} (${locale})`,
|
|
123
|
+
failure: `Failed creating screenshot set for ${chalk_1.default.bold(displayType)} (${locale})`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const existingScreenshots = screenshotSet.attributes.appScreenshots || [];
|
|
127
|
+
// Build a map of existing screenshots by filename for comparison
|
|
128
|
+
const existingByFilename = new Map();
|
|
129
|
+
for (const screenshot of existingScreenshots) {
|
|
130
|
+
existingByFilename.set(screenshot.attributes.fileName, screenshot);
|
|
131
|
+
}
|
|
132
|
+
// Track which screenshots to keep, upload, and delete
|
|
133
|
+
const screenshotIdsToKeep = [];
|
|
134
|
+
const pathsToUpload = [];
|
|
135
|
+
for (const relativePath of paths) {
|
|
136
|
+
const absolutePath = path_1.default.resolve(projectDir, relativePath);
|
|
137
|
+
const fileName = path_1.default.basename(absolutePath);
|
|
138
|
+
// Check if screenshot already exists with same name and file size
|
|
139
|
+
const existing = existingByFilename.get(fileName);
|
|
140
|
+
const localSize = fs_1.default.existsSync(absolutePath) ? fs_1.default.statSync(absolutePath).size : null;
|
|
141
|
+
if (existing &&
|
|
142
|
+
existing.isComplete() &&
|
|
143
|
+
(localSize === null || existing.attributes.fileSize === localSize)) {
|
|
144
|
+
screenshotIdsToKeep.push(existing.id);
|
|
145
|
+
existingByFilename.delete(fileName);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
pathsToUpload.push(absolutePath);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Delete screenshots that are no longer in config
|
|
152
|
+
for (const screenshot of existingByFilename.values()) {
|
|
153
|
+
await (0, log_2.logAsync)(() => screenshot.deleteAsync(), {
|
|
154
|
+
pending: `Deleting screenshot ${chalk_1.default.bold(screenshot.attributes.fileName)} (${locale})...`,
|
|
155
|
+
success: `Deleted screenshot ${chalk_1.default.bold(screenshot.attributes.fileName)} (${locale})`,
|
|
156
|
+
failure: `Failed deleting screenshot ${chalk_1.default.bold(screenshot.attributes.fileName)} (${locale})`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// Upload new screenshots
|
|
160
|
+
for (const absolutePath of pathsToUpload) {
|
|
161
|
+
const fileName = path_1.default.basename(absolutePath);
|
|
162
|
+
if (!fs_1.default.existsSync(absolutePath)) {
|
|
163
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Screenshot not found: ${absolutePath}}`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const newScreenshot = await (0, log_2.logAsync)(() => apple_utils_1.AppScreenshot.uploadAsync(localization.context, {
|
|
167
|
+
id: screenshotSet.id,
|
|
168
|
+
filePath: absolutePath,
|
|
169
|
+
waitForProcessing: true,
|
|
170
|
+
}), {
|
|
171
|
+
pending: `Uploading screenshot ${chalk_1.default.bold(fileName)} (${locale})...`,
|
|
172
|
+
success: `Uploaded screenshot ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
173
|
+
failure: `Failed uploading screenshot ${chalk_1.default.bold(fileName)} (${locale})`,
|
|
174
|
+
});
|
|
175
|
+
screenshotIdsToKeep.push(newScreenshot.id);
|
|
176
|
+
}
|
|
177
|
+
// Reorder screenshots to match config order
|
|
178
|
+
if (screenshotIdsToKeep.length > 0) {
|
|
179
|
+
const refreshedSet = await apple_utils_1.AppScreenshotSet.infoAsync(localization.context, {
|
|
180
|
+
id: screenshotSet.id,
|
|
181
|
+
});
|
|
182
|
+
const refreshedScreenshots = refreshedSet.attributes.appScreenshots || [];
|
|
183
|
+
const screenshotsByFilename = new Map();
|
|
184
|
+
for (const s of refreshedScreenshots) {
|
|
185
|
+
screenshotsByFilename.set(s.attributes.fileName, s);
|
|
186
|
+
}
|
|
187
|
+
// Build the desired order based on config paths
|
|
188
|
+
const orderedIds = [];
|
|
189
|
+
for (const relativePath of paths) {
|
|
190
|
+
const fileName = path_1.default.basename(relativePath);
|
|
191
|
+
const screenshot = screenshotsByFilename.get(fileName);
|
|
192
|
+
if (screenshot) {
|
|
193
|
+
orderedIds.push(screenshot.id);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Only call reorder if the order actually differs from current
|
|
197
|
+
const currentIds = refreshedScreenshots.map(s => s.id);
|
|
198
|
+
if (orderedIds.length > 0 &&
|
|
199
|
+
(orderedIds.length !== currentIds.length || orderedIds.some((id, i) => id !== currentIds[i]))) {
|
|
200
|
+
await screenshotSet.reorderScreenshotsAsync({ appScreenshots: orderedIds });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Download a screenshot to the local filesystem.
|
|
206
|
+
* Returns the relative path to the downloaded file.
|
|
207
|
+
*/
|
|
208
|
+
async function downloadScreenshotAsync(projectDir, locale, displayType, screenshot, index) {
|
|
209
|
+
const imageUrl = screenshot.getImageAssetUrl({ type: 'png' });
|
|
210
|
+
if (!imageUrl) {
|
|
211
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Could not get download URL for screenshot ${screenshot.attributes.fileName}}`);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
// Create directory structure: store/apple/screenshot/{locale}/{displayType}/
|
|
215
|
+
const screenshotsDir = path_1.default.join(projectDir, 'store', 'apple', 'screenshot', locale, displayType);
|
|
216
|
+
await fs_1.default.promises.mkdir(screenshotsDir, { recursive: true });
|
|
217
|
+
// Use original filename for matching during sync
|
|
218
|
+
const fileName = screenshot.attributes.fileName || `${String(index + 1).padStart(2, '0')}.png`;
|
|
219
|
+
const outputPath = path_1.default.join(screenshotsDir, fileName);
|
|
220
|
+
const relativePath = path_1.default.relative(projectDir, outputPath);
|
|
221
|
+
try {
|
|
222
|
+
const response = await (0, fetch_1.default)(imageUrl);
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
throw new Error(`HTTP ${response.status}`);
|
|
225
|
+
}
|
|
226
|
+
const buffer = await response.buffer();
|
|
227
|
+
await fs_1.default.promises.writeFile(outputPath, buffer);
|
|
228
|
+
log_1.default.log((0, chalk_1.default) `{dim Downloaded screenshot: ${relativePath}}`);
|
|
229
|
+
return relativePath;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Failed to download screenshot ${fileName}: ${error.message}}`);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -1,5 +1,33 @@
|
|
|
1
|
-
import type { AgeRatingDeclarationProps } from '@expo/apple-utils';
|
|
1
|
+
import type { AgeRatingDeclarationProps, AppClipAction, 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,8 +35,36 @@ 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;
|
|
41
|
+
/** App Clip metadata. Only applies to apps that ship an App Clip target. */
|
|
42
|
+
appClip?: AppleAppClip;
|
|
43
|
+
}
|
|
44
|
+
/** App Clip action enum values from App Store Connect API */
|
|
45
|
+
export type AppleAppClipAction = `${AppClipAction}`;
|
|
46
|
+
export interface AppleAppClip {
|
|
47
|
+
/** The default experience for this App Clip. There is exactly one per app. */
|
|
48
|
+
defaultExperience?: AppleAppClipDefaultExperience;
|
|
49
|
+
}
|
|
50
|
+
export interface AppleAppClipDefaultExperience {
|
|
51
|
+
/** Action button shown in the App Clip card. Defaults to OPEN if unset. */
|
|
52
|
+
action?: AppleAppClipAction;
|
|
53
|
+
/** Whether to release this default experience alongside the next App Store version. */
|
|
54
|
+
releaseWithAppStoreVersion?: boolean;
|
|
55
|
+
/** App Store review invocation URLs (used by App Review to launch the clip). */
|
|
56
|
+
reviewDetail?: AppleAppClipReviewDetail;
|
|
57
|
+
/** Per-locale subtitle and header image. */
|
|
58
|
+
info?: Record<AppleLocale, AppleAppClipLocalizedInfo>;
|
|
59
|
+
}
|
|
60
|
+
export interface AppleAppClipReviewDetail {
|
|
61
|
+
invocationUrls: string[];
|
|
62
|
+
}
|
|
63
|
+
export interface AppleAppClipLocalizedInfo {
|
|
64
|
+
/** Subtitle shown in the App Clip card. Apple limits this to 43 characters. */
|
|
65
|
+
subtitle?: string;
|
|
66
|
+
/** Relative path (from project root) to the App Clip header image PNG. */
|
|
67
|
+
headerImage?: string;
|
|
12
68
|
}
|
|
13
69
|
export type AppleAdvisory = Omit<Partial<AgeRatingDeclarationProps>, 'seventeenPlus' | 'gamblingAndContests'>;
|
|
14
70
|
/** Apps can define up to two categories, or categories with up to two subcategories */
|
|
@@ -30,6 +86,10 @@ export interface AppleInfo {
|
|
|
30
86
|
privacyPolicyText?: string;
|
|
31
87
|
privacyChoicesUrl?: string;
|
|
32
88
|
supportUrl?: string;
|
|
89
|
+
/** Screenshots for this locale, organized by display type */
|
|
90
|
+
screenshots?: AppleScreenshots;
|
|
91
|
+
/** Video previews for this locale, organized by display type */
|
|
92
|
+
previews?: ApplePreviews;
|
|
33
93
|
}
|
|
34
94
|
export interface AppleReview {
|
|
35
95
|
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>;
|