Compare commits

...
Author SHA1 Message Date
Quinlan Jung dc0b3b1237 [eas-cli] remove asp prompt 2021-11-05 16:15:33 -07:00
9 changed files with 120 additions and 331 deletions

No files matched your search

@@ -31,10 +31,10 @@ export enum SetupAscApiKeyChoice {
export class SetUpAscApiKey {
public choices: { title: string; value: string }[] = [
{
title: '[Choose an existing ASC Api Key]',
title: '[Choose an existing key]',
value: SetupAscApiKeyChoice.USE_EXISTING,
},
{ title: '[Add a new ASC Api Key]', value: SetupAscApiKeyChoice.GENERATE },
{ title: '[Add a new key]', value: SetupAscApiKeyChoice.GENERATE },
];
constructor(private app: AppLookupParams, private purpose: AppStoreApiKeyPurpose) {}
@@ -123,7 +123,7 @@ export class SetUpAscApiKey {
const result = await promptAsync({
type: 'select',
name: 'choice',
message: 'Select what to use for your project:',
message: 'Select the App Store Connect Api Key to use for your project:',
choices,
});
choice = result.choice;
@@ -1,70 +0,0 @@
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import wrapAnsi from 'wrap-ansi';
import { CommonIosAppCredentialsFragment } from '../../../graphql/generated';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { UnsupportedCredentialsChoiceError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
import { SetUpAscApiKey } from './SetUpAscApiKey';
export const PROMPT_FOR_APP_SPECIFIC_PASSWORD = 'PROMPT_FOR_APP_SPECIFIC_PASSWORD';
export class SetUpSubmissionCredentials {
private setupAscApiKeyAction;
constructor(app: AppLookupParams) {
this.setupAscApiKeyAction = new SetUpAscApiKey(app, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
// Add this unrelated choice to ASC Api Key setup for legacy purposes -- we will deprecate it soon
this.setupAscApiKeyAction.choices = this.setupAscApiKeyAction.choices.concat({
title: '[Enter an App Specific Password]',
value: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
});
}
public async runAsync(
ctx: CredentialsContext
): Promise<CommonIosAppCredentialsFragment | string> {
try {
const iosAppCredentials = await this.setupAscApiKeyAction.runAsync(ctx);
const { keyIdentifier, name } = nullthrows(
iosAppCredentials.appStoreConnectApiKeyForSubmissions,
'ASC Api Key must be defined for EAS Submit'
);
Log.log(`Using Api Key ID: ${keyIdentifier}${name ? ` (${name})` : ''}`);
return iosAppCredentials;
} catch (e) {
if (e instanceof UnsupportedCredentialsChoiceError) {
if (e.choice === PROMPT_FOR_APP_SPECIFIC_PASSWORD) {
return await this.promptForAppSpecificPasswordAsync();
}
}
throw e;
}
}
async promptForAppSpecificPasswordAsync(): Promise<string> {
Log.addNewLineIfNone();
Log.log(`Please enter your Apple app-specific password.`);
Log.log(learnMore('https://expo.fyi/apple-app-specific-password'));
Log.warn(
wrapAnsi(
`This option will be deprecated soon. You will still be able to provide your password with the ${chalk.bold(
'EXPO_APPLE_APP_SPECIFIC_PASSWORD'
)} environment variable.`,
process.stdout.columns || 80
)
);
const { appSpecificPassword } = await promptAsync({
name: 'appSpecificPassword',
message: 'Your Apple app-specific password:',
type: 'password',
validate: (val: string) => val !== '' || 'Apple app-specific password cannot be empty!',
});
return appSpecificPassword;
}
}
@@ -1,61 +0,0 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import {
getNewIosApiMock,
testCommonIosAppCredentialsFragment,
testTargets,
} from '../../../__tests__/fixtures-ios';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpAscApiKey } from '../SetUpAscApiKey';
import {
PROMPT_FOR_APP_SPECIFIC_PASSWORD,
SetUpSubmissionCredentials,
} from '../SetUpSubmissionCredentials';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(SetUpSubmissionCredentials, () => {
it('allows user to enter an App Specific Password', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({
choice: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
}))
.mockImplementationOnce(() => ({
appSpecificPassword: 'super secret',
}));
const ctx = createCtxMock({
nonInteractive: false,
ios: {
...getNewIosApiMock(),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpSubmissionCredentials(appLookupParams);
const asp = await setupAscApiKeyAction.runAsync(ctx);
// prompt to choose ASP, then prompt to input ASP
expect(asMock(promptAsync).mock.calls.length).toBe(2);
expect(asp).toBe('super secret');
});
it('returns an ASC API key', async () => {
try {
jest
.spyOn(SetUpAscApiKey.prototype, 'runAsync')
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
const ctx = createCtxMock();
const appLookupParams = getAppLookupParamsFromContext(
ctx,
findApplicationTarget(testTargets)
);
const setupAscApiKeyAction = new SetUpSubmissionCredentials(appLookupParams);
const asc = await setupAscApiKeyAction.runAsync(ctx);
expect(asc).toEqual(testCommonIosAppCredentialsFragment);
} finally {
jest.restoreAllMocks();
}
});
});
@@ -1,21 +1,33 @@
import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { promptForAscApiKeyPathAsync } from '../../credentials/ios/actions/AscApiKeyUtils';
import {
AppStoreApiKeyPurpose,
promptForAscApiKeyPathAsync,
} from '../../credentials/ios/actions/AscApiKeyUtils';
import { SetUpAscApiKey } from '../../credentials/ios/actions/SetUpAscApiKey';
import { AscApiKeyPath, MinimalAscApiKey } from '../../credentials/ios/credentials';
import Log from '../../log';
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
import { findAccountByName } from '../../user/Account';
import { SubmissionContext } from '../context';
import { isExistingFileAsync } from '../utils/files';
export enum AscApiKeySourceType {
path,
prompt,
credentialsService,
}
interface AscApiKeySourceBase {
sourceType: AscApiKeySourceType;
}
interface AscApiKeyCredentialsServiceSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.credentialsService;
}
interface AscApiKeyPromptSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.prompt;
}
@@ -25,7 +37,10 @@ interface AscApiKeyEnvVarSource extends AscApiKeySourceBase {
path: AscApiKeyPath;
}
export type AscApiKeySource = AscApiKeyEnvVarSource | AscApiKeyPromptSource;
export type AscApiKeySource =
| AscApiKeyEnvVarSource
| AscApiKeyPromptSource
| AscApiKeyCredentialsServiceSource;
type AscApiKeySummary = {
source: 'local' | 'EAS servers';
@@ -43,7 +58,55 @@ export type AscApiKeyResult = {
summary: AscApiKeySummary;
};
export async function getAscApiKeyLocallyAsync(
export async function getAscApiKeyResultAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeySource
): Promise<AscApiKeyResult> {
if (source.sourceType === AscApiKeySourceType.credentialsService) {
return await getAscApiKeyFromCredentialsServiceAsync(ctx);
} else {
return await getAscApiKeyLocallyAsync(ctx, source);
}
}
async function getAscApiKeyFromCredentialsServiceAsync(
ctx: SubmissionContext<Platform.IOS>
): Promise<AscApiKeyResult> {
const bundleIdentifier = await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
Log.log(`Looking up credentials configuration for ${bundleIdentifier}...`);
const appLookupParams = {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
bundleIdentifier,
};
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
const iosAppCredentials = await setupAscApiKeyAction.runAsync(ctx.credentialsCtx);
const ascKeyForSubmissions = nullthrows(
iosAppCredentials.appStoreConnectApiKeyForSubmissions,
`An EAS Submit ASC Api Key could not be found for ${iosAppCredentials.appleAppIdentifier.bundleIdentifier}`
);
const { id, keyIdentifier, name } = ascKeyForSubmissions;
Log.log(`Using Api Key ID: ${keyIdentifier}${name ? ` (${name})` : ''}`);
return {
result: {
ascApiKeyId: id,
},
summary: {
source: 'EAS servers',
keyId: keyIdentifier,
name: name ?? undefined,
},
};
}
async function getAscApiKeyLocallyAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeySource
): Promise<AscApiKeyResult> {
@@ -70,6 +133,8 @@ export async function getAscApiKeyPathAsync(
return await handlePathSourceAsync(ctx, source);
case AscApiKeySourceType.prompt:
return await handlePromptSourceAsync(ctx, source);
case AscApiKeySourceType.credentialsService:
throw new Error(`AscApiKeySourceType ${source} does not return a path.`);
}
}
@@ -1,56 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import { SetUpSubmissionCredentials } from '../../credentials/ios/actions/SetUpSubmissionCredentials';
import Log from '../../log';
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
import { findAccountByName } from '../../user/Account';
import { SubmissionContext } from '../context';
import { AscApiKeyResult } from './AscApiKeySource';
/**
* The Credentials Service will either return an ASC Api Key or an App Specific Password
* When we no longer support the App Specific Password user prompt, refactor this into the AscApiKeySource
*/
export const CREDENTIALS_SERVICE_SOURCE = 'CREDENTIALS_SERVICE_SOURCE';
export type CredentialsServiceSource = typeof CREDENTIALS_SERVICE_SOURCE;
export async function getFromCredentialsServiceAsync(
ctx: SubmissionContext<Platform.IOS>
): Promise<{ appSpecificPassword: string } | { ascApiKeyResult: AscApiKeyResult }> {
const bundleIdentifier = await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
Log.log(`Looking up credentials configuration for ${bundleIdentifier}...`);
const appLookupParams = {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
bundleIdentifier,
};
const setupSubmissionCredentialsAction = new SetUpSubmissionCredentials(appLookupParams);
const ascOrAsp = await setupSubmissionCredentialsAction.runAsync(ctx.credentialsCtx);
const isAppSpecificPassword = typeof ascOrAsp === 'string';
if (isAppSpecificPassword) {
return { appSpecificPassword: ascOrAsp };
} else {
const ascKeyForSubmissions = nullthrows(
ascOrAsp.appStoreConnectApiKeyForSubmissions,
`An EAS Submit ASC Api Key could not be found for ${ascOrAsp.appleAppIdentifier.bundleIdentifier}`
);
const { id, keyIdentifier, name } = ascKeyForSubmissions;
return {
ascApiKeyResult: {
result: {
ascApiKeyId: id,
},
summary: {
source: 'EAS servers',
keyId: keyIdentifier,
name: name ?? undefined,
},
},
};
}
}
@@ -18,7 +18,6 @@ import {
AppSpecificPasswordSourceType,
} from './AppSpecificPasswordSource';
import { AscApiKeySource, AscApiKeySourceType } from './AscApiKeySource';
import { CREDENTIALS_SERVICE_SOURCE, CredentialsServiceSource } from './CredentialsServiceSource';
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
export default class IosSubmitCommand {
@@ -34,15 +33,8 @@ export default class IosSubmitCommand {
private async resolveCredentialSubmissionOptionsAsync(): Promise<
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
| { ascApiKeySource: Result<AscApiKeySource> }
| { credentialsServiceSource: Result<CredentialsServiceSource> }
> {
const ascApiKeySource = this.resolveAscApiKeySource();
const shouldSkipAscApiKeySource =
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
if (!shouldSkipAscApiKeySource) {
return { ascApiKeySource };
}
// if an App Specific Password env var is not specified, use ASC Api Key
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
const shouldSkipAppSpecificPasswordSource =
!appSpecificPasswordSource.ok &&
@@ -51,9 +43,8 @@ export default class IosSubmitCommand {
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
}
return {
credentialsServiceSource: result(CREDENTIALS_SERVICE_SOURCE),
};
const ascApiKeySource = this.resolveAscApiKeySource();
return { ascApiKeySource };
}
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
@@ -65,10 +56,6 @@ export default class IosSubmitCommand {
: null;
const maybeAscApiKeySource =
'ascApiKeySource' in credentialsSource ? credentialsSource.ascApiKeySource : null;
const maybeCredentialsServiceSource =
'credentialsServiceSource' in credentialsSource
? credentialsSource.credentialsServiceSource
: null;
const ascAppIdentifier = await this.resolveAscAppIdentifierAsync();
const appleIdUsername = await this.resolveAppleIdUsernameAsync();
@@ -76,7 +63,6 @@ export default class IosSubmitCommand {
archiveSource,
...(maybeAppSpecificPasswordSource ? [maybeAppSpecificPasswordSource] : []),
...(maybeAscApiKeySource ? [maybeAscApiKeySource] : []),
...(maybeCredentialsServiceSource ? [maybeCredentialsServiceSource] : []),
ascAppIdentifier,
appleIdUsername,
].filter(r => !r.ok);
@@ -101,11 +87,6 @@ export default class IosSubmitCommand {
ascApiKeySource: maybeAscApiKeySource.enforceValue(),
}
: null),
...(maybeCredentialsServiceSource
? {
credentialsServiceSource: maybeCredentialsServiceSource.enforceValue(),
}
: null),
};
}
@@ -148,11 +129,7 @@ export default class IosSubmitCommand {
});
}
return result(
new MissingCredentialsError(
'Set the ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId fields in eas.json.'
)
);
return result({ sourceType: AscApiKeySourceType.credentialsService });
}
private resolveArchiveSource(): Result<ArchiveSource> {
@@ -20,12 +20,8 @@ import {
AscApiKeyFromExpoServers,
AscApiKeyResult,
AscApiKeySource,
getAscApiKeyLocallyAsync,
getAscApiKeyResultAsync,
} from './AscApiKeySource';
import {
CredentialsServiceSource,
getFromCredentialsServiceAsync,
} from './CredentialsServiceSource';
export interface IosSubmissionOptions
extends Pick<IosSubmissionConfigInput, 'appleIdUsername' | 'ascAppIdentifier'> {
@@ -33,7 +29,6 @@ export interface IosSubmissionOptions
archiveSource: ArchiveSource;
appSpecificPasswordSource?: AppSpecificPasswordSource;
ascApiKeySource?: AscApiKeySource;
credentialsServiceSource?: CredentialsServiceSource;
}
interface ResolvedSourceOptions {
@@ -80,16 +75,12 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
? await getAppSpecificPasswordAsync(this.options.appSpecificPasswordSource)
: null;
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
? await getAscApiKeyLocallyAsync(this.ctx, this.options.ascApiKeySource)
: null;
const maybeAscOrAspFromCredentialsService = this.options.credentialsServiceSource
? await getFromCredentialsServiceAsync(this.ctx)
? await getAscApiKeyResultAsync(this.ctx, this.options.ascApiKeySource)
: null;
return {
archive,
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
...(maybeAppStoreConnectApiKey ? { ascApiKeyResult: maybeAppStoreConnectApiKey } : null),
...(maybeAscOrAspFromCredentialsService ? maybeAscOrAspFromCredentialsService : null),
};
}
@@ -4,15 +4,18 @@ import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { testCommonIosAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-ios';
import { SetUpAscApiKey } from '../../../credentials/ios/actions/SetUpAscApiKey';
import { getCredentialsFromUserAsync } from '../../../credentials/utils/promptForCredentials';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
import { promptAsync } from '../../../prompts';
import { SubmissionContext, createSubmissionContextAsync } from '../../context';
import {
AscApiKeySource,
AscApiKeySourceType,
getAscApiKeyLocallyAsync,
getAscApiKeyPathAsync,
getAscApiKeyResultAsync,
} from '../AscApiKeySource';
jest.mock('fs');
@@ -24,6 +27,7 @@ jest.mock('../../../user/User', () => ({
jest.mock('../../../user/Account', () => ({
findAccountByName: jest.fn(() => mockJester.accounts[0]),
}));
jest.mock('../../../project/ios/bundleIdentifier');
const testProject = createTestProject(mockJester, {
android: {
@@ -167,14 +171,14 @@ describe(getAscApiKeyPathAsync, () => {
});
});
describe(getAscApiKeyLocallyAsync, () => {
describe(getAscApiKeyResultAsync, () => {
it('returns a local Asc Api Key file with a AscApiKeySourceType.path source', async () => {
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyResult = await getAscApiKeyLocallyAsync(ctx, source);
const ascApiKeyResult = await getAscApiKeyResultAsync(ctx, source);
expect(ascApiKeyResult).toMatchObject({
result: {
keyP8: 'super secret',
@@ -201,7 +205,7 @@ describe(getAscApiKeyLocallyAsync, () => {
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const serviceAccountResult = await getAscApiKeyLocallyAsync(ctx, source);
const serviceAccountResult = await getAscApiKeyResultAsync(ctx, source);
expect(serviceAccountResult).toMatchObject({
result: {
keyP8: 'super secret',
@@ -215,4 +219,39 @@ describe(getAscApiKeyLocallyAsync, () => {
},
});
});
it('returns an Asc Api Key from server with a AscApiKeySourceType.credentialService source', async () => {
const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
language: 'en-US',
},
nonInteractive: true,
});
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.credentialsService,
};
jest
.spyOn(SetUpAscApiKey.prototype, 'runAsync')
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
const result = await getAscApiKeyResultAsync(ctx, source);
expect(result).toEqual({
result: {
ascApiKeyId: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.id,
},
summary: {
keyId:
testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.keyIdentifier,
name: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.name,
source: 'EAS servers',
},
});
});
});
@@ -1,96 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { testCommonIosAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-ios';
import { SetUpSubmissionCredentials } from '../../../credentials/ios/actions/SetUpSubmissionCredentials';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
import { createSubmissionContextAsync } from '../../context';
import { getFromCredentialsServiceAsync } from '../CredentialsServiceSource';
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../credentials/utils/promptForCredentials');
jest.mock('../../../user/User', () => ({
getUserAsync: jest.fn(() => mockJester),
}));
jest.mock('../../../user/Account', () => ({
findAccountByName: jest.fn(() => mockJester.accounts[0]),
}));
jest.mock('../../../project/ios/bundleIdentifier');
const testProject = createTestProject(mockJester, {
android: {
package: 'com.expo.test.project',
},
});
const mockManifest = { exp: testProject.appJSON.expo };
jest.mock('@expo/config', () => ({
getConfig: jest.fn(() => mockManifest),
}));
const projectId = uuidv4();
beforeEach(() => {
jest.restoreAllMocks();
});
describe(getFromCredentialsServiceAsync, () => {
it('returns an App Specific Password from the credentialService source', async () => {
const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
language: 'en-US',
},
nonInteractive: true,
});
jest
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
.mockImplementation(async _ctx => 'super secret');
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
const result = await getFromCredentialsServiceAsync(ctx);
expect(result).toEqual({
appSpecificPassword: 'super secret',
});
});
it('returns an ASC Api Key from the credentialService source', async () => {
const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
language: 'en-US',
},
nonInteractive: true,
});
jest
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
const result = await getFromCredentialsServiceAsync(ctx);
expect(result).toEqual({
ascApiKeyResult: {
result: {
ascApiKeyId: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.id,
},
summary: {
keyId:
testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.keyIdentifier,
name: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.name,
source: 'EAS servers',
},
},
});
});
});