Compare commits

...
Author SHA1 Message Date
Quinlan Jung f20d970616 tmp 2024-05-20 15:55:47 +02:00
Quinlan Jung f3712d5dfb amend apple team 2024-05-20 15:55:47 +02:00
Quinlan Jung cba211d82c manage asc api key 2024-05-20 15:55:19 +02:00
26 changed files with 6687 additions and 961 deletions

No files matched your search

+10
View File
@@ -22838,6 +22838,16 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "SCALAR",
"name": "DevDomainName",
"description": "A DevDomainName for a deployed serverless app",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "DeploymentsMutation", "name": "DeploymentsMutation",
+18 -1
View File
@@ -16,6 +16,9 @@ import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/An
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField'; import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient'; import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { CredentialsContext } from '../credentials/context'; import { CredentialsContext } from '../credentials/context';
import { AppStoreApiKeyPurpose } from '../credentials/ios/actions/AscApiKeyUtils';
import { getAppFromContextAsync } from '../credentials/ios/actions/BuildCredentialsUtils';
import { GetAscApiKey } from '../credentials/ios/actions/GetAscApiKey';
import { CustomBuildConfigMetadata } from '../project/customBuildConfig'; import { CustomBuildConfigMetadata } from '../project/customBuildConfig';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils'; import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow'; import { resolveWorkflowAsync } from '../project/workflow';
@@ -155,10 +158,24 @@ export async function createBuildContextAsync<T extends Platform>({
} as BuildContext<T>; } as BuildContext<T>;
} else { } else {
const common = commonContext as CommonContext<Platform.IOS>; const common = commonContext as CommonContext<Platform.IOS>;
return { const iosBuildCtx = {
...common, ...common,
ios: await createIosContextAsync(common), ios: await createIosContextAsync(common),
} as BuildContext<T>; } as BuildContext<T>;
const appLookupParams = {
...(await getAppFromContextAsync(credentialsCtx)),
bundleIdentifier: (iosBuildCtx as BuildContext<Platform.IOS>).ios.bundleIdentifier,
};
const getAscApiKeyAction = new GetAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.BUILD_SERVICE
);
const ascApiKeyManagedByExpo = await getAscApiKeyAction.runAsync(credentialsCtx);
if (ascApiKeyManagedByExpo) {
// TODO: set credentialCtx with the correct options
}
return iosBuildCtx;
} }
} }
@@ -7,8 +7,10 @@ import * as AndroidGraphqlClient from './android/api/GraphqlClient';
import * as IosGraphqlClient from './ios/api/GraphqlClient'; import * as IosGraphqlClient from './ios/api/GraphqlClient';
import AppStoreApi from './ios/appstore/AppStoreApi'; import AppStoreApi from './ios/appstore/AppStoreApi';
import { AuthenticationMode } from './ios/appstore/authenticateTypes'; import { AuthenticationMode } from './ios/appstore/authenticateTypes';
import { MinimalAscApiKey } from './ios/credentials';
import { Analytics } from '../analytics/AnalyticsManager'; import { Analytics } from '../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient'; import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { AppStoreConnectApiKeyFragment } from '../graphql/generated';
import Log from '../log'; import Log from '../log';
import { getPrivateExpoConfig } from '../project/expoConfig'; import { getPrivateExpoConfig } from '../project/expoConfig';
import { confirmAsync } from '../prompts'; import { confirmAsync } from '../prompts';
@@ -85,6 +87,28 @@ export class CredentialsContext {
getPrivateExpoConfig(this.options.projectDir); getPrivateExpoConfig(this.options.projectDir);
} }
public configureAppStoreAuth(ascApiKey: AppStoreConnectApiKeyFragment): void {
/* keyP8: string;
keyId: string;
issuerId: string;
teamId?: string;
teamName?: string;
roles?: UserRole[];
name?: string; */
this.appStore.authCtx = {
ascApiKey: {
keyP8: ascApiKey.keyP8,
keyId: ascApiKey.keyIdentifier,
issuerId: ascApiKey.issuerIdentifier,
},
appleTeam: {
teamIdentifier: ascApiKey.appleTeam?.appleTeamIdentifier ?? '',
teamName: ascApiKey.appleTeam?.appleTeamName,
teamType: ascApiKey.appleTeam,
},
};
}
async bestEffortAppStoreAuthenticateAsync(): Promise<void> { async bestEffortAppStoreAuthenticateAsync(): Promise<void> {
if (this.appStore.authCtx || !this.shouldAskAuthenticateAppStore) { if (this.appStore.authCtx || !this.shouldAskAuthenticateAppStore) {
// skip prompts if already have apple ctx or already asked about it // skip prompts if already have apple ctx or already asked about it
@@ -1,3 +1,4 @@
import { UserRole } from '@expo/apple-utils';
import chalk from 'chalk'; import chalk from 'chalk';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
@@ -24,10 +25,19 @@ import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey
export enum AppStoreApiKeyPurpose { export enum AppStoreApiKeyPurpose {
SUBMISSION_SERVICE = 'EAS Submit', SUBMISSION_SERVICE = 'EAS Submit',
BUILD_SERVICE = 'EAS Build',
} }
export async function promptForAscApiKeyPathAsync(ctx: CredentialsContext): Promise<AscApiKeyPath> { export const PURPOSE_TO_ROLES: Record<AppStoreApiKeyPurpose, UserRole[]> = {
const { keyId, keyP8Path } = await promptForKeyP8AndIdAsync(); [AppStoreApiKeyPurpose.SUBMISSION_SERVICE]: [UserRole.ADMIN],
[AppStoreApiKeyPurpose.BUILD_SERVICE]: [UserRole.ADMIN],
};
export async function promptForAscApiKeyPathAsync(
ctx: CredentialsContext,
roles: UserRole[]
): Promise<AscApiKeyPath> {
const { keyId, keyP8Path } = await promptForKeyP8AndIdAsync(roles);
const bestEffortIssuerId = await getBestEffortIssuerIdAsync(ctx, keyId); const bestEffortIssuerId = await getBestEffortIssuerIdAsync(ctx, keyId);
if (bestEffortIssuerId) { if (bestEffortIssuerId) {
@@ -60,15 +70,15 @@ export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<Mi
export async function provideOrGenerateAscApiKeyAsync( export async function provideOrGenerateAscApiKeyAsync(
ctx: CredentialsContext, ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose roles: UserRole[]
): Promise<MinimalAscApiKey> { ): Promise<MinimalAscApiKey> {
if (ctx.nonInteractive) { if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`); throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
} }
const userProvided = await promptForAscApiKeyAsync(ctx); const userProvided = await promptForAscApiKeyAsync(ctx, roles);
if (!userProvided) { if (!userProvided) {
return await generateAscApiKeyAsync(ctx, purpose); return await generateAscApiKeyAsync(ctx, roles);
} }
if (!ctx.appStore.authCtx) { if (!ctx.appStore.authCtx) {
@@ -86,39 +96,49 @@ export async function provideOrGenerateAscApiKeyAsync(
if (useUserProvided) { if (useUserProvided) {
return userProvided; return userProvided;
} }
return await provideOrGenerateAscApiKeyAsync(ctx, purpose); return await provideOrGenerateAscApiKeyAsync(ctx, roles);
} }
async function generateAscApiKeyAsync( async function generateAscApiKeyAsync(
ctx: CredentialsContext, ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose roles: UserRole[]
): Promise<MinimalAscApiKey> { ): Promise<MinimalAscApiKey> {
await ctx.appStore.ensureAuthenticatedAsync(); await ctx.appStore.ensureAuthenticatedAsync();
const ascApiKey = await ctx.appStore.createAscApiKeyAsync(ctx.analytics, { const ascApiKey = await ctx.appStore.createAscApiKeyAsync(ctx.analytics, {
nickname: getAscApiKeyName(purpose), nickname: getAscApiKeyName(),
roles,
}); });
return await getMinimalAscApiKeyAsync(ascApiKey); return await getMinimalAscApiKeyAsync(ascApiKey);
} }
export function getAscApiKeyName(purpose: AppStoreApiKeyPurpose): string { export function getAscApiKeyName(): string {
const nameParts = ['[Expo]', purpose, nanoid(10)]; const nameParts = ['[Expo]', nanoid(10)];
return nameParts.join(' '); return nameParts.join(' ');
} }
async function promptForAscApiKeyAsync(ctx: CredentialsContext): Promise<MinimalAscApiKey | null> { async function promptForAscApiKeyAsync(
ctx: CredentialsContext,
roles: UserRole[]
): Promise<MinimalAscApiKey | null> {
const shouldAutoGenerateCredentials = await shouldAutoGenerateCredentialsAsync(ascApiKeyIdSchema); const shouldAutoGenerateCredentials = await shouldAutoGenerateCredentialsAsync(ascApiKeyIdSchema);
if (shouldAutoGenerateCredentials) { if (shouldAutoGenerateCredentials) {
return null; return null;
} }
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx); const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx, roles);
const { keyP8Path, keyId, issuerId } = ascApiKeyPath; const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
return { keyP8: await fs.readFile(keyP8Path, 'utf-8'), keyId, issuerId }; return { keyP8: await fs.readFile(keyP8Path, 'utf-8'), keyId, issuerId };
} }
async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> { async function promptForKeyP8AndIdAsync(
roles: UserRole[]
): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> {
const rolesStringified = roles.map(r => r.toString()).join(', ');
const isPlural = roles.length > 1;
Log.log( Log.log(
chalk.bold( chalk.bold(
'An App Store Connect Api key is required to upload your app to the Apple App Store Connect' `An App Store Connect Api key with the ${rolesStringified} role${
isPlural ? 's' : ''
} is required to upload your app to the Apple App Store Connect`
) )
); );
Log.log( Log.log(
@@ -241,13 +261,16 @@ export function sortAscApiKeysByUpdatedAtDesc(
} }
export function formatAscApiKey(ascApiKey: AppStoreConnectApiKeyFragment): string { export function formatAscApiKey(ascApiKey: AppStoreConnectApiKeyFragment): string {
const { keyIdentifier, appleTeam, name, updatedAt } = ascApiKey; const { keyIdentifier, appleTeam, roles, name, updatedAt } = ascApiKey;
let line: string = ''; let line: string = '';
line += `Key ID: ${keyIdentifier}`; line += `Key ID: ${keyIdentifier}`;
if (name) { if (name) {
line += chalk.gray(`\n Name: ${name}`); line += chalk.gray(`\n Name: ${name}`);
} }
if (roles && roles.length > 0) {
line += chalk.gray(`\n Roles: ${roles.join(', ')}`);
}
if (appleTeam) { if (appleTeam) {
line += chalk.gray(`\n ${formatAppleTeam(appleTeam)}`); line += chalk.gray(`\n ${formatAppleTeam(appleTeam)}`);
} }
@@ -1,7 +1,9 @@
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils'; import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
import { ChooseAssociatedAppleTeamId } from './ChooseAssociatedAppleTeamId';
import { UpdateAppleTeamInfo } from './UpdateAppleTeamInfo';
import { import {
AppStoreConnectApiKeyFragment, AppStoreConnectApiKeyFragment,
AppleTeamFragment,
CommonIosAppCredentialsFragment, CommonIosAppCredentialsFragment,
} from '../../../graphql/generated'; } from '../../../graphql/generated';
import Log from '../../../log'; import Log from '../../../log';
@@ -16,12 +18,15 @@ export class AssignAscApiKey {
ascApiKey: AppStoreConnectApiKeyFragment, ascApiKey: AppStoreConnectApiKeyFragment,
purpose: AppStoreApiKeyPurpose purpose: AppStoreApiKeyPurpose
): Promise<CommonIosAppCredentialsFragment> { ): Promise<CommonIosAppCredentialsFragment> {
const appleTeam = const [ascApiKeyWithAppleTeam, appleTeam] = await this.ensureKeyHasAppleTeamInfoAsync(
(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app)) ?? ascApiKey.appleTeam ?? null; ctx,
ascApiKey
);
const appCredentials = await ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync( const appCredentials = await ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync(
ctx.graphqlClient, ctx.graphqlClient,
this.app, this.app,
{ appleTeam: appleTeam ?? undefined } { appleTeam }
); );
let updatedAppCredentials; let updatedAppCredentials;
if (purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) { if (purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
@@ -29,7 +34,15 @@ export class AssignAscApiKey {
ctx.graphqlClient, ctx.graphqlClient,
appCredentials, appCredentials,
{ {
ascApiKeyIdForSubmissions: ascApiKey.id, ascApiKeyIdForSubmissions: ascApiKeyWithAppleTeam.id,
}
);
} else if (purpose === AppStoreApiKeyPurpose.BUILD_SERVICE) {
updatedAppCredentials = await ctx.ios.updateIosAppCredentialsAsync(
ctx.graphqlClient,
appCredentials,
{
ascApiKeyIdForBuilds: ascApiKeyWithAppleTeam.id,
} }
); );
} else { } else {
@@ -40,4 +53,28 @@ export class AssignAscApiKey {
); );
return updatedAppCredentials; return updatedAppCredentials;
} }
private async ensureKeyHasAppleTeamInfoAsync(
ctx: CredentialsContext,
ascApiKey: AppStoreConnectApiKeyFragment
): Promise<[AppStoreConnectApiKeyFragment, AppleTeamFragment]> {
let appleTeamIdentifier = ascApiKey.appleTeam?.appleTeamIdentifier;
if (!appleTeamIdentifier) {
const chooseAssociatedAppleTeamIdAction = new ChooseAssociatedAppleTeamId(
'App Store Connect API Key'
);
appleTeamIdentifier = await chooseAssociatedAppleTeamIdAction.runAsync(ctx);
}
const updateAppleTeamAction = new UpdateAppleTeamInfo(this.app.account, appleTeamIdentifier);
const appleTeam = await updateAppleTeamAction.runAsync(ctx);
if (!ascApiKey.appleTeam) {
const updatedAscApiKey = await ctx.ios.updateAscApiKeyAsync(
ctx.graphqlClient,
ascApiKey,
appleTeam
);
return [updatedAscApiKey, appleTeam];
}
return [ascApiKey, appleTeam];
}
} }
@@ -0,0 +1,48 @@
import { Teams } from '@expo/apple-utils';
import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
export class ChooseAssociatedAppleTeamId {
constructor(private associatedCredential: string) {}
public async runAsync(ctx: CredentialsContext): Promise<string> {
if (!ctx.appStore.authCtx) {
return await this.promptForAppleTeamIdAsync();
}
const teams = await Teams.getTeamsAsync();
const chosenTeamId = await this.promptForAppleTeamIdFromDeveloperPortalAsync(
teams.map(team => ({ prettyName: `${team.name} (${team.type})`, teamId: team.teamId }))
);
return chosenTeamId ?? (await this.promptForAppleTeamIdAsync());
}
private async promptForAppleTeamIdFromDeveloperPortalAsync(
teamChoices: { prettyName: string; teamId: string }[]
): Promise<string | null> {
if (teamChoices.length === 0) {
return null;
}
const { appleTeamId } = await promptAsync({
type: 'select',
message: `Select the Apple Team associated with your ${this.associatedCredential}:`,
name: 'appleTeamId',
choices: [
...teamChoices.map(({ prettyName, teamId }) => ({ title: prettyName, value: teamId })),
{ title: 'None of the above', value: null },
],
});
return appleTeamId;
}
private async promptForAppleTeamIdAsync(): Promise<string> {
const { appleTeamId } = await promptAsync({
type: 'text',
name: 'appleTeamId',
message: `Input the Apple Team ID associated with your ${this.associatedCredential}:`,
validate: (val: string) => val !== '',
});
return appleTeamId;
}
}
@@ -1,6 +1,17 @@
import { AppStoreApiKeyPurpose, provideOrGenerateAscApiKeyAsync } from './AscApiKeyUtils'; import {
import { AccountFragment, AppStoreConnectApiKeyFragment } from '../../../graphql/generated'; AppStoreApiKeyPurpose,
PURPOSE_TO_ROLES,
provideOrGenerateAscApiKeyAsync,
} from './AscApiKeyUtils';
import { ChooseAssociatedAppleTeamId } from './ChooseAssociatedAppleTeamId';
import { UpdateAppleTeamInfo } from './UpdateAppleTeamInfo';
import {
AccountFragment,
AppStoreConnectApiKeyFragment,
AppleTeamFragment,
} from '../../../graphql/generated';
import { CredentialsContext } from '../../context'; import { CredentialsContext } from '../../context';
import { MinimalAscApiKey } from '../credentials';
export class CreateAscApiKey { export class CreateAscApiKey {
constructor(private account: AccountFragment) {} constructor(private account: AccountFragment) {}
@@ -12,8 +23,32 @@ export class CreateAscApiKey {
if (ctx.nonInteractive) { if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`); throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
} }
const roles = PURPOSE_TO_ROLES[purpose];
if (!roles) {
throw new Error(`Unsupported purpose: ${purpose}`);
}
const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, purpose); const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, roles);
return await ctx.ios.createAscApiKeyAsync(ctx.graphqlClient, this.account, ascApiKey); const appleTeam = await this.ensureKeyHasAppleTeamInfoAsync(ctx, ascApiKey);
return await ctx.ios.createAscApiKeyAsync(ctx.graphqlClient, this.account, {
...ascApiKey,
teamId: appleTeam.appleTeamIdentifier,
});
}
private async ensureKeyHasAppleTeamInfoAsync(
ctx: CredentialsContext,
ascApiKey: MinimalAscApiKey
): Promise<AppleTeamFragment> {
let appleTeamIdentifier = ascApiKey.teamId;
if (!appleTeamIdentifier) {
const chooseAssociatedAppleTeamIdAction = new ChooseAssociatedAppleTeamId(
'App Store Connect API Key'
);
appleTeamIdentifier = await chooseAssociatedAppleTeamIdAction.runAsync(ctx);
}
const updateAppleTeamAction = new UpdateAppleTeamInfo(this.account, appleTeamIdentifier);
return await updateAppleTeamAction.runAsync(ctx);
} }
} }
@@ -0,0 +1,39 @@
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
export class GetAscApiKey {
constructor(
private app: AppLookupParams,
private purpose: AppStoreApiKeyPurpose
) {}
public async runAsync(ctx: CredentialsContext): Promise<AppStoreConnectApiKeyFragment | null> {
const appCredentials = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(
ctx.graphqlClient,
this.app
);
if (this.purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
return appCredentials?.appStoreConnectApiKeyForSubmissions ?? null;
} else if (this.purpose === AppStoreApiKeyPurpose.BUILD_SERVICE) {
const ascApiKey = appCredentials?.appStoreConnectApiKeyForBuilds;
if (!ascApiKey) {
return null;
}
const appleTeam = ascApiKey.appleTeam;
const hasAppleTeamInfo =
!!appleTeam && !!appleTeam.appleTeamIdentifier && !!appleTeam.appleTeamType;
if (!hasAppleTeamInfo) {
Log.warn(
`App Store Connect API Key for ${this.purpose} is missing Apple Team information. Run "eas credentials" and select "Use an existing API Key" to update the missing information.`
);
return null;
}
return ascApiKey;
} else {
throw new Error(`App Store Connect API Keys are not yet supported for ${this.purpose}.`);
}
}
}
@@ -110,10 +110,13 @@ export class SetUpAscApiKey {
ctx.graphqlClient, ctx.graphqlClient,
this.app this.app
); );
if (purpose !== AppStoreApiKeyPurpose.SUBMISSION_SERVICE) { if (purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
return !!appCredentials?.appStoreConnectApiKeyForSubmissions;
} else if (purpose === AppStoreApiKeyPurpose.BUILD_SERVICE) {
return !!appCredentials?.appStoreConnectApiKeyForBuilds;
} else {
throw new Error(`App Store Connect API Key setup is not yet supported for ${purpose}.`); throw new Error(`App Store Connect API Key setup is not yet supported for ${purpose}.`);
} }
return !!appCredentials?.appStoreConnectApiKeyForSubmissions;
} }
private async processChoicesAsync( private async processChoicesAsync(
@@ -0,0 +1,88 @@
import { Teams } from '@expo/apple-utils';
import { AccountFragment, AppleTeamFragment, AppleTeamType } from '../../../graphql/generated';
import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { AppleTeamQuery } from '../api/graphql/queries/AppleTeamQuery';
/* *
* Update the Apple Team for the app.
* If the user is logged in, we sync the Team info from the Developer Portal
* Else we ask the user to choose their Team Type (Company/Organization, Individual, In-House)
*/
export class UpdateAppleTeamInfo {
constructor(
private account: AccountFragment,
private appleTeamIdentifier: string
) {}
public async runAsync(ctx: CredentialsContext): Promise<AppleTeamFragment> {
const appleTeam = await AppleTeamQuery.getByAppleTeamIdentifierAsync(
ctx.graphqlClient,
this.account.id,
this.appleTeamIdentifier
);
const hasAppleTeamInfo = !!appleTeam && !!appleTeam.appleTeamType;
if (hasAppleTeamInfo) {
return appleTeam;
}
if (!ctx.appStore.authCtx) {
return await this.promptAndUpdateAppleTeamTypeAsync(ctx);
}
const teams = await Teams.getTeamsAsync();
const team = teams.find(t => t.teamId === this.appleTeamIdentifier);
if (!team) {
return await this.promptAndUpdateAppleTeamTypeAsync(ctx);
}
const { teamId, type, name } = team;
const teamType = appStoreTeamTypeToEnum(type);
return await ctx.ios.createOrUpdateExistingAppleTeamAsync(ctx.graphqlClient, this.account, {
appleTeamIdentifier: teamId,
appleTeamName: name,
appleTeamType: teamType,
});
}
private async promptAndUpdateAppleTeamTypeAsync(
ctx: CredentialsContext
): Promise<AppleTeamFragment> {
if (ctx.nonInteractive) {
throw new Error(
'Unable to get Apple Team information in non-interactive mode. Please re-run with interactive mode.'
);
}
const teamType = await promptForAppleTeamTypeAsync();
return await ctx.ios.createOrUpdateExistingAppleTeamAsync(ctx.graphqlClient, this.account, {
appleTeamIdentifier: this.appleTeamIdentifier,
appleTeamType: teamType,
});
}
}
function appStoreTeamTypeToEnum(
teamType: 'In-House' | 'Company/Organization' | 'Individual' | string
): AppleTeamType | null {
if (teamType === 'In-House') {
return AppleTeamType.InHouse;
} else if (teamType === 'Company/Organization') {
return AppleTeamType.CompanyOrOrganization;
} else if (teamType === 'Individual') {
return AppleTeamType.Individual;
}
return null;
}
async function promptForAppleTeamTypeAsync(): Promise<AppleTeamType> {
const { appleTeamType } = await promptAsync({
type: 'select',
message: 'Select your Apple Team Type:',
name: 'appleTeamType',
choices: [
{ title: 'Enterprise', value: AppleTeamType.InHouse },
{ title: 'Company/Organization', value: AppleTeamType.CompanyOrOrganization },
{ title: 'Individual', value: AppleTeamType.Individual },
],
});
return appleTeamType;
}
@@ -1,21 +1,15 @@
import { UserRole } from '@expo/apple-utils';
import { promptAsync } from '../../../../prompts'; import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore'; import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context'; import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKey } from '../../../__tests__/fixtures-ios'; import { testAscApiKey } from '../../../__tests__/fixtures-ios';
import { getCredentialsFromUserAsync } from '../../../utils/promptForCredentials'; import { getCredentialsFromUserAsync } from '../../../utils/promptForCredentials';
import { import { getAscApiKeyName, promptForAscApiKeyPathAsync } from '../AscApiKeyUtils';
AppStoreApiKeyPurpose,
getAscApiKeyName,
promptForAscApiKeyPathAsync,
} from '../AscApiKeyUtils';
jest.mock('../../../../prompts'); jest.mock('../../../../prompts');
jest.mock('../../../utils/promptForCredentials'); jest.mock('../../../utils/promptForCredentials');
function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
return Object.keys(obj) as K[];
}
afterEach(() => { afterEach(() => {
jest.mocked(promptAsync).mockClear(); jest.mocked(promptAsync).mockClear();
jest.mocked(getCredentialsFromUserAsync).mockClear(); jest.mocked(getCredentialsFromUserAsync).mockClear();
@@ -24,11 +18,8 @@ afterEach(() => {
describe(getAscApiKeyName, () => { describe(getAscApiKeyName, () => {
// Apple enforces a 30 char limit on this name // Apple enforces a 30 char limit on this name
it('produces a name under 30 chars', async () => { it('produces a name under 30 chars', async () => {
for (const value of enumKeys(AppStoreApiKeyPurpose)) { const name = getAscApiKeyName();
const purpose = AppStoreApiKeyPurpose[value]; expect(name.length < 30).toBe(true);
const name = getAscApiKeyName(purpose);
expect(name.length < 30).toBe(true);
}
}); });
}); });
@@ -51,7 +42,7 @@ describe(promptForAscApiKeyPathAsync, () => {
authCtx: null, authCtx: null,
}, },
}); });
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx); const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx, [UserRole.ADMIN]);
expect(ascApiKeyPath).toEqual({ expect(ascApiKeyPath).toEqual({
keyId: 'test-key-id', keyId: 'test-key-id',
issuerId: 'test-issuer-id', issuerId: 'test-issuer-id',
@@ -76,7 +67,7 @@ describe(promptForAscApiKeyPathAsync, () => {
getAscApiKeyAsync: jest.fn(() => testAscApiKey), getAscApiKeyAsync: jest.fn(() => testAscApiKey),
}, },
}); });
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx); const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx, [UserRole.ADMIN]);
expect(ascApiKeyPath).toEqual({ expect(ascApiKeyPath).toEqual({
keyId: 'test-key-id', keyId: 'test-key-id',
issuerId: 'test-issuer-id-from-apple', issuerId: 'test-issuer-id-from-apple',
@@ -38,6 +38,7 @@ import {
AppleDistributionCertificateFragment, AppleDistributionCertificateFragment,
ApplePushKeyFragment, ApplePushKeyFragment,
AppleTeamFragment, AppleTeamFragment,
AppleTeamInput,
CommonIosAppCredentialsFragment, CommonIosAppCredentialsFragment,
IosAppBuildCredentialsFragment, IosAppBuildCredentialsFragment,
IosDistributionType, IosDistributionType,
@@ -189,28 +190,26 @@ export async function updateIosAppCredentialsAsync(
{ {
applePushKeyId, applePushKeyId,
ascApiKeyIdForSubmissions, ascApiKeyIdForSubmissions,
ascApiKeyIdForBuilds,
}: { }: {
applePushKeyId?: string; applePushKeyId?: string;
ascApiKeyIdForSubmissions?: string; ascApiKeyIdForSubmissions?: string;
ascApiKeyIdForBuilds?: string;
} }
): Promise<CommonIosAppCredentialsFragment> { ): Promise<CommonIosAppCredentialsFragment> {
let updatedAppCredentials = appCredentials; const updatedAppCredentials = appCredentials;
if (applePushKeyId) { if (!applePushKeyId && !ascApiKeyIdForSubmissions && !ascApiKeyIdForBuilds) {
updatedAppCredentials = await IosAppCredentialsMutation.setPushKeyAsync( return updatedAppCredentials;
graphqlClient,
updatedAppCredentials.id,
applePushKeyId
);
} }
if (ascApiKeyIdForSubmissions) { return await IosAppCredentialsMutation.updateIosAppCredentialsAsync(
updatedAppCredentials = graphqlClient,
await IosAppCredentialsMutation.setAppStoreConnectApiKeyForSubmissionsAsync( appCredentials.id,
graphqlClient, {
updatedAppCredentials.id, pushKeyId: applePushKeyId,
ascApiKeyIdForSubmissions appStoreConnectApiKeyForSubmissionsId: ascApiKeyIdForSubmissions,
); appStoreConnectApiKeyForBuildsId: ascApiKeyIdForBuilds,
} }
return updatedAppCredentials; );
} }
async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync( async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync(
@@ -263,7 +262,42 @@ async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAsync(
export async function createOrGetExistingAppleTeamAsync( export async function createOrGetExistingAppleTeamAsync(
graphqlClient: ExpoGraphqlClient, graphqlClient: ExpoGraphqlClient,
account: AccountFragment, account: AccountFragment,
{ appleTeamIdentifier, appleTeamName }: { appleTeamIdentifier: string; appleTeamName?: string } { appleTeamIdentifier, appleTeamName, appleTeamType }: AppleTeamInput
): Promise<AppleTeamFragment> {
return await createOrManageExistingAppleTeamAsync(
graphqlClient,
account,
{
appleTeamIdentifier,
appleTeamName,
appleTeamType,
},
{ updateMissingFields: false }
);
}
export async function createOrUpdateExistingAppleTeamAsync(
graphqlClient: ExpoGraphqlClient,
account: AccountFragment,
{ appleTeamIdentifier, appleTeamName, appleTeamType }: AppleTeamInput
): Promise<AppleTeamFragment> {
return await createOrManageExistingAppleTeamAsync(
graphqlClient,
account,
{
appleTeamIdentifier,
appleTeamName,
appleTeamType,
},
{ updateMissingFields: true }
);
}
async function createOrManageExistingAppleTeamAsync(
graphqlClient: ExpoGraphqlClient,
account: AccountFragment,
{ appleTeamIdentifier, appleTeamName, appleTeamType }: AppleTeamInput,
{ updateMissingFields }: { updateMissingFields: boolean }
): Promise<AppleTeamFragment> { ): Promise<AppleTeamFragment> {
const appleTeam = await AppleTeamQuery.getByAppleTeamIdentifierAsync( const appleTeam = await AppleTeamQuery.getByAppleTeamIdentifierAsync(
graphqlClient, graphqlClient,
@@ -271,11 +305,24 @@ export async function createOrGetExistingAppleTeamAsync(
appleTeamIdentifier appleTeamIdentifier
); );
if (appleTeam) { if (appleTeam) {
const didTeamNameChange = appleTeamName && appleTeam.appleTeamName !== appleTeamName;
const didTeamTypeChange = appleTeamType && appleTeam.appleTeamType !== appleTeamType;
const didFieldsChange = didTeamNameChange || didTeamTypeChange;
if (updateMissingFields && didFieldsChange) {
return await AppleTeamMutation.updateAppleTeamAsync(
graphqlClient,
{
appleTeamName: didTeamNameChange ? appleTeamName : undefined,
appleTeamType: didTeamTypeChange ? appleTeamType : undefined,
},
appleTeam.id
);
}
return appleTeam; return appleTeam;
} else { } else {
return await AppleTeamMutation.createAppleTeamAsync( return await AppleTeamMutation.createAppleTeamAsync(
graphqlClient, graphqlClient,
{ appleTeamIdentifier, appleTeamName }, { appleTeamIdentifier, appleTeamName, appleTeamType },
account.id account.id
); );
} }
@@ -521,6 +568,20 @@ export async function createAscApiKeyAsync(
); );
} }
export async function updateAscApiKeyAsync(
graphqlClient: ExpoGraphqlClient,
ascApiKey: AppStoreConnectApiKeyFragment,
appleTeam: AppleTeamFragment
): Promise<AppStoreConnectApiKeyFragment> {
return await AppStoreConnectApiKeyMutation.updateAppStoreConnectApiKeyAsync(
graphqlClient,
{
appleTeamId: appleTeam.id,
},
ascApiKey.id
);
}
export async function getAscApiKeysForAccountAsync( export async function getAscApiKeysForAccountAsync(
graphqlClient: ExpoGraphqlClient, graphqlClient: ExpoGraphqlClient,
account: AccountFragment account: AccountFragment
@@ -7,8 +7,10 @@ import { withErrorHandlingAsync } from '../../../../../graphql/client';
import { import {
AppStoreConnectApiKeyFragment, AppStoreConnectApiKeyFragment,
AppStoreConnectApiKeyInput, AppStoreConnectApiKeyInput,
AppStoreConnectApiKeyUpdateInput,
CreateAppStoreConnectApiKeyMutation, CreateAppStoreConnectApiKeyMutation,
DeleteAppStoreConnectApiKeyMutation, DeleteAppStoreConnectApiKeyMutation,
UpdateAppStoreConnectApiKeyMutation,
} from '../../../../../graphql/generated'; } from '../../../../../graphql/generated';
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey'; import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
@@ -51,6 +53,44 @@ export const AppStoreConnectApiKeyMutation = {
); );
return data.appStoreConnectApiKey.createAppStoreConnectApiKey; return data.appStoreConnectApiKey.createAppStoreConnectApiKey;
}, },
async updateAppStoreConnectApiKeyAsync(
graphqlClient: ExpoGraphqlClient,
appStoreConnectApiKeyUpdateInput: AppStoreConnectApiKeyUpdateInput,
appStoreConnectApiKeyId: string
): Promise<AppStoreConnectApiKeyFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateAppStoreConnectApiKeyMutation>(
gql`
mutation UpdateAppStoreConnectApiKeyMutation(
$appStoreConnectApiKeyUpdateInput: AppStoreConnectApiKeyUpdateInput!
$id: ID!
) {
appStoreConnectApiKey {
updateAppStoreConnectApiKey(
appStoreConnectApiKeyUpdateInput: $appStoreConnectApiKeyUpdateInput
id: $id
) {
id
...AppStoreConnectApiKeyFragment
}
}
}
${print(AppStoreConnectApiKeyFragmentNode)}
`,
{
appStoreConnectApiKeyUpdateInput,
appStoreConnectApiKeyId,
}
)
.toPromise()
);
assert(
data.appStoreConnectApiKey.updateAppStoreConnectApiKey,
'GraphQL: `updateAppStoreConnectApiKey` not defined in server response'
);
return data.appStoreConnectApiKey.updateAppStoreConnectApiKey;
},
async deleteAppStoreConnectApiKeyAsync( async deleteAppStoreConnectApiKeyAsync(
graphqlClient: ExpoGraphqlClient, graphqlClient: ExpoGraphqlClient,
appStoreConnectApiKeyId: string appStoreConnectApiKeyId: string
@@ -7,7 +7,9 @@ import {
AccountFragment, AccountFragment,
AppleTeamFragment, AppleTeamFragment,
AppleTeamInput, AppleTeamInput,
AppleTeamUpdateInput,
CreateAppleTeamMutation, CreateAppleTeamMutation,
UpdateAppleTeamMutation,
} from '../../../../../graphql/generated'; } from '../../../../../graphql/generated';
import { AccountFragmentNode } from '../../../../../graphql/types/Account'; import { AccountFragmentNode } from '../../../../../graphql/types/Account';
import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam'; import { AppleTeamFragmentNode } from '../../../../../graphql/types/credentials/AppleTeam';
@@ -50,4 +52,40 @@ export const AppleTeamMutation = {
); );
return data.appleTeam.createAppleTeam; return data.appleTeam.createAppleTeam;
}, },
async updateAppleTeamAsync(
graphqlClient: ExpoGraphqlClient,
appleTeamUpdateInput: AppleTeamUpdateInput,
appleTeamId: string
): Promise<AppleTeamMutationResult> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateAppleTeamMutation>(
gql`
mutation UpdateAppleTeamMutation(
$appleTeamUpdateInput: AppleTeamUpdateInput!
$appleTeamId: ID!
) {
appleTeam {
updateAppleTeam(appleTeamUpdateInput: $appleTeamUpdateInput, id: $appleTeamId) {
id
...AppleTeamFragment
account {
id
...AccountFragment
}
}
}
}
${print(AppleTeamFragmentNode)}
${print(AccountFragmentNode)}
`,
{
appleTeamUpdateInput,
appleTeamId,
}
)
.toPromise()
);
return data.appleTeam.updateAppleTeam;
},
}; };
@@ -10,6 +10,7 @@ import {
IosAppCredentialsInput, IosAppCredentialsInput,
SetAppStoreConnectApiKeyForSubmissionsMutation, SetAppStoreConnectApiKeyForSubmissionsMutation,
SetPushKeyMutation, SetPushKeyMutation,
UpdateIosAppCredentialsMutation,
} from '../../../../../graphql/generated'; } from '../../../../../graphql/generated';
import { CommonIosAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppCredentials'; import { CommonIosAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppCredentials';
@@ -56,6 +57,44 @@ export const IosAppCredentialsMutation = {
); );
return data.iosAppCredentials.createIosAppCredentials; return data.iosAppCredentials.createIosAppCredentials;
}, },
async updateIosAppCredentialsAsync(
graphqlClient: ExpoGraphqlClient,
iosAppCredentialsId: string,
iosAppCredentialsInput: IosAppCredentialsInput
): Promise<CommonIosAppCredentialsFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateIosAppCredentialsMutation>(
gql`
mutation UpdateIosAppCredentialsMutation(
$iosAppCredentialsInput: IosAppCredentialsInput!
$iosAppCredentialsId: ID!
) {
iosAppCredentials {
updateIosAppCredentials(
id: $iosAppCredentialsId
iosAppCredentialsInput: $iosAppCredentialsInput
) {
id
...CommonIosAppCredentialsFragment
}
}
}
${print(CommonIosAppCredentialsFragmentNode)}
`,
{
iosAppCredentialsInput,
iosAppCredentialsId,
}
)
.toPromise()
);
assert(
data.iosAppCredentials.updateIosAppCredentials,
'GraphQL: `updateIosAppCredentials` not defined in server response'
);
return data.iosAppCredentials.updateIosAppCredentials;
},
async setPushKeyAsync( async setPushKeyAsync(
graphqlClient: ExpoGraphqlClient, graphqlClient: ExpoGraphqlClient,
iosAppCredentialsId: string, iosAppCredentialsId: string,
@@ -1,4 +1,4 @@
import { ProfileType } from '@expo/app-store'; import { ProfileType, UserRole } from '@expo/app-store';
import { import {
AscApiKey, AscApiKey,
@@ -45,16 +45,23 @@ import { createOrReuseAdhocProvisioningProfileAsync } from './provisioningProfil
import { createPushKeyAsync, listPushKeysAsync, revokePushKeyAsync } from './pushKey'; import { createPushKeyAsync, listPushKeysAsync, revokePushKeyAsync } from './pushKey';
import { hasAscEnvVars } from './resolveCredentials'; import { hasAscEnvVars } from './resolveCredentials';
import { Analytics } from '../../../analytics/AnalyticsManager'; import { Analytics } from '../../../analytics/AnalyticsManager';
import { AppleTeamType } from '../../../graphql/generated';
import Log from '../../../log'; import Log from '../../../log';
import { MinimalAscApiKey } from '../credentials';
type AscApiKeyAuth = {
ascApiKey: MinimalAscApiKey;
appleTeam: { teamIdentifier: string; teamName?: string; teamType: AppleTeamType };
};
export default class AppStoreApi { export default class AppStoreApi {
public authCtx?: AuthCtx; public authCtx?: AuthCtx;
public defaultAuthenticationMode: AuthenticationMode; public defaultAuthenticationMode: AuthenticationMode;
public readonly ascApiKeyAuth?: AscApiKeyAuth;
constructor() { constructor(options: { ascApiKeyAuth?: AscApiKeyAuth } = {}) {
this.defaultAuthenticationMode = hasAscEnvVars() this.ascApiKeyAuth = options.ascApiKeyAuth;
? AuthenticationMode.API_KEY this.defaultAuthenticationMode =
: AuthenticationMode.USER; hasAscEnvVars() || this.ascApiKeyAuth ? AuthenticationMode.API_KEY : AuthenticationMode.USER;
} }
public async ensureUserAuthenticatedAsync(options?: AuthenticateOptions): Promise<UserAuthCtx> { public async ensureUserAuthenticatedAsync(options?: AuthenticateOptions): Promise<UserAuthCtx> {
@@ -195,10 +202,10 @@ export default class AppStoreApi {
public async createAscApiKeyAsync( public async createAscApiKeyAsync(
analytics: Analytics, analytics: Analytics,
{ nickname }: { nickname: string } { nickname, roles }: { nickname: string; roles: UserRole[] }
): Promise<AscApiKey> { ): Promise<AscApiKey> {
const userCtx = await this.ensureUserAuthenticatedAsync(); const userCtx = await this.ensureUserAuthenticatedAsync();
return await createAscApiKeyAsync(analytics, userCtx, { nickname }); return await createAscApiKeyAsync(analytics, userCtx, { nickname, roles });
} }
public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> { public async revokeAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
@@ -9,6 +9,7 @@ export enum AuthenticationMode {
USER, USER,
} }
// TODO: move this to graphql types
export enum AppleTeamType { export enum AppleTeamType {
IN_HOUSE = 'IN_HOUSE', IN_HOUSE = 'IN_HOUSE',
COMPANY_OR_ORGANIZATION = 'COMPANY_OR_ORGANIZATION', COMPANY_OR_ORGANIZATION = 'COMPANY_OR_ORGANIZATION',
@@ -2,53 +2,53 @@
exports[`print credentials prints the IosAppCredentials map 1`] = ` exports[`print credentials prints the IosAppCredentials map 1`] = `
"iOS Credentials "iOS Credentials
Project @quinlanj/test52 Project @quinlanj/test52
Bundle Identifier com.quinlanj.test52 Bundle Identifier com.quinlanj.test52
Apple Team test-apple-identifier Apple Team test-apple-identifier
Push Key Push Key
Developer Portal ID 9839M6AY8W Developer Portal ID 9839M6AY8W
Apple Team 77KQ969CHE Apple Team 77KQ969CHE
Updated 6 months ago Updated 6 months ago
App Store Connect API Key EAS Submit App Store Connect API Key
Developer Portal ID NYN468XNN3 Developer Portal ID NYN468XNN3
Name Expo Submissions pl94k8_lnS Name Expo Submissions pl94k8_lnS
Issuer ID 69a6de93-e4e4-47e3-e053-5b8c7c11a4d1 Issuer ID 69a6de93-e4e4-47e3-e053-5b8c7c11a4d1
Roles ADMIN Roles ADMIN
Apple Team 77KQ969CHE Apple Team 77KQ969CHE
Updated 6 months ago Updated 6 months ago
App Store Configuration App Store Configuration
Distribution Certificate Distribution Certificate
Serial Number test-serial Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier Apple Team test-apple-identifier
Updated 6 months ago Updated 6 months ago
Provisioning Profile Provisioning Profile
Developer Portal ID test-profile-id Developer Portal ID test-profile-id
Status active Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier Apple Team test-apple-identifier
Updated 5 months ago Updated 5 months ago
Ad Hoc Configuration Ad Hoc Configuration
Distribution Certificate Distribution Certificate
Serial Number test-serial Serial Number test-serial
Expiration Date Wed, 13 Oct 2021 18:28:44 UTC Expiration Date Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier Apple Team test-apple-identifier
Updated 6 months ago Updated 6 months ago
Provisioning Profile Provisioning Profile
Developer Portal ID test-profile-id Developer Portal ID test-profile-id
Status active Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier Apple Team test-apple-identifier
Updated 5 months ago Updated 5 months ago
" "
`; `;
@@ -12,6 +12,7 @@ import { APPLE_DEVICE_CLASS_LABELS } from '../../../graphql/types/credentials/Ap
import Log from '../../../log'; import Log from '../../../log';
import { fromNow } from '../../../utils/date'; import { fromNow } from '../../../utils/date';
import formatFields from '../../../utils/formatFields'; import formatFields from '../../../utils/formatFields';
import { AppStoreApiKeyPurpose } from '../actions/AscApiKeyUtils';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams'; import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { App, IosAppBuildCredentialsMap, IosAppCredentialsMap, Target } from '../types'; import { App, IosAppBuildCredentialsMap, IosAppCredentialsMap, Target } from '../types';
@@ -92,7 +93,12 @@ export function displayIosCredentials(
continue; continue;
} }
const { appleTeam, pushKey, appStoreConnectApiKeyForSubmissions } = targetAppCredentials; const {
appleTeam,
pushKey,
appStoreConnectApiKeyForSubmissions,
appStoreConnectApiKeyForBuilds,
} = targetAppCredentials;
if (appleTeam) { if (appleTeam) {
const { appleTeamIdentifier, appleTeamName } = appleTeam; const { appleTeamIdentifier, appleTeamName } = appleTeam;
fields.push({ fields.push({
@@ -107,7 +113,15 @@ export function displayIosCredentials(
} }
if (appStoreConnectApiKeyForSubmissions) { if (appStoreConnectApiKeyForSubmissions) {
displayAscApiKey(appStoreConnectApiKeyForSubmissions, fields); displayAscApiKey(
AppStoreApiKeyPurpose.SUBMISSION_SERVICE,
appStoreConnectApiKeyForSubmissions,
fields
);
}
if (appStoreConnectApiKeyForBuilds) {
displayAscApiKey(AppStoreApiKeyPurpose.BUILD_SERVICE, appStoreConnectApiKeyForBuilds, fields);
} }
const sortedIosAppBuildCredentialsList = sortBuildCredentialsByDistributionType( const sortedIosAppBuildCredentialsList = sortBuildCredentialsByDistributionType(
@@ -236,10 +250,11 @@ function displayApplePushKey(
} }
function displayAscApiKey( function displayAscApiKey(
purpose: AppStoreApiKeyPurpose,
maybeAscApiKey: AppStoreConnectApiKeyFragment | null, maybeAscApiKey: AppStoreConnectApiKeyFragment | null,
fields: { label: string; value: string }[] fields: { label: string; value: string }[]
): void { ): void {
fields.push({ label: 'App Store Connect API Key', value: '' }); fields.push({ label: `${purpose.toString()} App Store Connect API Key`, value: '' });
if (maybeAscApiKey) { if (maybeAscApiKey) {
const { keyIdentifier, issuerIdentifier, appleTeam, name, roles, updatedAt } = maybeAscApiKey; const { keyIdentifier, issuerIdentifier, appleTeam, name, roles, updatedAt } = maybeAscApiKey;
fields.push({ label: 'Developer Portal ID', value: keyIdentifier }); fields.push({ label: 'Developer Portal ID', value: keyIdentifier });
@@ -29,7 +29,6 @@ export enum AndroidActionType {
UseExistingGsaKeyForSubmissions, UseExistingGsaKeyForSubmissions,
RemoveGsaKey, RemoveGsaKey,
SetUpGsaKeyForSubmissions, SetUpGsaKeyForSubmissions,
CreateGsaKeyForFcmV1,
UseExistingGsaKeyForFcmV1, UseExistingGsaKeyForFcmV1,
RemoveGsaKeyForFcmV1, RemoveGsaKeyForFcmV1,
SetUpGsaKeyForFcmV1, SetUpGsaKeyForFcmV1,
@@ -56,8 +55,8 @@ export enum IosActionType {
CreatePushKey, CreatePushKey,
UseExistingPushKey, UseExistingPushKey,
RemovePushKey, RemovePushKey,
SetUpAscApiKeyForSubmissions, SetUpAscApiKeyForService,
UseExistingAscApiKeyForSubmissions, CreateAscApiKeyForService,
CreateAscApiKeyForSubmissions, UseExistingAscApiKeyForService,
RemoveAscApiKey, RemoveAscApiKey,
} }
@@ -77,22 +77,22 @@ export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
]; ];
} }
export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] { export function getAscApiKeyActions(): ActionInfo[] {
return [ return [
{ {
value: IosActionType.SetUpAscApiKeyForSubmissions, value: IosActionType.SetUpAscApiKeyForService,
title: 'Set up your project to use an API Key for EAS Submit', title: `Set up your project to use an API Key`,
scope: Scope.Project, scope: Scope.Project,
}, },
{ {
value: IosActionType.UseExistingAscApiKeyForSubmissions, value: IosActionType.UseExistingAscApiKeyForService,
title: 'Use an existing API Key for EAS Submit', title: `Use an existing API Key in your project`,
scope: Scope.Project, scope: Scope.Project,
}, },
{ {
value: IosActionType.CreateAscApiKeyForSubmissions, value: IosActionType.CreateAscApiKeyForService,
title: 'Add a new API Key For EAS Submit', title: `Add a new API Key to use in your project`,
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account, scope: Scope.Project,
}, },
{ {
value: IosActionType.RemoveAscApiKey, value: IosActionType.RemoveAscApiKey,
@@ -85,7 +85,7 @@ export class ManageIos {
}); });
const buildCredentialsActions = getBuildCredentialsActions(ctx); const buildCredentialsActions = getBuildCredentialsActions(ctx);
const pushKeyActions = getPushKeyActions(ctx); const pushKeyActions = getPushKeyActions(ctx);
const ascApiKeyActions = getAscApiKeyActions(ctx); const ascApiKeyActions = getAscApiKeyActions();
await ctx.bestEffortAppStoreAuthenticateAsync(); await ctx.bestEffortAppStoreAuthenticateAsync();
@@ -226,8 +226,6 @@ export class ManageIos {
await new CreateDistributionCertificate(account).runAsync(ctx); await new CreateDistributionCertificate(account).runAsync(ctx);
} else if (action === IosActionType.CreatePushKey) { } else if (action === IosActionType.CreatePushKey) {
await new CreatePushKey(account).runAsync(ctx); await new CreatePushKey(account).runAsync(ctx);
} else if (action === IosActionType.CreateAscApiKeyForSubmissions) {
await new CreateAscApiKey(account).runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
} else if (action === IosActionType.RemovePushKey) { } else if (action === IosActionType.RemovePushKey) {
await new SelectAndRemovePushKey(account).runAsync(ctx); await new SelectAndRemovePushKey(account).runAsync(ctx);
} else if (action === IosActionType.RemoveAscApiKey) { } else if (action === IosActionType.RemoveAscApiKey) {
@@ -354,40 +352,29 @@ export class ManageIos {
} }
return; return;
} }
case IosActionType.SetUpAscApiKeyForSubmissions: { case IosActionType.SetUpAscApiKeyForService: {
await new SetUpAscApiKey( const purpose = await this.selectAscApiKeyPurposeAsync();
appLookupParams, await new SetUpAscApiKey(appLookupParams, purpose).runAsync(ctx);
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
).runAsync(ctx);
return; return;
} }
case IosActionType.UseExistingAscApiKeyForSubmissions: { case IosActionType.UseExistingAscApiKeyForService: {
const purpose = await this.selectAscApiKeyPurposeAsync();
const ascApiKey = await selectAscApiKeysFromAccountAsync(ctx, appLookupParams.account, { const ascApiKey = await selectAscApiKeysFromAccountAsync(ctx, appLookupParams.account, {
filterDifferentAppleTeam: true, filterDifferentAppleTeam: true,
}); });
if (ascApiKey) { if (ascApiKey) {
await new AssignAscApiKey(appLookupParams).runAsync( await new AssignAscApiKey(appLookupParams).runAsync(ctx, ascApiKey, purpose);
ctx,
ascApiKey,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
} }
return; return;
} }
case IosActionType.CreateAscApiKeyForSubmissions: { case IosActionType.CreateAscApiKeyForService: {
const ascApiKey = await new CreateAscApiKey(appLookupParams.account).runAsync( const purpose = await this.selectAscApiKeyPurposeAsync();
ctx, const ascApiKey = await new CreateAscApiKey(appLookupParams.account).runAsync(ctx, purpose);
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
const confirm = await confirmAsync({ const confirm = await confirmAsync({
message: `Do you want ${appLookupParams.projectName} to use the new API Key?`, message: `Do you want ${appLookupParams.projectName} to use the new API Key?`,
}); });
if (confirm) { if (confirm) {
await new AssignAscApiKey(appLookupParams).runAsync( await new AssignAscApiKey(appLookupParams).runAsync(ctx, ascApiKey, purpose);
ctx,
ascApiKey,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
} }
return; return;
} }
@@ -431,4 +418,17 @@ export class ManageIos {
})) }))
); );
} }
protected async selectAscApiKeyPurposeAsync(): Promise<AppStoreApiKeyPurpose> {
const { keyPurpose } = await promptAsync({
type: 'select',
message: `Select the service you'd like to use your ASC Api Key for:`,
name: 'keyPurpose',
choices: [
{ title: 'EAS Submit', value: AppStoreApiKeyPurpose.SUBMISSION_SERVICE },
{ title: 'EAS Build', value: AppStoreApiKeyPurpose.BUILD_SERVICE },
],
});
return keyPurpose;
}
} }
+6026 -821
View File
File diff suppressed because it is too large. Load diff
@@ -5,5 +5,6 @@ export const AppleTeamFragmentNode = gql`
id id
appleTeamIdentifier appleTeamIdentifier
appleTeamName appleTeamName
appleTeamType
} }
`; `;
@@ -30,6 +30,10 @@ export const CommonIosAppCredentialsWithoutBuildCredentialsFragmentNode = gql`
id id
...AppStoreConnectApiKeyFragment ...AppStoreConnectApiKeyFragment
} }
appStoreConnectApiKeyForBuilds {
id
...AppStoreConnectApiKeyFragment
}
} }
${AppFragmentNode} ${AppFragmentNode}
${AppleTeamFragmentNode} ${AppleTeamFragmentNode}
@@ -1,3 +1,4 @@
import { UserRole } from '@expo/apple-utils';
import { Platform } from '@expo/eas-build-job'; import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra'; import fs from 'fs-extra';
import nullthrows from 'nullthrows'; import nullthrows from 'nullthrows';
@@ -189,7 +190,7 @@ async function handlePromptSourceAsync(
ctx: SubmissionContext<Platform.IOS>, ctx: SubmissionContext<Platform.IOS>,
_source: AscApiKeyPromptSource _source: AscApiKeyPromptSource
): Promise<AscApiKeyPath> { ): Promise<AscApiKeyPath> {
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx.credentialsCtx); const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx.credentialsCtx, [UserRole.ADMIN]);
return await getAscApiKeyPathAsync(ctx, { return await getAscApiKeyPathAsync(ctx, {
sourceType: AscApiKeySourceType.path, sourceType: AscApiKeySourceType.path,
path: ascApiKeyPath, path: ascApiKeyPath,