This commit is contained in:
Ben Schwartz 2022-05-12 14:18:30 -04:00
parent e3fda57b25
commit 1295d3d9e7
7 changed files with 53 additions and 54 deletions

View file

@ -49,7 +49,7 @@ export type Account = Readonly<{
email: string;
uuid: string;
email_verified: boolean;
status: string;
status: 'active' | 'warning' | 'locked';
status_message: string;
}>;

View file

@ -66,6 +66,8 @@
"error-do-account-info": "Failed to get DigitalOcean account information",
"error-do-auth": "Authentication with DigitalOcean failed",
"error-do-regions": "Failed to get list of available regions",
"error-do-limit": "Your DigitalOcean account has reached its Droplet limit. You can request an increase at https://cloud.digitalocean.com/account/team/droplet_limit_increase",
"error-do-warning": "DigitalOcean warning:",
"error-gcp-auth": "Authentication with Google Cloud Platform failed",
"error-feedback": "Failed to submit feedback. Please try again.",
"error-hostname-invalid": "Must be an IP address or valid hostname.",

View file

@ -319,6 +319,14 @@
"message": "Failed to get list of available regions",
"description": "This string appears in an error notification toast. It is shown when there is an error retrieving the regions available for server deployment."
},
"error_do_limit": {
"message": "Your DigitalOcean account has reached its Droplet limit. You can request an increase at https://cloud.digitalocean.com/account/team/droplet_limit_increase",
"description": "This string appears in an error notification toast. It is shown when the user has created the maximum number of allowed servers."
},
"error_do_warning": {
"message": "DigitalOcean warning:",
"description": "This string appears at the start of an error notification toast. It is followed by an English-language error description in quotes."
},
"error_gcp_auth": {
"message": "Authentication with Google Cloud Platform failed",
"description": "This string appears in an error notification toast. It is shown when there is an error when logging in to the user's Google Cloud Platform account. Google Cloud Platform is a cloud server provider name and should not be translated."

View file

@ -38,15 +38,10 @@ export interface RegionOption extends location.CloudLocationOption {
readonly cloudLocation: Region;
}
export enum Status {
ACTIVE,
EMAIL_UNVERIFIED,
MISSING_BILLING_INFORMATION,
}
export interface AccountInfo {
status: Status;
warning?: string;
export interface Status {
readonly needsBillingInfo: boolean;
readonly needsEmailVerification: boolean;
readonly warning?: string;
}
export interface Account {
@ -55,7 +50,7 @@ export interface Account {
// Returns a user-friendly name (email address) associated with the account.
getName(): Promise<string>;
// Returns the status of the account.
getStatus(): Promise<AccountInfo>;
getStatus(): Promise<Status>;
// Lists all existing Shadowboxes. If `fetchFromHost` is true, performs a network request to
// retrieve the servers; otherwise resolves with a cached server list.
listServers(fetchFromHost?: boolean): Promise<ManagedServer[]>;
@ -64,6 +59,7 @@ export interface Account {
// Creates a server and returning it when it becomes active (i.e. the server has
// created, not necessarily once shadowbox installation has finished).
createServer(region: Region, name: string): Promise<ManagedServer>;
// Returns true if the account has reached its Droplet limit and
// will not be allowed to create new droplets.
hasReachedLimit(): Promise<boolean>;
}

View file

@ -353,10 +353,13 @@ export class App {
id: this.digitalOceanAccount.getId(),
name: await this.digitalOceanAccount.getName(),
};
const info = await this.digitalOceanAccount.getStatus();
if (info.status !== digitalocean.Status.ACTIVE) {
const status = await this.digitalOceanAccount.getStatus();
if (status.needsBillingInfo || status.needsEmailVerification) {
return [];
}
if (status.warning) {
this.showDigitalOceanWarning(status.warning);
}
const servers = await this.digitalOceanAccount.listServers();
for (const server of servers) {
this.addServer(this.digitalOceanAccount.getId(), server);
@ -370,6 +373,10 @@ export class App {
return [];
}
private showDigitalOceanWarning(warning: string) {
this.appRoot.showError(`${this.appRoot.localize('error-do-warning')} ${warning}`);
}
private async loadGcpAccount(gcpAccount: gcp.Account): Promise<server.ManagedServer[]> {
if (!gcpAccount) {
return [];
@ -505,16 +512,20 @@ export class App {
};
const oauthUi = this.appRoot.getDigitalOceanOauthFlow(signOutAction);
for (;;) {
const info = await this.digitalOceanRetry(async () => {
const status = await this.digitalOceanRetry(async () => {
if (cancelled) {
throw CANCELLED_ERROR;
}
return await digitalOceanAccount.getStatus();
});
if (info.warning) {
this.appRoot.showError(`DigitalOcean warning: ${info.warning}`);
}
if (info.status === digitalocean.Status.ACTIVE) {
if (status.needsBillingInfo) {
oauthUi.showBilling();
} else if (status.needsEmailVerification) {
oauthUi.showEmailVerification();
} else {
if (status.warning) {
this.showDigitalOceanWarning(status.warning);
}
bringToFront();
if (activatingAccount) {
// Show the 'account active' screen for a few seconds if the account was activated
@ -526,11 +537,6 @@ export class App {
}
this.appRoot.showDigitalOceanOauthFlow();
activatingAccount = true;
if (info.status === digitalocean.Status.MISSING_BILLING_INFORMATION) {
oauthUi.showBilling();
} else {
oauthUi.showEmailVerification();
}
await sleep(1000);
if (this.appRoot.currentPage !== 'digitalOceanOauth') {
// The user navigated away.
@ -725,10 +731,8 @@ export class App {
try {
if (await digitalOceanAccount.hasReachedLimit()) {
this.appRoot.showError(
'Your DigitalOcean account has reached its Droplet limit. You can request an increase at https://cloud.digitalocean.com/account/team/droplet_limit_increase'
);
return;
this.appRoot.showError(this.appRoot.localize('error-do-limit'));
return; // Don't proceed to the region picker.
}
} catch (e) {
console.error('Failed to check droplet limit status', e);

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Account, DigitalOceanSession, DropletInfo, RestApiSession} from '../cloud/digitalocean_api';
import {DigitalOceanSession, DropletInfo, RestApiSession} from '../cloud/digitalocean_api';
import * as crypto from '../infrastructure/crypto';
import * as do_install_script from '../install_scripts/do_install_script';
import * as digitalocean from '../model/digitalocean';
@ -46,31 +46,20 @@ export class DigitalOceanAccount implements digitalocean.Account {
return (await this.digitalOcean.getAccount())?.email;
}
async getStatus(): Promise<digitalocean.AccountInfo> {
async getStatus(): Promise<digitalocean.Status> {
const account = await this.digitalOcean.getAccount();
return {
status: await this.summarizeStatus(account),
warning: account.status !== 'active' ? account.status_message : '',
};
}
private async summarizeStatus(account: Account): Promise<digitalocean.Status> {
if (account.status === 'active') {
return digitalocean.Status.ACTIVE;
const needsEmailVerification = !account.email_verified;
let needsBillingInfo = false;
if (!needsEmailVerification && account.status === 'locked') {
// If the account is locked for no discernible reason, and there are no droplets,
// assume the billing info is missing.
const droplets = await this.digitalOcean.getDroplets();
if (droplets.length == 0) {
needsBillingInfo = true;
}
}
if (!account.email_verified) {
return digitalocean.Status.EMAIL_UNVERIFIED;
}
if (account.status === 'warning') {
return digitalocean.Status.ACTIVE;
}
const servers = await this.digitalOcean.getDroplets();
if (servers.length > 0) {
// The account is locked, but it has droplets so it must have
// been activated with billing information.
return digitalocean.Status.ACTIVE;
}
return digitalocean.Status.MISSING_BILLING_INFORMATION;
const warning = account.status !== 'active' ? account.status_message : '';
return {needsBillingInfo, needsEmailVerification, warning};
}
// Return a list of regions indicating whether they are available and support

View file

@ -28,8 +28,8 @@ export class FakeDigitalOceanAccount implements digitalocean.Account {
async getName(): Promise<string> {
return 'fake-digitalocean-account-name';
}
async getStatus(): Promise<digitalocean.AccountInfo> {
return {status: digitalocean.Status.ACTIVE};
async getStatus(): Promise<digitalocean.Status> {
return {needsBillingInfo: false, needsEmailVerification: false};
}
listServers() {
return Promise.resolve(this.servers);