From 0b1e75051d50f1c5ae5f59af0c40059028131633 Mon Sep 17 00:00:00 2001 From: fatme Date: Thu, 25 Jul 2019 16:36:40 +0300 Subject: [PATCH 1/5] feat: support accounts with two-factor authentication on publish --- lib/commands/appstore-list.ts | 8 +- lib/commands/appstore-upload.ts | 15 +- lib/common/constants.ts | 1 + lib/common/http-client.ts | 4 +- lib/declarations.d.ts | 10 +- lib/options.ts | 4 +- .../apple-portal-application-service.ts | 9 +- .../apple-portal-cookie-service.ts | 4 +- .../apple-portal-session-service.ts | 165 ++++++++++++++---- lib/services/apple-portal/definitions.d.ts | 23 ++- lib/services/itmstransporter-service.ts | 13 +- 11 files changed, 206 insertions(+), 50 deletions(-) diff --git a/lib/commands/appstore-list.ts b/lib/commands/appstore-list.ts index 94acbabec3..1704ecc5b6 100644 --- a/lib/commands/appstore-list.ts +++ b/lib/commands/appstore-list.ts @@ -6,6 +6,7 @@ export class ListiOSApps implements ICommand { constructor(private $injector: IInjector, private $applePortalApplicationService: IApplePortalApplicationService, + private $applePortalSessionService: IApplePortalSessionService, private $logger: ILogger, private $projectData: IProjectData, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, @@ -31,7 +32,12 @@ export class ListiOSApps implements ICommand { password = await this.$prompter.getPassword("Apple ID password"); } - const applications = await this.$applePortalApplicationService.getApplications({ username, password }); + const user = await this.$applePortalSessionService.createUserSession({ username, password }); + if (!user.areCredentialsValid) { + this.$errors.failWithoutHelp(`Invalid username and password combination. Used '${username}' as the username.`); + } + + const applications = await this.$applePortalApplicationService.getApplications(user); if (!applications || !applications.length) { this.$logger.info("Seems you don't have any applications yet."); diff --git a/lib/commands/appstore-upload.ts b/lib/commands/appstore-upload.ts index e8dfc56508..9d239cedd6 100644 --- a/lib/commands/appstore-upload.ts +++ b/lib/commands/appstore-upload.ts @@ -8,6 +8,7 @@ export class PublishIOS implements ICommand { new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)]; constructor( + private $applePortalSessionService: IApplePortalSessionService, private $injector: IInjector, private $itmsTransporterService: IITMSTransporterService, private $logger: ILogger, @@ -38,6 +39,15 @@ export class PublishIOS implements ICommand { password = await this.$prompter.getPassword("Apple ID password"); } + const user = await this.$applePortalSessionService.createUserSession({ username, password }, { + applicationSpecificPassword: this.$options.appleApplicationSpecificPassword, + sessionBase64: this.$options.appleSessionBase64, + ensureConsoleIsInteractive: true + }); + if (!user.areCredentialsValid) { + this.$errors.failWithoutHelp(`Invalid username and password combination. Used '${username}' as the username.`); + } + if (!mobileProvisionIdentifier && !ipaFilePath) { this.$logger.warn("No mobile provision identifier set. A default mobile provision will be used. You can set one in app/App_Resources/iOS/build.xcconfig"); } @@ -69,8 +79,9 @@ export class PublishIOS implements ICommand { } await this.$itmsTransporterService.upload({ - username, - password, + credentials: { username, password }, + user, + applicationSpecificPassword: this.$options.appleApplicationSpecificPassword, ipaFilePath, shouldExtractIpa: !!this.$options.ipa, verboseLogging: this.$logger.getLevel() === "TRACE" diff --git a/lib/common/constants.ts b/lib/common/constants.ts index 699647df64..c49c5aa15c 100644 --- a/lib/common/constants.ts +++ b/lib/common/constants.ts @@ -95,6 +95,7 @@ export class HttpStatusCodes { static NOT_MODIFIED = 304; static PAYMENT_REQUIRED = 402; static PROXY_AUTHENTICATION_REQUIRED = 407; + static CONFLICTING_RESOURCE = 409; } export const HttpProtocolToPort: IDictionary = { diff --git a/lib/common/http-client.ts b/lib/common/http-client.ts index 5c970b7eea..fe2dd0b105 100644 --- a/lib/common/http-client.ts +++ b/lib/common/http-client.ts @@ -255,7 +255,9 @@ private defaultUserAgent: string; this.$logger.error(`You can run ${EOL}\t${clientNameLowerCase} proxy set .${EOL}In order to supply ${clientNameLowerCase} with the credentials needed.`); return "Your proxy requires authentication."; } else if (statusCode === HttpStatusCodes.PAYMENT_REQUIRED) { - return util.format("Your subscription has expired."); + return "Your subscription has expired."; + } else if (statusCode === HttpStatusCodes.CONFLICTING_RESOURCE) { + return "The request conflicts with the current state of the server."; } else { this.$logger.trace("Request was unsuccessful. Server returned: ", body); try { diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 389e2c17b7..f7bf0994fe 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -568,6 +568,8 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai analyticsLogFile: string; performance: Object; cleanupLogFile: string; + appleApplicationSpecificPassword: string; + appleSessionBase64: string; } interface IEnvOptions { @@ -609,7 +611,13 @@ interface IAndroidResourcesMigrationService { /** * Describes properties needed for uploading a package to iTunes Connect */ -interface IITMSData extends ICredentials { +interface IITMSData { + credentials: ICredentials; + + user: IApplePortalUserDetail; + + applicationSpecificPassword: string; + /** * Path to a .ipa file which will be uploaded. * @type {string} diff --git a/lib/options.ts b/lib/options.ts index 4c9d1fa83f..f2fe7bfa2f 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -145,7 +145,9 @@ export class Options { hooks: { type: OptionType.Boolean, default: true, hasSensitiveValue: false }, link: { type: OptionType.Boolean, default: false, hasSensitiveValue: false }, aab: { type: OptionType.Boolean, hasSensitiveValue: false }, - performance: { type: OptionType.Object, hasSensitiveValue: true } + performance: { type: OptionType.Object, hasSensitiveValue: true }, + appleApplicationSpecificPassword: { type: OptionType.String, hasSensitiveValue: true }, + appleSessionBase64: { type: OptionType.String, hasSensitiveValue: true }, }; } diff --git a/lib/services/apple-portal/apple-portal-application-service.ts b/lib/services/apple-portal/apple-portal-application-service.ts index 791e420bb1..56c3194043 100644 --- a/lib/services/apple-portal/apple-portal-application-service.ts +++ b/lib/services/apple-portal/apple-portal-application-service.ts @@ -5,10 +5,9 @@ export class ApplePortalApplicationService implements IApplePortalApplicationSer private $httpClient: Server.IHttpClient ) { } - public async getApplications(credentials: ICredentials): Promise { + public async getApplications(user: IApplePortalUserDetail): Promise { let result: IApplePortalApplicationSummary[] = []; - const user = await this.$applePortalSessionService.createUserSession(credentials); for (const account of user.associatedAccounts) { const contentProviderId = account.contentProvider.contentProviderId; const dsId = user.sessionToken.dsId; @@ -36,10 +35,10 @@ export class ApplePortalApplicationService implements IApplePortalApplicationSer return JSON.parse(response.body).data; } - public async getApplicationByBundleId(credentials: ICredentials, bundleId: string): Promise { - const applications = await this.getApplications(credentials); + public async getApplicationByBundleId(user: IApplePortalUserDetail, bundleId: string): Promise { + const applications = await this.getApplications(user); if (!applications || !applications.length) { - this.$errors.failWithoutHelp(`Cannot find any registered applications for Apple ID ${credentials.username} in iTunes Connect.`); + this.$errors.failWithoutHelp(`Cannot find any registered applications for Apple ID ${user.userName} in iTunes Connect.`); } const application = _.find(applications, app => app.bundleId === bundleId); diff --git a/lib/services/apple-portal/apple-portal-cookie-service.ts b/lib/services/apple-portal/apple-portal-cookie-service.ts index 241652cd82..f58d6701ba 100644 --- a/lib/services/apple-portal/apple-portal-cookie-service.ts +++ b/lib/services/apple-portal/apple-portal-cookie-service.ts @@ -1,6 +1,6 @@ export class ApplePortalCookieService implements IApplePortalCookieService { private userSessionCookies: IStringDictionary = {}; - private validUserSessionCookieNames = ["myacinfo", "dqsid", "itctx", "itcdq", "acn01"]; + private validUserSessionCookieNames = ["myacinfo", "dqsid", "itctx", "itcdq", "acn01", "DES"]; private validWebSessionCookieNames = ["wosid", "woinst", "itctx"]; public getWebSessionCookie(cookiesData: string[]): string { @@ -29,7 +29,7 @@ export class ApplePortalCookieService implements IApplePortalCookieService { for (const cookie of parts) { const trimmedCookie = cookie.trim(); const [cookieKey, cookieValue] = trimmedCookie.split("="); - if (_.includes(validCookieNames, cookieKey)) { + if (_.includes(validCookieNames, cookieKey) || _.some(validCookieNames, validCookieName => cookieKey.startsWith(validCookieName))) { result[cookieKey] = { key: cookieKey, value: cookieValue, cookie: trimmedCookie }; } } diff --git a/lib/services/apple-portal/apple-portal-session-service.ts b/lib/services/apple-portal/apple-portal-session-service.ts index 83adf60272..12da130f86 100644 --- a/lib/services/apple-portal/apple-portal-session-service.ts +++ b/lib/services/apple-portal/apple-portal-session-service.ts @@ -1,3 +1,5 @@ +import { isInteractive } from "../../common/helpers"; + export class ApplePortalSessionService implements IApplePortalSessionService { private loginConfigEndpoint = "https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com"; private defaultLoginConfig = { @@ -7,42 +9,33 @@ export class ApplePortalSessionService implements IApplePortalSessionService { constructor( private $applePortalCookieService: IApplePortalCookieService, + private $errors: IErrors, private $httpClient: Server.IHttpClient, - private $logger: ILogger + private $logger: ILogger, + private $prompter: IPrompter ) { } - public async createUserSession(credentials: ICredentials): Promise { - const loginConfig = await this.getLoginConfig(); - const loginUrl = `${loginConfig.authServiceUrl}/auth/signin`; - const loginResponse = await this.$httpClient.httpRequest({ - url: loginUrl, - method: "POST", - body: JSON.stringify({ - accountName: credentials.username, - password: credentials.password, - rememberMe: true - }), - headers: { - 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest', - 'X-Apple-Widget-Key': loginConfig.authServiceKey, - 'Accept': 'application/json, text/javascript' - } - }); - - this.$applePortalCookieService.updateUserSessionCookie(loginResponse.headers["set-cookie"]); + public async createUserSession(credentials: ICredentials, opts?: IAppleCreateUserSessionOptions): Promise { + const loginResult = await this.login(credentials, opts); - const sessionResponse = await this.$httpClient.httpRequest({ - url: "https://appstoreconnect.apple.com/olympus/v1/session", - method: "GET", - headers: { - 'Cookie': this.$applePortalCookieService.getUserSessionCookie() + if (!opts || !opts.sessionBase64) { + if (loginResult.isTwoFactorAuthenticationEnabled) { + const authServiceKey = (await this.getLoginConfig()).authServiceKey; + await this.handleTwoFactorAuthentication(loginResult.scnt, loginResult.xAppleIdSessionId, authServiceKey); } - }); - this.$applePortalCookieService.updateUserSessionCookie(sessionResponse.headers["set-cookie"]); + const sessionResponse = await this.$httpClient.httpRequest({ + url: "https://appstoreconnect.apple.com/olympus/v1/session", + method: "GET", + headers: { + 'Cookie': this.$applePortalCookieService.getUserSessionCookie() + } + }); + + this.$applePortalCookieService.updateUserSessionCookie(sessionResponse.headers["set-cookie"]); + } - const userDetailResponse = await this.$httpClient.httpRequest({ + const userDetailsResponse = await this.$httpClient.httpRequest({ url: "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/user/detail", method: "GET", headers: { @@ -51,9 +44,12 @@ export class ApplePortalSessionService implements IApplePortalSessionService { } }); - this.$applePortalCookieService.updateUserSessionCookie(userDetailResponse.headers["set-cookie"]); + this.$applePortalCookieService.updateUserSessionCookie(userDetailsResponse.headers["set-cookie"]); - return JSON.parse(userDetailResponse.body).data; + const userdDetails = JSON.parse(userDetailsResponse.body).data; + const result = { ...userdDetails, ...loginResult, userSessionCookie: this.$applePortalCookieService.getUserSessionCookie() }; + + return result; } public async createWebSession(contentProviderId: number, dsId: string): Promise { @@ -79,6 +75,72 @@ export class ApplePortalSessionService implements IApplePortalSessionService { return webSessionCookie; } + private async login(credentials: ICredentials, opts?: IAppleCreateUserSessionOptions): Promise { + const result = { + scnt: null, + xAppleIdSessionId: null, + isTwoFactorAuthenticationEnabled: false, + areCredentialsValid: true + }; + + if (opts && opts.sessionBase64) { + const decodedSession = Buffer.from(opts.sessionBase64, "base64").toString("utf8"); + + this.$applePortalCookieService.updateUserSessionCookie([decodedSession]); + + result.isTwoFactorAuthenticationEnabled = decodedSession.indexOf("DES") > -1; + } else { + try { + await this.loginCore(credentials); + } catch (err) { + const statusCode = err && err.response && err.response.statusCode; + result.areCredentialsValid = statusCode !== 401 && statusCode !== 403; + result.isTwoFactorAuthenticationEnabled = statusCode === 409; + if (result.isTwoFactorAuthenticationEnabled && opts && !opts.applicationSpecificPassword) { + this.$errors.failWithoutHelp(`Your account has two-factor authentication enabled but --appleApplicationSpecificPassword option is not provided. +To generate an application-specific password, please go to https://appleid.apple.com/account/manage. +This password will be used for the iTunes Transporter, which is used to upload your application.`); + } + + if (result.isTwoFactorAuthenticationEnabled && opts && opts.ensureConsoleIsInteractive && !isInteractive()) { + this.$errors.failWithoutHelp(`Your account has two-factor authentication enabled, but your console is not interactive. +For more details how to set up your environment, please execute "tns publish ios --help".`); + } + + const headers = (err && err.response && err.response.headers) || {}; + result.scnt = headers.scnt; + result.xAppleIdSessionId = headers['x-apple-id-session-id']; + } + } + + return result; + } + + private async loginCore(credentials: ICredentials): Promise { + const loginConfig = await this.getLoginConfig(); + const loginUrl = `${loginConfig.authServiceUrl}/auth/signin`; + const headers = { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-Apple-Widget-Key': loginConfig.authServiceKey, + 'Accept': 'application/json, text/javascript' + }; + const body = JSON.stringify({ + accountName: credentials.username, + password: credentials.password, + rememberMe: true + }); + + const loginResponse = await this.$httpClient.httpRequest({ + url: loginUrl, + method: "POST", + body, + headers + }); + + this.$applePortalCookieService.updateUserSessionCookie(loginResponse.headers["set-cookie"]); + } + private async getLoginConfig(): Promise<{authServiceUrl: string, authServiceKey: string}> { let config = null; @@ -91,5 +153,46 @@ export class ApplePortalSessionService implements IApplePortalSessionService { return config || this.defaultLoginConfig; } + + private async handleTwoFactorAuthentication(scnt: string, xAppleIdSessionId: string, authServiceKey: string): Promise { + const headers = { + 'scnt': scnt, + 'X-Apple-Id-Session-Id': xAppleIdSessionId, + 'X-Apple-Widget-Key': authServiceKey, + 'Accept': 'application/json' + }; + const authResponse = await this.$httpClient.httpRequest({ + url: "https://idmsa.apple.com/appleauth/auth", + method: "GET", + headers + }); + + const data = JSON.parse(authResponse.body); + if (data.trustedPhoneNumbers && data.trustedPhoneNumbers.length) { + const parsedAuthResponse = JSON.parse(authResponse.body); + const token = await this.$prompter.getString(`Please enter the ${parsedAuthResponse.securityCode.length} digit code`, { allowEmpty: false }); + + await this.$httpClient.httpRequest({ + url: `https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode`, + method: "POST", + body: JSON.stringify({ + securityCode: { + code: token.toString() + } + }), + headers: { ...headers, 'Content-Type': "application/json" } + }); + + const authTrustResponse = await this.$httpClient.httpRequest({ + url: "https://idmsa.apple.com/appleauth/auth/2sv/trust", + method: "GET", + headers + }); + + this.$applePortalCookieService.updateUserSessionCookie(authTrustResponse.headers["set-cookie"]); + } else { + this.$errors.failWithoutHelp(`Although response from Apple indicated activated Two-step Verification or Two-factor Authentication, NativeScript CLI don't know how to handle this response: ${data}`); + } + } } $injector.register("applePortalSessionService", ApplePortalSessionService); diff --git a/lib/services/apple-portal/definitions.d.ts b/lib/services/apple-portal/definitions.d.ts index fdaf16968e..d53865ff54 100644 --- a/lib/services/apple-portal/definitions.d.ts +++ b/lib/services/apple-portal/definitions.d.ts @@ -1,6 +1,6 @@ interface IApplePortalSessionService { - createUserSession(credentials: ICredentials): Promise; createWebSession(contentProviderId: number, dsId: string): Promise; + createUserSession(credentials: ICredentials, opts?: IAppleCreateUserSessionOptions): Promise; } interface IApplePortalCookieService { @@ -10,12 +10,25 @@ interface IApplePortalCookieService { } interface IApplePortalApplicationService { - getApplications(credentials: ICredentials): Promise + getApplications(user: IApplePortalUserDetail): Promise getApplicationsByProvider(contentProviderId: number, dsId: string): Promise; - getApplicationByBundleId(credentials: ICredentials, bundleId: string): Promise; + getApplicationByBundleId(user: IApplePortalUserDetail, bundleId: string): Promise; } -interface IApplePortalUserDetail { +interface IAppleCreateUserSessionOptions { + applicationSpecificPassword: string; + sessionBase64: string; + ensureConsoleIsInteractive: boolean; +} + +interface IAppleLoginResult { + scnt: string; + xAppleIdSessionId: string; + isTwoFactorAuthenticationEnabled: boolean; + areCredentialsValid: boolean; +} + +interface IApplePortalUserDetail extends IAppleLoginResult { associatedAccounts: IApplePortalAssociatedAccountData[]; sessionToken: { dsId: string; @@ -29,8 +42,10 @@ interface IApplePortalUserDetail { userName: string; userId: string; contentProvider: string; + authServiceKey: string; visibility: boolean; DYCVisibility: boolean; + userSessionCookie: string; } interface IApplePortalAssociatedAccountData { diff --git a/lib/services/itmstransporter-service.ts b/lib/services/itmstransporter-service.ts index c129e9332f..fc3cfc2bc7 100644 --- a/lib/services/itmstransporter-service.ts +++ b/lib/services/itmstransporter-service.ts @@ -28,7 +28,7 @@ export class ITMSTransporterService implements IITMSTransporterService { const ipaFileLocation = path.join(innerDirectory, ipaFileName); const loggingLevel = data.verboseLogging ? ITMSConstants.VerboseLoggingLevels.Verbose : ITMSConstants.VerboseLoggingLevels.Informational; const bundleId = await this.getBundleIdentifier(data); - const application = await this.$applePortalApplicationService.getApplicationByBundleId(data, bundleId); + const application = await this.$applePortalApplicationService.getApplicationByBundleId(data.user, bundleId); this.$fs.createDirectory(innerDirectory); @@ -40,7 +40,16 @@ export class ITMSTransporterService implements IITMSTransporterService { this.$fs.writeFile(path.join(innerDirectory, ITMSConstants.ApplicationMetadataFile), metadata); - await this.$childProcess.spawnFromEvent(itmsTransporterPath, ["-m", "upload", "-f", itmsDirectory, "-u", quoteString(data.username), "-p", quoteString(data.password), "-v", loggingLevel], "close", { stdio: "inherit" }); + const password = data.user.isTwoFactorAuthenticationEnabled ? data.applicationSpecificPassword : data.credentials.password; + await this.$childProcess.spawnFromEvent(itmsTransporterPath, + [ + "-m", "upload", + "-f", itmsDirectory, + "-u", quoteString(data.credentials.username), + "-p", quoteString(password), + "-v", loggingLevel + ], + "close", { stdio: "inherit" }); } private async getBundleIdentifier(data: IITMSData): Promise { From 802e8e6ccd12e2dbae3eb2b992118b277e89e016 Mon Sep 17 00:00:00 2001 From: fatme Date: Thu, 25 Jul 2019 16:37:51 +0300 Subject: [PATCH 2/5] feat: add apple-login command --- lib/bootstrap.ts | 1 + lib/commands/apple-login.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 lib/commands/apple-login.ts diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 00cbc57b25..704c759f47 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -100,6 +100,7 @@ $injector.requireCommand("dev-generate-help", "./commands/generate-help"); $injector.requireCommand("appstore|*list", "./commands/appstore-list"); $injector.requireCommand("appstore|upload", "./commands/appstore-upload"); $injector.requireCommand("publish|ios", "./commands/appstore-upload"); +$injector.requireCommand("apple-login", "./commands/apple-login"); $injector.require("itmsTransporterService", "./services/itmstransporter-service"); $injector.requireCommand("setup|*", "./commands/setup"); diff --git a/lib/commands/apple-login.ts b/lib/commands/apple-login.ts new file mode 100644 index 0000000000..3fcd3a6076 --- /dev/null +++ b/lib/commands/apple-login.ts @@ -0,0 +1,34 @@ +import { StringCommandParameter } from "../common/command-params"; + +export class AppleLogin implements ICommand { + public allowedParameters: ICommandParameter[] = [new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)]; + + constructor( + private $applePortalSessionService: IApplePortalSessionService, + private $errors: IErrors, + private $injector: IInjector, + private $logger: ILogger, + private $prompter: IPrompter + ) { } + + public async execute(args: string[]): Promise { + let username = args[0]; + if (!username) { + username = await this.$prompter.getString("Apple ID", { allowEmpty: false }); + } + + let password = args[1]; + if (!password) { + password = await this.$prompter.getPassword("Apple ID password"); + } + + const user = await this.$applePortalSessionService.createUserSession({ username, password }); + if (!user.areCredentialsValid) { + this.$errors.failWithoutHelp(`Invalid username and password combination. Used '${username}' as the username.`); + } + + const output = Buffer.from(user.userSessionCookie).toString("base64"); + this.$logger.info(output); + } +} +$injector.registerCommand("apple-login", AppleLogin); From a6e17e53638a4ab241792d43a0844f5cfc97221b Mon Sep 17 00:00:00 2001 From: fatme Date: Thu, 25 Jul 2019 16:38:04 +0300 Subject: [PATCH 3/5] test: fix unit tests --- test/tns-appstore-upload.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/tns-appstore-upload.ts b/test/tns-appstore-upload.ts index 053b784c1b..9c5fa90520 100644 --- a/test/tns-appstore-upload.ts +++ b/test/tns-appstore-upload.ts @@ -71,6 +71,13 @@ class AppStore { return this.iOSPlatformData; } }, + "applePortalSessionService": { + createUserSession: () => { + return { + areCredentialsValid: true + }; + } + } } }); @@ -120,8 +127,8 @@ class AppStore { this.itmsTransporterService.upload = (options: IITMSData) => { this.itmsTransporterServiceUploadCalls++; chai.assert.equal(options.ipaFilePath, "/Users/person/git/MyProject/platforms/ios/archive/MyProject.ipa"); - chai.assert.equal(options.username, AppStore.itunesconnect.user); - chai.assert.equal(options.password, AppStore.itunesconnect.pass); + chai.assert.equal(options.credentials.username, AppStore.itunesconnect.user); + chai.assert.equal(options.credentials.password, AppStore.itunesconnect.pass); chai.assert.equal(options.verboseLogging, false); return Promise.resolve(); }; From 9ca5ba5f181f8d036cea3f35664d97dc0ec582d2 Mon Sep 17 00:00:00 2001 From: fatme Date: Fri, 26 Jul 2019 13:38:57 +0300 Subject: [PATCH 4/5] docs: add help for new options and command --- docs/man_pages/publishing/apple-login.md | 39 ++++++++++++++++++++ docs/man_pages/publishing/appstore-upload.md | 2 + docs/man_pages/publishing/publish-ios.md | 4 +- 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 docs/man_pages/publishing/apple-login.md diff --git a/docs/man_pages/publishing/apple-login.md b/docs/man_pages/publishing/apple-login.md new file mode 100644 index 0000000000..fd74d704d6 --- /dev/null +++ b/docs/man_pages/publishing/apple-login.md @@ -0,0 +1,39 @@ +<% if (isJekyll) { %>--- +title: tns appstore +position: 5 +---<% } %> + +# tns apple-login + +### Description + +Uses the provided Apple credentials to obtain Apple session which can be used when publishing to Apple AppStore. + +### Commands + +Usage | Synopsis +---|--- +General | `$ tns apple-login [] []` + +<% if((isConsole && isMacOS) || isHtml) { %> + +### Options + +### Arguments + +* `` and `` are your credentials for logging into iTunes Connect. + +### Command Limitations + +### Related Commands + +Command | Description +----------|---------- +[appstore](appstore.html) | Lists applications registered in iTunes Connect. +[appstore upload](appstore-upload.html) | Uploads project to iTunes Connect. +[build](../project/testing/build.html) | Builds the project for the selected target platform and produces an application package that you can manually deploy on device or in the native emulator. +[build ios](../project/testing/build-ios.html) | Builds the project for iOS and produces an APP or IPA that you can manually deploy in the iOS Simulator or on device, respectively. +[deploy](../project/testing/deploy.html) | Builds and deploys the project to a connected physical or virtual device. +[run](../project/testing/run.html) | Runs your project on a connected device or in the native emulator for the selected platform. +[run ios](../project/testing/run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. +<% } %> \ No newline at end of file diff --git a/docs/man_pages/publishing/appstore-upload.md b/docs/man_pages/publishing/appstore-upload.md index 60dbe1f214..121966a8c7 100644 --- a/docs/man_pages/publishing/appstore-upload.md +++ b/docs/man_pages/publishing/appstore-upload.md @@ -22,6 +22,8 @@ Upload package | `$ tns appstore upload [ []] --ipa []] --ipa ` and `` are your credentials for logging into iTunes Connect. From b63f34ed428f90a90900260869421a0b909b2b36 Mon Sep 17 00:00:00 2001 From: fatme Date: Tue, 30 Jul 2019 08:18:24 +0300 Subject: [PATCH 5/5] fix: fix PR comments --- docs/man_pages/publishing/apple-login.md | 8 ++------ docs/man_pages/publishing/appstore-upload.md | 5 +++-- docs/man_pages/publishing/publish-ios.md | 5 +++-- lib/commands/appstore-list.ts | 7 +++++-- lib/commands/appstore-upload.ts | 2 +- lib/services/apple-portal/apple-portal-session-service.ts | 2 +- lib/services/apple-portal/definitions.d.ts | 4 ++-- 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/man_pages/publishing/apple-login.md b/docs/man_pages/publishing/apple-login.md index fd74d704d6..4e14cb6046 100644 --- a/docs/man_pages/publishing/apple-login.md +++ b/docs/man_pages/publishing/apple-login.md @@ -1,5 +1,5 @@ <% if (isJekyll) { %>--- -title: tns appstore +title: tns apple-login position: 5 ---<% } %> @@ -15,15 +15,11 @@ Usage | Synopsis ---|--- General | `$ tns apple-login [] []` -<% if((isConsole && isMacOS) || isHtml) { %> - -### Options - ### Arguments * `` and `` are your credentials for logging into iTunes Connect. -### Command Limitations +<% if(isHtml) { %>s ### Related Commands diff --git a/docs/man_pages/publishing/appstore-upload.md b/docs/man_pages/publishing/appstore-upload.md index 121966a8c7..67df8607ee 100644 --- a/docs/man_pages/publishing/appstore-upload.md +++ b/docs/man_pages/publishing/appstore-upload.md @@ -8,6 +8,7 @@ position: 1 ### Description Uploads project to iTunes Connect. The command either issues a production build and uploads it to iTunes Connect, or uses an already built package to upload. +The user will be prompted interactively for verification code when two-factor authentication enabled account is used. As on non-interactive console (CI), you will not be prompt for verification code. In this case, you need to generate a login session for your apple's account in advance using `tns apple-login` command. The generated value must be provided via the `--appleSessionBase64` option and is only valid for up to a month. Meaning you'll need to create a new session every month. <% if(isConsole && (isLinux || isWindows)) { %>WARNING: You can run this command only on macOS systems. To view the complete help for this command, run `$ tns help appstore upload`<% } %> <% if((isConsole && isMacOS) || isHtml) { %> @@ -22,8 +23,8 @@ Upload package | `$ tns appstore upload [ []] --ipa WARNING: You can run this command only on macOS systems. To view the complete help for this command, run `$ tns help publish ios`<% } %> @@ -24,8 +25,8 @@ Upload package | `$ tns publish ios [ []] --ipa