mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-30 13:03:53 +01:00
Merge pull request #558 from bitwarden/feature/tax-info-collection
Feature/tax info collection
This commit is contained in:
commit
87973e9775
2
jslib
2
jslib
@ -1 +1 @@
|
|||||||
Subproject commit 28d21ca5dfacd1487cd92345d8be9554079fbd96
|
Subproject commit 635a6fd0d8758384021d674d9140266f9976e9e0
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
aria-hidden="true">×</span></button>
|
aria-hidden="true">×</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>
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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) +
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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) {
|
||||||
|
313
src/app/settings/tax-info.component.html
Normal file
313
src/app/settings/tax-info.component.html
Normal 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>
|
132
src/app/settings/tax-info.component.ts
Normal file
132
src/app/settings/tax-info.component.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user