Skip to content

Commit 85843c6

Browse files
author
Fatme
authored
Merge pull request #4547 from NativeScript/fatme/publish
fix: fix upload to appstore for accounts without 2 factor authentication
2 parents 9ab9639 + b2bd5b4 commit 85843c6

11 files changed

+339
-161
lines changed

lib/bootstrap.ts

+4
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,7 @@ $injector.require("qrCodeTerminalService", "./services/qr-code-terminal-service"
192192
$injector.require("testInitializationService", "./services/test-initialization-service");
193193

194194
$injector.require("networkConnectivityValidator", "./helpers/network-connectivity-validator");
195+
196+
$injector.require("applePortalSessionService", "./services/apple-portal/apple-portal-session-service");
197+
$injector.require("applePortalCookieService", "./services/apple-portal/apple-portal-cookie-service");
198+
$injector.require("applePortalApplicationService", "./services/apple-portal/apple-portal-application-service");

lib/commands/appstore-list.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export class ListiOSApps implements ICommand {
55
public allowedParameters: ICommandParameter[] = [new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)];
66

77
constructor(private $injector: IInjector,
8-
private $itmsTransporterService: IITMSTransporterService,
8+
private $applePortalApplicationService: IApplePortalApplicationService,
99
private $logger: ILogger,
1010
private $projectData: IProjectData,
1111
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
@@ -31,13 +31,14 @@ export class ListiOSApps implements ICommand {
3131
password = await this.$prompter.getPassword("Apple ID password");
3232
}
3333

34-
const iOSApplications = await this.$itmsTransporterService.getiOSApplications({ username, password });
34+
const applications = await this.$applePortalApplicationService.getApplications({ username, password });
3535

36-
if (!iOSApplications || !iOSApplications.length) {
36+
if (!applications || !applications.length) {
3737
this.$logger.out("Seems you don't have any applications yet.");
3838
} else {
39-
const table: any = createTable(["Application Name", "Bundle Identifier", "Version"], iOSApplications.map(element => {
40-
return [element.name, element.bundleId, element.version];
39+
const table: any = createTable(["Application Name", "Bundle Identifier", "In Flight Version"], applications.map(application => {
40+
const version = (application && application.versionSets && application.versionSets.length && application.versionSets[0].inFlightVersion && application.versionSets[0].inFlightVersion.version) || "";
41+
return [application.name, application.bundleId, version];
4142
}));
4243

4344
this.$logger.out(table.toString());

lib/commands/appstore-upload.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ export class PublishIOS implements ICommand {
66
public allowedParameters: ICommandParameter[] = [new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector),
77
new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)];
88

9-
constructor(private $errors: IErrors,
9+
constructor(
1010
private $injector: IInjector,
1111
private $itmsTransporterService: IITMSTransporterService,
1212
private $logger: ILogger,
1313
private $projectData: IProjectData,
1414
private $options: IOptions,
1515
private $prompter: IPrompter,
16-
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants) {
16+
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
17+
private $hostInfo: IHostInfo,
18+
private $errors: IErrors) {
1719
this.$projectData.initializeProjectData();
1820
}
1921

@@ -107,11 +109,16 @@ export class PublishIOS implements ICommand {
107109
username,
108110
password,
109111
ipaFilePath,
112+
shouldExtractIpa: !!this.$options.ipa,
110113
verboseLogging: this.$logger.getLevel() === "TRACE"
111114
});
112115
}
113116

114117
public async canExecute(args: string[]): Promise<boolean> {
118+
if (!this.$hostInfo.isDarwin) {
119+
this.$errors.failWithoutHelp("iOS publishing is only available on Mac OS X.");
120+
}
121+
115122
if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) {
116123
this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`);
117124
}

lib/declarations.d.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,11 @@ interface IITMSData extends ICredentials {
622622
* @type {string}
623623
*/
624624
ipaFilePath: string;
625+
626+
/**
627+
* Specify if the service should extract the `.ipa` file into `temp` directory in order to get bundleIdentifier from info.plist
628+
*/
629+
shouldExtractIpa: boolean;
625630
/**
626631
* Specifies whether the logging level of the itmstransporter command-line tool should be set to verbose.
627632
* @type {string}
@@ -639,12 +644,6 @@ interface IITMSTransporterService {
639644
* @return {Promise<void>}
640645
*/
641646
upload(data: IITMSData): Promise<void>;
642-
/**
643-
* Queries Apple's content delivery API to get the user's registered iOS applications.
644-
* @param {ICredentials} credentials Credentials for authentication with iTunes Connect.
645-
* @return {Promise<IItunesConnectApplication[]>} The user's iOS applications.
646-
*/
647-
getiOSApplications(credentials: ICredentials): Promise<IiTunesConnectApplication[]>;
648647
}
649648

650649
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export class ApplePortalApplicationService implements IApplePortalApplicationService {
2+
constructor(
3+
private $applePortalSessionService: IApplePortalSessionService,
4+
private $errors: IErrors,
5+
private $httpClient: Server.IHttpClient
6+
) { }
7+
8+
public async getApplications(credentials: ICredentials): Promise<IApplePortalApplicationSummary[]> {
9+
let result: IApplePortalApplicationSummary[] = [];
10+
11+
const user = await this.$applePortalSessionService.createUserSession(credentials);
12+
for (const account of user.associatedAccounts) {
13+
const contentProviderId = account.contentProvider.contentProviderId;
14+
const dsId = user.sessionToken.dsId;
15+
const applications = await this.getApplicationsByProvider(contentProviderId, dsId);
16+
result = result.concat(applications.summaries);
17+
}
18+
19+
return result;
20+
}
21+
22+
public async getApplicationsByProvider(contentProviderId: number, dsId: string): Promise<IApplePortalApplication> {
23+
const webSessionCookie = await this.$applePortalSessionService.createWebSession(contentProviderId, dsId);
24+
const response = await this.$httpClient.httpRequest({
25+
url: "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/manageyourapps/summary/v2",
26+
method: "GET",
27+
body: JSON.stringify({
28+
contentProviderId
29+
}),
30+
headers: {
31+
'Content-Type': 'application/json',
32+
'Cookie': webSessionCookie
33+
}
34+
});
35+
36+
return JSON.parse(response.body).data;
37+
}
38+
39+
public async getApplicationByBundleId(credentials: ICredentials, bundleId: string): Promise<IApplePortalApplicationSummary> {
40+
const applications = await this.getApplications(credentials);
41+
if (!applications || !applications.length) {
42+
this.$errors.failWithoutHelp(`Cannot find any registered applications for Apple ID ${credentials.username} in iTunes Connect.`);
43+
}
44+
45+
const application = _.find(applications, app => app.bundleId === bundleId);
46+
47+
if (!application) {
48+
this.$errors.failWithoutHelp(`Cannot find registered applications that match the specified identifier ${bundleId} in iTunes Connect.`);
49+
}
50+
51+
return application;
52+
}
53+
}
54+
$injector.register("applePortalApplicationService", ApplePortalApplicationService);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export class ApplePortalCookieService implements IApplePortalCookieService {
2+
private userSessionCookies: IStringDictionary = {};
3+
private validUserSessionCookieNames = ["myacinfo", "dqsid", "itctx", "itcdq", "acn01"];
4+
private validWebSessionCookieNames = ["wosid", "woinst", "itctx"];
5+
6+
public getWebSessionCookie(cookiesData: string[]): string {
7+
const webSessionCookies = _.cloneDeep(this.userSessionCookies);
8+
9+
const parsedCookies = this.parseCookiesData(cookiesData, this.validWebSessionCookieNames);
10+
_.each(parsedCookies, parsedCookie => webSessionCookies[parsedCookie.key] = parsedCookie.cookie);
11+
12+
return _.values(webSessionCookies).join("; ");
13+
}
14+
15+
public getUserSessionCookie(): string {
16+
return _.values(this.userSessionCookies).join("; ");
17+
}
18+
19+
public updateUserSessionCookie(cookiesData: string[]): void {
20+
const parsedCookies = this.parseCookiesData(cookiesData, this.validUserSessionCookieNames);
21+
_.each(parsedCookies, parsedCookie => this.userSessionCookies[parsedCookie.key] = parsedCookie.cookie);
22+
}
23+
24+
private parseCookiesData(cookiesData: string[], validCookieNames: string[]): IDictionary<{key: string, value: string, cookie: string}> {
25+
const result: IDictionary<{key: string, value: string, cookie: string}> = {};
26+
27+
for (const c of cookiesData) {
28+
const parts = c.split(";");
29+
for (const cookie of parts) {
30+
const trimmedCookie = cookie.trim();
31+
const [cookieKey, cookieValue] = trimmedCookie.split("=");
32+
if (_.includes(validCookieNames, cookieKey)) {
33+
result[cookieKey] = { key: cookieKey, value: cookieValue, cookie: trimmedCookie };
34+
}
35+
}
36+
}
37+
38+
return result;
39+
}
40+
}
41+
$injector.register("applePortalCookieService", ApplePortalCookieService);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
export class ApplePortalSessionService implements IApplePortalSessionService {
2+
private loginConfigEndpoint = "https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com";
3+
private defaultLoginConfig = {
4+
authServiceUrl : "https://idmsa.apple.com/appleautcodh",
5+
authServiceKey : "e0b80c3bf78523bfe80974d320935bfa30add02e1bff88ec2166c6bd5a706c42"
6+
};
7+
8+
constructor(
9+
private $applePortalCookieService: IApplePortalCookieService,
10+
private $httpClient: Server.IHttpClient,
11+
private $logger: ILogger
12+
) { }
13+
14+
public async createUserSession(credentials: ICredentials): Promise<IApplePortalUserDetail> {
15+
const loginConfig = await this.getLoginConfig();
16+
const loginUrl = `${loginConfig.authServiceUrl}/auth/signin`;
17+
const loginResponse = await this.$httpClient.httpRequest({
18+
url: loginUrl,
19+
method: "POST",
20+
body: JSON.stringify({
21+
accountName: credentials.username,
22+
password: credentials.password,
23+
rememberMe: true
24+
}),
25+
headers: {
26+
'Content-Type': 'application/json',
27+
'X-Requested-With': 'XMLHttpRequest',
28+
'X-Apple-Widget-Key': loginConfig.authServiceKey,
29+
'Accept': 'application/json, text/javascript'
30+
}
31+
});
32+
33+
this.$applePortalCookieService.updateUserSessionCookie(loginResponse.headers["set-cookie"]);
34+
35+
const sessionResponse = await this.$httpClient.httpRequest({
36+
url: "https://appstoreconnect.apple.com/olympus/v1/session",
37+
method: "GET",
38+
headers: {
39+
'Cookie': this.$applePortalCookieService.getUserSessionCookie()
40+
}
41+
});
42+
43+
this.$applePortalCookieService.updateUserSessionCookie(sessionResponse.headers["set-cookie"]);
44+
45+
const userDetailResponse = await this.$httpClient.httpRequest({
46+
url: "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/user/detail",
47+
method: "GET",
48+
headers: {
49+
'Content-Type': 'application/json',
50+
'Cookie': this.$applePortalCookieService.getUserSessionCookie(),
51+
}
52+
});
53+
54+
this.$applePortalCookieService.updateUserSessionCookie(userDetailResponse.headers["set-cookie"]);
55+
56+
return JSON.parse(userDetailResponse.body).data;
57+
}
58+
59+
public async createWebSession(contentProviderId: number, dsId: string): Promise<string> {
60+
const webSessionResponse = await this.$httpClient.httpRequest({
61+
url: "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/v1/session/webSession",
62+
method: "POST",
63+
body: JSON.stringify({
64+
contentProviderId,
65+
dsId,
66+
ipAddress: null
67+
}),
68+
headers: {
69+
'Accept': 'application/json, text/plain, */*',
70+
'Accept-Encoding': 'gzip, deflate, br',
71+
'X-Csrf-Itc': 'itc',
72+
'Content-Type': 'application/json;charset=UTF-8',
73+
'Cookie': this.$applePortalCookieService.getUserSessionCookie()
74+
}
75+
});
76+
77+
const webSessionCookie = this.$applePortalCookieService.getWebSessionCookie(webSessionResponse.headers["set-cookie"]);
78+
79+
return webSessionCookie;
80+
}
81+
82+
private async getLoginConfig(): Promise<{authServiceUrl: string, authServiceKey: string}> {
83+
let config = null;
84+
85+
try {
86+
const response = await this.$httpClient.httpRequest({ url: this.loginConfigEndpoint, method: "GET" });
87+
config = JSON.parse(response.body);
88+
} catch (err) {
89+
this.$logger.trace(`Error while executing request to ${this.loginConfigEndpoint}. More info: ${err}`);
90+
}
91+
92+
return config || this.defaultLoginConfig;
93+
}
94+
}
95+
$injector.register("applePortalSessionService", ApplePortalSessionService);
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
interface IApplePortalSessionService {
2+
createUserSession(credentials: ICredentials): Promise<IApplePortalUserDetail>;
3+
createWebSession(contentProviderId: number, dsId: string): Promise<string>;
4+
}
5+
6+
interface IApplePortalCookieService {
7+
getWebSessionCookie(cookiesData: string[]): string;
8+
getUserSessionCookie(): string;
9+
updateUserSessionCookie(cookie: string[]): void;
10+
}
11+
12+
interface IApplePortalApplicationService {
13+
getApplications(credentials: ICredentials): Promise<IApplePortalApplicationSummary[]>
14+
getApplicationsByProvider(contentProviderId: number, dsId: string): Promise<IApplePortalApplication>;
15+
getApplicationByBundleId(credentials: ICredentials, bundleId: string): Promise<IApplePortalApplicationSummary>;
16+
}
17+
18+
interface IApplePortalUserDetail {
19+
associatedAccounts: IApplePortalAssociatedAccountData[];
20+
sessionToken: {
21+
dsId: string;
22+
contentProviderId: number;
23+
ipAddress: string;
24+
}
25+
contentProviderFeatures: string[];
26+
contentProviderId: number;
27+
firstname: string;
28+
displayName: string;
29+
userName: string;
30+
userId: string;
31+
contentProvider: string;
32+
visibility: boolean;
33+
DYCVisibility: boolean;
34+
}
35+
36+
interface IApplePortalAssociatedAccountData {
37+
contentProvider: {
38+
name: string;
39+
contentProviderId: number;
40+
contentProviderPublicId: string;
41+
contentProviderTypes: string[];
42+
};
43+
roles: string[];
44+
lastLogin: number;
45+
}
46+
47+
interface IApplePortalApplication {
48+
summaries: IApplePortalApplicationSummary[];
49+
showSharedSecret: boolean;
50+
macBundlesEnabled: boolean;
51+
canCreateMacApps: boolean;
52+
cloudStorageEnabled: boolean;
53+
sharedSecretLink: string;
54+
gameCenterGroupLink: string;
55+
enabledPlatforms: string[];
56+
cloudStorageLink: string;
57+
catalogReportsLink: string;
58+
canCreateIOSApps: boolean;
59+
}
60+
61+
interface IApplePortalApplicationSummary {
62+
name: string;
63+
adamId: string;
64+
vendorId: string;
65+
bundleId: string;
66+
appType: any;
67+
versionSets: any[];
68+
lastModifiedDate: number;
69+
iconUrl: string;
70+
issuesCount: number;
71+
priceTier: string;
72+
}

0 commit comments

Comments
 (0)