1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-27 12:36:14 +01:00

Merge pull request #558 from bitwarden/feature/tax-info-collection

Feature/tax info collection
This commit is contained in:
Chad Scharf 2020-06-18 11:33:08 -04:00 committed by GitHub
commit 87973e9775
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 534 additions and 7 deletions

2
jslib

@ -1 +1 @@
Subproject commit 28d21ca5dfacd1487cd92345d8be9554079fbd96 Subproject commit 635a6fd0d8758384021d674d9140266f9976e9e0

View File

@ -115,6 +115,7 @@ import { PremiumComponent } from './settings/premium.component';
import { ProfileComponent } from './settings/profile.component'; import { ProfileComponent } from './settings/profile.component';
import { PurgeVaultComponent } from './settings/purge-vault.component'; import { PurgeVaultComponent } from './settings/purge-vault.component';
import { SettingsComponent } from './settings/settings.component'; import { SettingsComponent } from './settings/settings.component';
import { TaxInfoComponent } from './settings/tax-info.component';
import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authenticator.component'; import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authenticator.component';
import { TwoFactorDuoComponent } from './settings/two-factor-duo.component'; import { TwoFactorDuoComponent } from './settings/two-factor-duo.component';
import { TwoFactorEmailComponent } from './settings/two-factor-email.component'; import { TwoFactorEmailComponent } from './settings/two-factor-email.component';
@ -350,6 +351,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
ShareComponent, ShareComponent,
StopClickDirective, StopClickDirective,
StopPropDirective, StopPropDirective,
TaxInfoComponent,
ToolsComponent, ToolsComponent,
TrueFalseValueDirective, TrueFalseValueDirective,
TwoFactorAuthenticatorComponent, TwoFactorAuthenticatorComponent,

View File

@ -49,9 +49,17 @@
<h1>{{'taxInformation' | i18n}}</h1> <h1>{{'taxInformation' | i18n}}</h1>
</div> </div>
<p>{{'taxInformationDesc' | i18n}}</p> <p>{{'taxInformationDesc' | i18n}}</p>
<a href="https://bitwarden.com/contact/" target="_blank" rel="noopener" class="btn btn-outline-secondary"> <div *ngIf="!org || loading">
{{'contactSupport' | i18n}} <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</a> <span class="sr-only">{{'loading' | i18n}}</span>
</div>
<form *ngIf="org && !loading" #formTax (ngSubmit)="submitTaxInfo()" [appApiAction]="taxFormPromise" ngNativeValidate>
<app-tax-info></app-tax-info>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="formTax.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
</form>
<div class="secondary-header text-danger border-0 mb-0"> <div class="secondary-header text-danger border-0 mb-0">
<h1>{{'dangerZone' | i18n}}</h1> <h1>{{'dangerZone' | i18n}}</h1>
</div> </div>

View File

@ -18,6 +18,7 @@ import { OrganizationResponse } from 'jslib/models/response/organizationResponse
import { ModalComponent } from '../../modal.component'; import { ModalComponent } from '../../modal.component';
import { PurgeVaultComponent } from '../../settings/purge-vault.component'; import { PurgeVaultComponent } from '../../settings/purge-vault.component';
import { TaxInfoComponent } from '../../settings/tax-info.component';
import { ApiKeyComponent } from './api-key.component'; import { ApiKeyComponent } from './api-key.component';
import { DeleteOrganizationComponent } from './delete-organization.component'; import { DeleteOrganizationComponent } from './delete-organization.component';
import { RotateApiKeyComponent } from './rotate-api-key.component'; import { RotateApiKeyComponent } from './rotate-api-key.component';
@ -31,11 +32,13 @@ export class AccountComponent {
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef; @ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
@ViewChild('apiKeyTemplate', { read: ViewContainerRef }) apiKeyModalRef: ViewContainerRef; @ViewChild('apiKeyTemplate', { read: ViewContainerRef }) apiKeyModalRef: ViewContainerRef;
@ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef }) rotateApiKeyModalRef: ViewContainerRef; @ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef }) rotateApiKeyModalRef: ViewContainerRef;
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
loading = true; loading = true;
canUseApi = false; canUseApi = false;
org: OrganizationResponse; org: OrganizationResponse;
formPromise: Promise<any>; formPromise: Promise<any>;
taxFormPromise: Promise<any>;
private organizationId: string; private organizationId: string;
private modal: ModalComponent = null; private modal: ModalComponent = null;
@ -71,6 +74,13 @@ export class AccountComponent {
} catch { } } catch { }
} }
async submitTaxInfo() {
this.taxFormPromise = this.taxInfo.submitTaxInfo();
await this.taxFormPromise;
this.analytics.eventTrack.next({ action: 'Updated Organization Tax Info' });
this.toasterService.popAsync('success', null, this.i18nService.t('taxInfoUpdated'));
}
deleteOrganization() { deleteOrganization() {
if (this.modal != null) { if (this.modal != null) {
this.modal.close(); this.modal.close();

View File

@ -4,6 +4,7 @@
aria-hidden="true">&times;</span></button> aria-hidden="true">&times;</span></button>
<h3 class="card-body-header">{{(currentType != null ? 'changePaymentMethod' : 'addPaymentMethod') | i18n}}</h3> <h3 class="card-body-header">{{(currentType != null ? 'changePaymentMethod' : 'addPaymentMethod') | i18n}}</h3>
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment> <app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span> <span>{{'submit' | i18n}}</span>

View File

@ -17,6 +17,7 @@ import { PaymentRequest } from 'jslib/models/request/paymentRequest';
import { PaymentMethodType } from 'jslib/enums/paymentMethodType'; import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
import { PaymentComponent } from './payment.component'; import { PaymentComponent } from './payment.component';
import { TaxInfoComponent } from './tax-info.component';
@Component({ @Component({
selector: 'app-adjust-payment', selector: 'app-adjust-payment',
@ -24,6 +25,7 @@ import { PaymentComponent } from './payment.component';
}) })
export class AdjustPaymentComponent { export class AdjustPaymentComponent {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
@Input() currentType?: PaymentMethodType; @Input() currentType?: PaymentMethodType;
@Input() organizationId: string; @Input() organizationId: string;
@ -42,9 +44,17 @@ export class AdjustPaymentComponent {
this.formPromise = this.paymentComponent.createPaymentToken().then((result) => { this.formPromise = this.paymentComponent.createPaymentToken().then((result) => {
request.paymentToken = result[0]; request.paymentToken = result[0];
request.paymentMethodType = result[1]; request.paymentMethodType = result[1];
request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
request.country = this.taxInfoComponent.taxInfo.country;
if (this.organizationId == null) { if (this.organizationId == null) {
return this.apiService.postAccountPayment(request); return this.apiService.postAccountPayment(request);
} else { } else {
request.taxId = this.taxInfoComponent.taxInfo.taxId;
request.state = this.taxInfoComponent.taxInfo.state;
request.line1 = this.taxInfoComponent.taxInfo.line1;
request.line2 = this.taxInfoComponent.taxInfo.line2;
request.city = this.taxInfoComponent.taxInfo.city;
request.state = this.taxInfoComponent.taxInfo.state;
return this.apiService.postOrganizationPayment(this.organizationId, request); return this.apiService.postOrganizationPayment(this.organizationId, request);
} }
}); });
@ -60,4 +70,16 @@ export class AdjustPaymentComponent {
cancel() { cancel() {
this.onCanceled.emit(); this.onCanceled.emit();
} }
changeCountry() {
if (this.taxInfoComponent.taxInfo.country === 'US') {
this.paymentComponent.hideBank = !this.organizationId;
} else {
this.paymentComponent.hideBank = true;
if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
}
} }

View File

@ -202,6 +202,7 @@
<small class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small> <small class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small>
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2> <h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
<app-payment [hideCredit]="true"></app-payment> <app-payment [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
</ng-container> </ng-container>
<ng-container *ngIf="!createOrganization"> <ng-container *ngIf="!createOrganization">
<app-payment [showMethods]="false"></app-payment> <app-payment [showMethods]="false"></app-payment>

View File

@ -19,6 +19,7 @@ import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SyncService } from 'jslib/abstractions/sync.service'; import { SyncService } from 'jslib/abstractions/sync.service';
import { PaymentComponent } from './payment.component'; import { PaymentComponent } from './payment.component';
import { TaxInfoComponent } from './tax-info.component';
import { PlanType } from 'jslib/enums/planType'; import { PlanType } from 'jslib/enums/planType';
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest'; import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
@ -30,6 +31,7 @@ import { OrganizationUpgradeRequest } from 'jslib/models/request/organizationUpg
}) })
export class OrganizationPlansComponent { export class OrganizationPlansComponent {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
@Input() organizationId: string; @Input() organizationId: string;
@Input() showFree = true; @Input() showFree = true;
@ -153,6 +155,15 @@ export class OrganizationPlansComponent {
} else { } else {
request.planType = this.plans[this.plan].annualPlanType; request.planType = this.plans[this.plan].annualPlanType;
} }
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
if (this.taxComponent.taxInfo.includeTaxId) {
request.taxIdNumber = this.taxComponent.taxInfo.taxId;
request.billingAddressLine1 = this.taxComponent.taxInfo.line1;
request.billingAddressLine2 = this.taxComponent.taxInfo.line2;
request.billingAddressCity = this.taxComponent.taxInfo.city;
request.billingAddressState = this.taxComponent.taxInfo.state;
}
} }
const response = await this.apiService.postOrganization(request); const response = await this.apiService.postOrganization(request);
orgId = response.id; orgId = response.id;
@ -262,6 +273,16 @@ export class OrganizationPlansComponent {
return 0; return 0;
} }
changedCountry() {
this.paymentComponent.hideBank = this.taxComponent.taxInfo.country !== 'US';
// Bank Account payments are only available for US customers
if (this.paymentComponent.hideBank &&
this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
get total(): number { get total(): number {
const annual = this.interval === 'year'; const annual = this.interval === 'year';
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) + return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) +

View File

@ -198,8 +198,8 @@ export class PaymentComponent implements OnInit {
return; return;
} }
const handleCardPayment = () => this.showMethods ? const handleCardPayment = () => this.showMethods ?
this.stripe.handleCardPayment(clientSecret, this.stripeCardNumberElement) : this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement) :
this.stripe.handleCardPayment(clientSecret); this.stripe.handleCardSetup(clientSecret);
return handleCardPayment().then(async (result: any) => { return handleCardPayment().then(async (result: any) => {
if (result.error) { if (result.error) {
reject(result.error.message); reject(result.error.message);

View File

@ -75,6 +75,7 @@
<small class="text-muted font-italic">{{'paymentChargedAnnually' | i18n}}</small> <small class="text-muted font-italic">{{'paymentChargedAnnually' | i18n}}</small>
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2> <h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
<app-payment [hideBank]="true"></app-payment> <app-payment [hideBank]="true"></app-payment>
<app-tax-info></app-tax-info>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span> <span>{{'submit' | i18n}}</span>

View File

@ -17,6 +17,7 @@ import { TokenService } from 'jslib/abstractions/token.service';
import { UserService } from 'jslib/abstractions/user.service'; import { UserService } from 'jslib/abstractions/user.service';
import { PaymentComponent } from './payment.component'; import { PaymentComponent } from './payment.component';
import { TaxInfoComponent } from './tax-info.component';
@Component({ @Component({
selector: 'app-premium', selector: 'app-premium',
@ -24,6 +25,7 @@ import { PaymentComponent } from './payment.component';
}) })
export class PremiumComponent implements OnInit { export class PremiumComponent implements OnInit {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
canAccessPremium = false; canAccessPremium = false;
selfHosted = false; selfHosted = false;
@ -83,6 +85,8 @@ export class PremiumComponent implements OnInit {
fd.append('paymentToken', result[0]); fd.append('paymentToken', result[0]);
} }
fd.append('additionalStorageGb', (this.additionalStorage || 0).toString()); fd.append('additionalStorageGb', (this.additionalStorage || 0).toString());
fd.append('country', this.taxInfoComponent.taxInfo.country);
fd.append('postalCode', this.taxInfoComponent.taxInfo.postalCode);
return this.apiService.postPremium(fd); return this.apiService.postPremium(fd);
}).then((paymentResponse) => { }).then((paymentResponse) => {
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) { if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {

View File

@ -0,0 +1,313 @@
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="addressCountry">{{'country' | i18n}}</label>
<select id="addressCountry" class="form-control" [(ngModel)]="taxInfo.country" required name="addressCountry"
autocomplete="country" (change)="changeCountry()">
<option value="">-- Select --</option>
<option value="US">United States</option>
<option value="CN">China</option>
<option value="FR">France</option>
<option value="DE">Germany</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
<option value="IN">India</option>
<option value="-" disabled></option>
<option value="AF">Afghanistan</option>
<option value="AX">Åland Islands</option>
<option value="AL">Albania</option>
<option value="DZ">Algeria</option>
<option value="AS">American Samoa</option>
<option value="AD">Andorra</option>
<option value="AO">Angola</option>
<option value="AI">Anguilla</option>
<option value="AQ">Antarctica</option>
<option value="AG">Antigua and Barbuda</option>
<option value="AR">Argentina</option>
<option value="AM">Armenia</option>
<option value="AW">Aruba</option>
<option value="AT">Austria</option>
<option value="AZ">Azerbaijan</option>
<option value="BS">Bahamas</option>
<option value="BH">Bahrain</option>
<option value="BD">Bangladesh</option>
<option value="BB">Barbados</option>
<option value="BY">Belarus</option>
<option value="BE">Belgium</option>
<option value="BZ">Belize</option>
<option value="BJ">Benin</option>
<option value="BM">Bermuda</option>
<option value="BT">Bhutan</option>
<option value="BO">Bolivia, Plurinational State of</option>
<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
<option value="BA">Bosnia and Herzegovina</option>
<option value="BW">Botswana</option>
<option value="BV">Bouvet Island</option>
<option value="BR">Brazil</option>
<option value="IO">British Indian Ocean Territory</option>
<option value="BN">Brunei Darussalam</option>
<option value="BG">Bulgaria</option>
<option value="BF">Burkina Faso</option>
<option value="BI">Burundi</option>
<option value="KH">Cambodia</option>
<option value="CM">Cameroon</option>
<option value="CV">Cape Verde</option>
<option value="KY">Cayman Islands</option>
<option value="CF">Central African Republic</option>
<option value="TD">Chad</option>
<option value="CL">Chile</option>
<option value="CX">Christmas Island</option>
<option value="CC">Cocos (Keeling) Islands</option>
<option value="CO">Colombia</option>
<option value="KM">Comoros</option>
<option value="CG">Congo</option>
<option value="CD">Congo, the Democratic Republic of the</option>
<option value="CK">Cook Islands</option>
<option value="CR">Costa Rica</option>
<option value="CI">Côte d'Ivoire</option>
<option value="HR">Croatia</option>
<option value="CU">Cuba</option>
<option value="CW">Curaçao</option>
<option value="CY">Cyprus</option>
<option value="CZ">Czech Republic</option>
<option value="DK">Denmark</option>
<option value="DJ">Djibouti</option>
<option value="DM">Dominica</option>
<option value="DO">Dominican Republic</option>
<option value="EC">Ecuador</option>
<option value="EG">Egypt</option>
<option value="SV">El Salvador</option>
<option value="GQ">Equatorial Guinea</option>
<option value="ER">Eritrea</option>
<option value="EE">Estonia</option>
<option value="ET">Ethiopia</option>
<option value="FK">Falkland Islands (Malvinas)</option>
<option value="FO">Faroe Islands</option>
<option value="FJ">Fiji</option>
<option value="FI">Finland</option>
<option value="GF">French Guiana</option>
<option value="PF">French Polynesia</option>
<option value="TF">French Southern Territories</option>
<option value="GA">Gabon</option>
<option value="GM">Gambia</option>
<option value="GE">Georgia</option>
<option value="GH">Ghana</option>
<option value="GI">Gibraltar</option>
<option value="GR">Greece</option>
<option value="GL">Greenland</option>
<option value="GD">Grenada</option>
<option value="GP">Guadeloupe</option>
<option value="GU">Guam</option>
<option value="GT">Guatemala</option>
<option value="GG">Guernsey</option>
<option value="GN">Guinea</option>
<option value="GW">Guinea-Bissau</option>
<option value="GY">Guyana</option>
<option value="HT">Haiti</option>
<option value="HM">Heard Island and McDonald Islands</option>
<option value="VA">Holy See (Vatican City State)</option>
<option value="HN">Honduras</option>
<option value="HK">Hong Kong</option>
<option value="HU">Hungary</option>
<option value="IS">Iceland</option>
<option value="ID">Indonesia</option>
<option value="IR">Iran, Islamic Republic of</option>
<option value="IQ">Iraq</option>
<option value="IE">Ireland</option>
<option value="IM">Isle of Man</option>
<option value="IL">Israel</option>
<option value="IT">Italy</option>
<option value="JM">Jamaica</option>
<option value="JP">Japan</option>
<option value="JE">Jersey</option>
<option value="JO">Jordan</option>
<option value="KZ">Kazakhstan</option>
<option value="KE">Kenya</option>
<option value="KI">Kiribati</option>
<option value="KP">Korea, Democratic People's Republic of</option>
<option value="KR">Korea, Republic of</option>
<option value="KW">Kuwait</option>
<option value="KG">Kyrgyzstan</option>
<option value="LA">Lao People's Democratic Republic</option>
<option value="LV">Latvia</option>
<option value="LB">Lebanon</option>
<option value="LS">Lesotho</option>
<option value="LR">Liberia</option>
<option value="LY">Libya</option>
<option value="LI">Liechtenstein</option>
<option value="LT">Lithuania</option>
<option value="LU">Luxembourg</option>
<option value="MO">Macao</option>
<option value="MK">Macedonia, the former Yugoslav Republic of</option>
<option value="MG">Madagascar</option>
<option value="MW">Malawi</option>
<option value="MY">Malaysia</option>
<option value="MV">Maldives</option>
<option value="ML">Mali</option>
<option value="MT">Malta</option>
<option value="MH">Marshall Islands</option>
<option value="MQ">Martinique</option>
<option value="MR">Mauritania</option>
<option value="MU">Mauritius</option>
<option value="YT">Mayotte</option>
<option value="MX">Mexico</option>
<option value="FM">Micronesia, Federated States of</option>
<option value="MD">Moldova, Republic of</option>
<option value="MC">Monaco</option>
<option value="MN">Mongolia</option>
<option value="ME">Montenegro</option>
<option value="MS">Montserrat</option>
<option value="MA">Morocco</option>
<option value="MZ">Mozambique</option>
<option value="MM">Myanmar</option>
<option value="NA">Namibia</option>
<option value="NR">Nauru</option>
<option value="NP">Nepal</option>
<option value="NL">Netherlands</option>
<option value="NC">New Caledonia</option>
<option value="NZ">New Zealand</option>
<option value="NI">Nicaragua</option>
<option value="NE">Niger</option>
<option value="NG">Nigeria</option>
<option value="NU">Niue</option>
<option value="NF">Norfolk Island</option>
<option value="MP">Northern Mariana Islands</option>
<option value="NO">Norway</option>
<option value="OM">Oman</option>
<option value="PK">Pakistan</option>
<option value="PW">Palau</option>
<option value="PS">Palestinian Territory, Occupied</option>
<option value="PA">Panama</option>
<option value="PG">Papua New Guinea</option>
<option value="PY">Paraguay</option>
<option value="PE">Peru</option>
<option value="PH">Philippines</option>
<option value="PN">Pitcairn</option>
<option value="PL">Poland</option>
<option value="PT">Portugal</option>
<option value="PR">Puerto Rico</option>
<option value="QA">Qatar</option>
<option value="RE">Réunion</option>
<option value="RO">Romania</option>
<option value="RU">Russian Federation</option>
<option value="RW">Rwanda</option>
<option value="BL">Saint Barthélemy</option>
<option value="SH">Saint Helena, Ascension and Tristan da Cunha</option>
<option value="KN">Saint Kitts and Nevis</option>
<option value="LC">Saint Lucia</option>
<option value="MF">Saint Martin (French part)</option>
<option value="PM">Saint Pierre and Miquelon</option>
<option value="VC">Saint Vincent and the Grenadines</option>
<option value="WS">Samoa</option>
<option value="SM">San Marino</option>
<option value="ST">Sao Tome and Principe</option>
<option value="SA">Saudi Arabia</option>
<option value="SN">Senegal</option>
<option value="RS">Serbia</option>
<option value="SC">Seychelles</option>
<option value="SL">Sierra Leone</option>
<option value="SG">Singapore</option>
<option value="SX">Sint Maarten (Dutch part)</option>
<option value="SK">Slovakia</option>
<option value="SI">Slovenia</option>
<option value="SB">Solomon Islands</option>
<option value="SO">Somalia</option>
<option value="ZA">South Africa</option>
<option value="GS">South Georgia and the South Sandwich Islands</option>
<option value="SS">South Sudan</option>
<option value="ES">Spain</option>
<option value="LK">Sri Lanka</option>
<option value="SD">Sudan</option>
<option value="SR">Suriname</option>
<option value="SJ">Svalbard and Jan Mayen</option>
<option value="SZ">Swaziland</option>
<option value="SE">Sweden</option>
<option value="CH">Switzerland</option>
<option value="SY">Syrian Arab Republic</option>
<option value="TW">Taiwan, Province of China</option>
<option value="TJ">Tajikistan</option>
<option value="TZ">Tanzania, United Republic of</option>
<option value="TH">Thailand</option>
<option value="TL">Timor-Leste</option>
<option value="TG">Togo</option>
<option value="TK">Tokelau</option>
<option value="TO">Tonga</option>
<option value="TT">Trinidad and Tobago</option>
<option value="TN">Tunisia</option>
<option value="TR">Turkey</option>
<option value="TM">Turkmenistan</option>
<option value="TC">Turks and Caicos Islands</option>
<option value="TV">Tuvalu</option>
<option value="UG">Uganda</option>
<option value="UA">Ukraine</option>
<option value="AE">United Arab Emirates</option>
<option value="UM">United States Minor Outlying Islands</option>
<option value="UY">Uruguay</option>
<option value="UZ">Uzbekistan</option>
<option value="VU">Vanuatu</option>
<option value="VE">Venezuela, Bolivarian Republic of</option>
<option value="VN">Viet Nam</option>
<option value="VG">Virgin Islands, British</option>
<option value="VI">Virgin Islands, U.S.</option>
<option value="WF">Wallis and Futuna</option>
<option value="EH">Western Sahara</option>
<option value="YE">Yemen</option>
<option value="ZM">Zambia</option>
<option value="ZW">Zimbabwe</option>
</select>
</div>
</div>
<div class="col-3">
<div class="form-group">
<label for="addressPostalCode">{{'zipPostalCode' | i18n}}</label>
<input id="addressPostalCode" class="form-control" type="text" name="addressPostalCode"
[(ngModel)]="taxInfo.postalCode" [required]="taxInfo.country === 'US'" autocomplete="postal-code">
</div>
</div>
<div class="col-6" *ngIf="organizationId && taxInfo.country !== 'US'">
<div class="form-group form-check">
<input class="form-check-input" id="addressIncludeTaxId" name="addressIncludeTaxId" type="checkbox"
[(ngModel)]="taxInfo.includeTaxId">
<label class="form-check-label" for="addressIncludeTaxId">{{'includeVAT' | i18n}}</label>
</div>
</div>
</div>
<div class="row" *ngIf="organizationId && taxInfo.includeTaxId">
<div class="col-6">
<div class="form-group">
<label for="taxId">{{'taxIdNumber' | i18n}}</label>
<input id="taxId" class="form-control" type="text" name="taxId" [(ngModel)]="taxInfo.taxId">
</div>
</div>
</div>
<div class="row" *ngIf="organizationId && taxInfo.includeTaxId">
<div class="col-6">
<div class="form-group">
<label for="addressLine1">{{'address1' | i18n}}</label>
<input id="addressLine1" class="form-control" type="text" name="addressLine1"
[(ngModel)]="taxInfo.line1" autocomplete="address-line1">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressLine2">{{'address2' | i18n}}</label>
<input id="addressLine2" class="form-control" type="text" name="addressLine2"
[(ngModel)]="taxInfo.line2" autocomplete="address-line2">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressCity">{{'cityTown' | i18n}}</label>
<input id="addressCity" class="form-control" type="text" name="addressCity"
[(ngModel)]="taxInfo.city" autocomplete="address-level2">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="addressState">{{'stateProvince' | i18n}}</label>
<input id="addressState" class="form-control" type="text" name="addressState"
[(ngModel)]="taxInfo.state" autocomplete="address-level1">
</div>
</div>
</div>

View File

@ -0,0 +1,132 @@
import {
Component,
EventEmitter,
Output,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { OrganizationTaxInfoUpdateRequest } from 'jslib/models/request/organizationTaxInfoUpdateRequest';
import { TaxInfoUpdateRequest } from 'jslib/models/request/taxInfoUpdateRequest';
@Component({
selector: 'app-tax-info',
templateUrl: 'tax-info.component.html',
})
export class TaxInfoComponent {
@Output() onCountryChanged = new EventEmitter();
loading: boolean = true;
organizationId: string;
taxInfo: any = {
taxId: null,
line1: null,
line2: null,
city: null,
state: null,
postalCode: null,
country: 'US',
includeTaxId: false,
};
private pristine: any = {
taxId: null,
line1: null,
line2: null,
city: null,
state: null,
postalCode: null,
country: 'US',
includeTaxId: false,
};
constructor(private apiService: ApiService, private route: ActivatedRoute) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
if (this.organizationId) {
try {
const taxInfo = await this.apiService.getOrganizationTaxInfo(this.organizationId);
if (taxInfo) {
this.taxInfo.taxId = taxInfo.taxId;
this.taxInfo.state = taxInfo.state;
this.taxInfo.line1 = taxInfo.line1;
this.taxInfo.line2 = taxInfo.line2;
this.taxInfo.city = taxInfo.city;
this.taxInfo.state = taxInfo.state;
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || 'US';
this.taxInfo.includeTaxId = this.taxInfo.country !== 'US' && (
!!taxInfo.taxId
|| !!taxInfo.line1
|| !!taxInfo.line2
|| !!taxInfo.city
|| !!taxInfo.state);
}
} catch { }
} else {
const taxInfo = await this.apiService.getTaxInfo();
if (taxInfo) {
this.taxInfo.postalCode = taxInfo.postalCode;
this.taxInfo.country = taxInfo.country || 'US';
}
}
this.pristine = Object.assign({}, this.taxInfo);
// If not the default (US) then trigger onCountryChanged
if (this.taxInfo.country !== 'US') {
this.onCountryChanged.emit();
}
});
this.loading = false;
}
getTaxInfoRequest(): TaxInfoUpdateRequest {
if (this.organizationId) {
const request = new OrganizationTaxInfoUpdateRequest();
request.taxId = this.taxInfo.taxId;
request.state = this.taxInfo.state;
request.line1 = this.taxInfo.line1;
request.line2 = this.taxInfo.line2;
request.city = this.taxInfo.city;
request.state = this.taxInfo.state;
request.postalCode = this.taxInfo.postalCode;
request.country = this.taxInfo.country;
return request;
} else {
const request = new TaxInfoUpdateRequest();
request.postalCode = this.taxInfo.postalCode;
request.country = this.taxInfo.country;
return request;
}
}
submitTaxInfo(): Promise<any> {
if (!this.hasChanged()) {
return new Promise((resolve) => { resolve(); });
}
const request = this.getTaxInfoRequest();
return this.organizationId ? this.apiService.putOrganizationTaxInfo(this.organizationId,
request as OrganizationTaxInfoUpdateRequest) : this.apiService.putTaxInfo(request);
}
changeCountry() {
if (this.taxInfo.country === 'US') {
this.taxInfo.includeTaxId = false;
this.taxInfo.taxId = null;
this.taxInfo.line1 = null;
this.taxInfo.line2 = null;
this.taxInfo.city = null;
this.taxInfo.state = null;
}
this.onCountryChanged.emit();
}
private hasChanged(): boolean {
for (const key in this.taxInfo) {
if (this.pristine.hasOwnProperty(key) && this.pristine[key] !== this.taxInfo[key]) {
return true;
}
}
return false;
}
}

View File

@ -2643,7 +2643,7 @@
"message": "Tax Information" "message": "Tax Information"
}, },
"taxInformationDesc": { "taxInformationDesc": {
"message": "Please contact support to provide (or update) tax information for your invoices." "message": "For customers within the US, ZIP code is required to satisfy sales tax requirements, for other countries you may optionally provide a tax identification number (VAT/GST) and/or address to appear on your invoices."
}, },
"billingPlan": { "billingPlan": {
"message": "Plan", "message": "Plan",
@ -3151,5 +3151,17 @@
}, },
"hidePasswords": { "hidePasswords": {
"message": "Hide Passwords" "message": "Hide Passwords"
},
"countryPostalCodeRequiredDesc": {
"message": "We require this information for calculating sales tax and financial reporting only."
},
"includeVAT": {
"message": "Include VAT/GST Information (optional)"
},
"taxIdNumber": {
"message": "VAT/GST Tax ID"
},
"taxInfoUpdated": {
"message": "Tax information updated."
} }
} }