Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/man_pages/publishing/apple-login.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<% if (isJekyll) { %>---
title: tns apple-login
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 [<Apple ID>] [<Password>]`

### Arguments

* `<Apple ID>` and `<Password>` are your credentials for logging into iTunes Connect.

<% if(isHtml) { %>s

### 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.
<% } %>
3 changes: 3 additions & 0 deletions docs/man_pages/publishing/appstore-upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) { %>
Expand All @@ -22,6 +23,8 @@ Upload package | `$ tns appstore upload [<Apple ID> [<Password>]] --ipa <Ipa Fil
### Options

* `--ipa` - If set, will use provided .ipa file instead of building the project.
* `--appleApplicationSpecificPassword` - Specifies the password for accessing the information you store in iTunes Transporter application.
* `--appleSessionBase64` - The session that will be used instead of triggering a new login each time NativeScript CLI communicates with Apple's APIs.

### Arguments

Expand Down
5 changes: 4 additions & 1 deletion docs/man_pages/publishing/publish-ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ position: 3
### 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 publish ios`<% } %>

Expand All @@ -24,7 +25,9 @@ Upload package | `$ tns publish ios [<Apple ID> [<Password>]] --ipa <Ipa File Pa

* `--ipa` - If set, will use provided .ipa file instead of building the project.
* `--team-id` - Specified the team id for which Xcode will try to find distribution certificate and provisioning profile when exporting for AppStore submission.

* `--appleApplicationSpecificPassword` - Specifies the password for accessing the information you store in iTunes Transporter application.
* `--appleSessionBase64` - The session that will be used instead of triggering a new login each time NativeScript CLI communicates with Apple's APIs.

### Arguments

* `<Apple ID>` and `<Password>` are your credentials for logging into iTunes Connect.
Expand Down
1 change: 1 addition & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
34 changes: 34 additions & 0 deletions lib/commands/apple-login.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
13 changes: 11 additions & 2 deletions lib/commands/appstore-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ 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,
private $platformValidationService: IPlatformValidationService,
private $errors: IErrors,
private $prompter: IPrompter) {
private $prompter: IPrompter,
private $options: IOptions) {
this.$projectData.initializeProjectData();
}

Expand All @@ -31,7 +33,14 @@ 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 }, {
sessionBase64: this.$options.appleSessionBase64,
});
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.");
Expand Down
15 changes: 13 additions & 2 deletions lib/commands/appstore-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
requireInteractiveConsole: 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");
}
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions lib/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> = {
Expand Down
4 changes: 3 additions & 1 deletion lib/common/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,9 @@ private defaultUserAgent: string;
this.$logger.error(`You can run ${EOL}\t${clientNameLowerCase} proxy set <url> <username> <password>.${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 {
Expand Down
10 changes: 9 additions & 1 deletion lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,8 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai
analyticsLogFile: string;
performance: Object;
cleanupLogFile: string;
appleApplicationSpecificPassword: string;
appleSessionBase64: string;
}

interface IEnvOptions {
Expand Down Expand Up @@ -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}
Expand Down
4 changes: 3 additions & 1 deletion lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
};
}

Expand Down
9 changes: 4 additions & 5 deletions lib/services/apple-portal/apple-portal-application-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ export class ApplePortalApplicationService implements IApplePortalApplicationSer
private $httpClient: Server.IHttpClient
) { }

public async getApplications(credentials: ICredentials): Promise<IApplePortalApplicationSummary[]> {
public async getApplications(user: IApplePortalUserDetail): Promise<IApplePortalApplicationSummary[]> {
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;
Expand Down Expand Up @@ -36,10 +35,10 @@ export class ApplePortalApplicationService implements IApplePortalApplicationSer
return JSON.parse(response.body).data;
}

public async getApplicationByBundleId(credentials: ICredentials, bundleId: string): Promise<IApplePortalApplicationSummary> {
const applications = await this.getApplications(credentials);
public async getApplicationByBundleId(user: IApplePortalUserDetail, bundleId: string): Promise<IApplePortalApplicationSummary> {
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);
Expand Down
4 changes: 2 additions & 2 deletions lib/services/apple-portal/apple-portal-cookie-service.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 };
}
}
Expand Down
Loading