diff --git a/src/app/authentication/authentication.module.ts b/src/app/authentication/authentication.module.ts
index eda3084cbe..99cb336db9 100644
--- a/src/app/authentication/authentication.module.ts
+++ b/src/app/authentication/authentication.module.ts
@@ -3,6 +3,9 @@ import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
+import { ScanPayEmailComponent } from 'authentication/scan-pay/email/scan-pay-email.component';
+import { ComponentModule } from 'shared/component/component.module';
+import { FormattersModule } from 'shared/formatters/formatters.module';
import { MaterialModule } from '../app.module';
import { DialogsModule } from '../shared/dialogs/dialogs.module';
@@ -15,6 +18,10 @@ import { AuthenticationLoginComponent } from './login/authentication-login.compo
import { AuthenticationMercedesDataUsageComponent } from './mercedes-data-usage/authentication-mercedes-data-usage.component';
import { AuthenticationRegisterComponent } from './register/authentication-register.component';
import { AuthenticationResetPasswordComponent } from './reset-password/authentication-reset-password.component';
+import { ScanPayInvoiceComponent } from './scan-pay/invoices/scan-pay-invoice.component';
+import { ScanPayComponent } from './scan-pay/scan-pay.component';
+import { ScanPayShowTransactionComponent } from './scan-pay/show-transaction/scan-pay-show-transaction.component';
+import { ScanPayStripePaymentIntentComponent } from './scan-pay/stripe/scan-pay-stripe-payment-intent.component';
import { AuthenticationVerifyEmailComponent } from './verify-email/authentication-verify-email.component';
@NgModule({
@@ -27,6 +34,8 @@ import { AuthenticationVerifyEmailComponent } from './verify-email/authenticatio
TranslateModule,
DialogsModule,
CommonDirectivesModule,
+ ComponentModule,
+ FormattersModule,
],
declarations: [
AuthenticationLoginComponent,
@@ -36,7 +45,12 @@ import { AuthenticationVerifyEmailComponent } from './verify-email/authenticatio
AuthenticationResetPasswordComponent,
AuthenticationDefinePasswordComponent,
AuthenticationVerifyEmailComponent,
- AccountOnboardingComponent
+ AccountOnboardingComponent,
+ ScanPayStripePaymentIntentComponent,
+ ScanPayEmailComponent,
+ ScanPayShowTransactionComponent,
+ ScanPayComponent,
+ ScanPayInvoiceComponent,
],
})
diff --git a/src/app/authentication/authentication.routing.ts b/src/app/authentication/authentication.routing.ts
index c11126f57e..87296a0085 100644
--- a/src/app/authentication/authentication.routing.ts
+++ b/src/app/authentication/authentication.routing.ts
@@ -7,6 +7,10 @@ import { AuthenticationLoginComponent } from './login/authentication-login.compo
import { AuthenticationMercedesDataUsageComponent } from './mercedes-data-usage/authentication-mercedes-data-usage.component';
import { AuthenticationRegisterComponent } from './register/authentication-register.component';
import { AuthenticationResetPasswordComponent } from './reset-password/authentication-reset-password.component';
+import { ScanPayEmailComponent } from './scan-pay/email/scan-pay-email.component';
+import { ScanPayInvoiceComponent } from './scan-pay/invoices/scan-pay-invoice.component';
+import { ScanPayShowTransactionComponent } from './scan-pay/show-transaction/scan-pay-show-transaction.component';
+import { ScanPayStripePaymentIntentComponent } from './scan-pay/stripe/scan-pay-stripe-payment-intent.component';
import { AuthenticationVerifyEmailComponent } from './verify-email/authentication-verify-email.component';
export const AuthenticationRoutes: Routes = [
@@ -37,7 +41,28 @@ export const AuthenticationRoutes: Routes = [
{
path: 'account-onboarding',
component: AccountOnboardingComponent,
- }, {
+ },
+ // Step #1 - flash QR code
+ {
+ path: 'scan-pay/:chargingStationID/:connectorID',
+ component: ScanPayEmailComponent,
+ },
+ // Step #2 - click on the link from the email
+ {
+ path: 'scan-pay',
+ component: ScanPayStripePaymentIntentComponent,
+ },
+ // Step #3 - show and can stop transaction
+ {
+ path: 'scan-pay/stop/:transactionID/:email/:token',
+ component: ScanPayShowTransactionComponent,
+ },
+ // Step #4 - download invoice from email once transaction is billed
+ {
+ path: 'scan-pay/invoice/:invoiceID/download',
+ component: ScanPayInvoiceComponent
+ },
+ {
path: '**',
redirectTo: 'login',
},
diff --git a/src/app/authentication/scan-pay/email/scan-pay-email.component.html b/src/app/authentication/scan-pay/email/scan-pay-email.component.html
new file mode 100644
index 0000000000..323a165b6b
--- /dev/null
+++ b/src/app/authentication/scan-pay/email/scan-pay-email.component.html
@@ -0,0 +1,74 @@
+
+
+
diff --git a/src/app/authentication/scan-pay/email/scan-pay-email.component.ts b/src/app/authentication/scan-pay/email/scan-pay-email.component.ts
new file mode 100644
index 0000000000..8081fbaa7a
--- /dev/null
+++ b/src/app/authentication/scan-pay/email/scan-pay-email.component.ts
@@ -0,0 +1,168 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { AbstractControl, FormControl, UntypedFormGroup, Validators } from '@angular/forms';
+import { ActivatedRoute, Params } from '@angular/router';
+import { StatusCodes } from 'http-status-codes';
+import { ReCaptchaV3Service } from 'ngx-captcha';
+
+import { CentralServerService } from '../../../services/central-server.service';
+import { ConfigService } from '../../../services/config.service';
+import { LocaleService } from '../../../services/locale.service';
+import { SpinnerService } from '../../../services/spinner.service';
+import { WindowService } from '../../../services/window.service';
+import { Constants } from '../../../utils/Constants';
+import { Utils } from '../../../utils/Utils';
+
+@Component({
+ selector: 'app-scan-pay-email',
+ templateUrl: 'scan-pay-email.component.html',
+})
+
+export class ScanPayEmailComponent implements OnInit, OnDestroy {
+ public email: AbstractControl;
+ public name: AbstractControl;
+ public firstName: AbstractControl;
+ public formGroup: UntypedFormGroup;
+ public isSendClicked: boolean;
+ public tenantLogo = Constants.NO_IMAGE;
+ public acceptEula: AbstractControl;
+ public headerClass = 'card-header-primary';
+ public title = 'settings.scan_pay.payment_intent.create_account_title';
+ public message: string;
+ public locale: string;
+
+ private siteKey: string;
+ private subDomain: string;
+ private params: Params;
+
+ public constructor(
+ private centralServerService: CentralServerService,
+ private spinnerService: SpinnerService,
+ private reCaptchaV3Service: ReCaptchaV3Service,
+ private windowService: WindowService,
+ private configService: ConfigService,
+ private activatedRoute: ActivatedRoute,
+ private localeService: LocaleService) {
+ this.localeService.getCurrentLocaleSubject().subscribe((locale) => {
+ this.locale = locale.currentLocaleJS;
+ });
+
+ // Get the Site Key
+ this.siteKey = this.configService.getUser().captchaSiteKey;
+ this.params = this.activatedRoute?.snapshot?.params;
+ // Init Form
+ this.formGroup = new UntypedFormGroup({
+ email: new FormControl(null,
+ Validators.compose([
+ Validators.required,
+ Validators.email,
+ ])),
+ name: new FormControl(null,
+ Validators.compose([
+ Validators.required
+ ])),
+ firstName: new FormControl(null,
+ Validators.compose([
+ Validators.required
+ ])),
+ acceptEula: new FormControl(null,
+ Validators.compose([
+ Validators.required,
+ ])),
+ });
+ // Keep the sub-domain
+ this.subDomain = this.windowService.getSubdomain();
+ // Form
+ this.email = this.formGroup.controls['email'];
+ this.name = this.formGroup.controls['name'];
+ this.firstName = this.formGroup.controls['firstName'];
+ this.acceptEula = this.formGroup.controls['acceptEula'];
+ setTimeout(() => {
+ const card = document.getElementsByClassName('card')[0];
+ // After 700 ms we add the class animated to the login/register card
+ card.classList.remove('card-hidden');
+ }, 700);
+ this.isSendClicked = false;
+ }
+
+ public ngOnInit() {
+ const body = document.getElementsByTagName('body')[0];
+ body.classList.add('lock-page');
+ body.classList.add('off-canvas-sidebar');
+ if (this.subDomain) {
+ // Retrieve tenant's logo
+ this.centralServerService.getTenantLogoBySubdomain(this.subDomain).subscribe({
+ next: (tenantLogo: string) => {
+ if (tenantLogo) {
+ this.tenantLogo = tenantLogo;
+ }
+ },
+ error: (error) => {
+ this.spinnerService.hide();
+ switch (error.status) {
+ case StatusCodes.NOT_FOUND:
+ this.tenantLogo = Constants.NO_IMAGE;
+ break;
+ default:
+ this.isSendClicked = true;
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.unexpected_error_payment_intend';
+ }
+ }
+ });
+ } else {
+ this.tenantLogo = Constants.MASTER_TENANT_LOGO;
+ }
+ }
+
+ public ngOnDestroy() {
+ const body = document.getElementsByTagName('body')[0];
+ body.classList.remove('lock-page');
+ body.classList.remove('off-canvas-sidebar');
+ }
+
+ public toUpperCase(control: AbstractControl) {
+ control.setValue(control.value.toUpperCase());
+ }
+
+ public firstLetterToUpperCase(control: AbstractControl) {
+ if (control.value) {
+ control.setValue(Utils.firstLetterInUpperCase(control.value));
+ }
+ }
+
+ public sendEmailVerificationScanAndPay(data: any) {
+ // Show
+ this.spinnerService.show();
+ this.reCaptchaV3Service.execute(this.siteKey, 'VerifScanPay', (token) => {
+ if (token) {
+ data['captcha'] = token;
+ data['chargingStationID'] = this.params?.chargingStationID;
+ data['connectorID'] = this.params?.connectorID;
+ data['locale'] = this.locale;
+ } else {
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.payment_intent.authentication.invalid_captcha_token';
+ return;
+ }
+ // launch email verif
+ this.centralServerService.scanPayVerifyEmail(data).subscribe({
+ next: (response) => {
+ this.isSendClicked = true;
+ this.spinnerService.hide();
+ this.headerClass = 'card-header-success';
+ this.title = 'settings.scan_pay.payment_intent.create_account_success_title';
+ this.message = 'settings.scan_pay.payment_intent.create_account_success';
+ },
+ error: (error) => {
+ this.isSendClicked = true;
+ this.spinnerService.hide();
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.payment_intent.create_account_error';
+ }
+ });
+ });
+ }
+}
diff --git a/src/app/authentication/scan-pay/invoices/scan-pay-invoice.component.html b/src/app/authentication/scan-pay/invoices/scan-pay-invoice.component.html
new file mode 100644
index 0000000000..b95ce9ab8f
--- /dev/null
+++ b/src/app/authentication/scan-pay/invoices/scan-pay-invoice.component.html
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/app/authentication/scan-pay/invoices/scan-pay-invoice.component.ts b/src/app/authentication/scan-pay/invoices/scan-pay-invoice.component.ts
new file mode 100644
index 0000000000..ed19571385
--- /dev/null
+++ b/src/app/authentication/scan-pay/invoices/scan-pay-invoice.component.ts
@@ -0,0 +1,79 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { TranslateService } from '@ngx-translate/core';
+import FileSaver from 'file-saver';
+
+import { AuthorizationService } from '../../../services/authorization.service';
+import { CentralServerService } from '../../../services/central-server.service';
+import { ComponentService } from '../../../services/component.service';
+import { SpinnerService } from '../../../services/spinner.service';
+import { WindowService } from '../../../services/window.service';
+import { TenantComponents } from '../../../types/Tenant';
+import { User } from '../../../types/User';
+
+@Component({
+ selector: 'app-scan-pay-invoice',
+ templateUrl: 'scan-pay-invoice.component.html',
+})
+export class ScanPayInvoiceComponent implements OnInit {
+ public isBillingComponentActive: boolean;
+ public token: string;
+ public invoiceID: string;
+ public user: Partial;
+ public email: string;
+ public headerClass = 'card-header-primary';
+ public title = 'settings.scan_pay.download_title';
+ public message = 'settings.scan_pay.download';
+
+ public constructor(
+ private centralServerService: CentralServerService,
+ private componentService: ComponentService,
+ private spinnerService: SpinnerService,
+ public translateService: TranslateService,
+ public activatedRoute: ActivatedRoute,
+ public windowService: WindowService,
+ private authorizationService: AuthorizationService) {
+ this.isBillingComponentActive = this.componentService.isActive(TenantComponents.BILLING);
+ this.invoiceID = this.activatedRoute?.snapshot?.params?.['invoiceID'];
+ this.email = this.activatedRoute?.snapshot?.queryParams?.['email'];
+ this.token = this.activatedRoute?.snapshot?.queryParams?.['VerificationToken'];
+ this.user = { email: this.email, verificationToken: this.token, password: this.token, acceptEula: true } as Partial;
+ }
+
+ public ngOnInit(): void {
+ try {
+ this.spinnerService.show();
+ // clear User and UserAuthorization
+ this.authorizationService.cleanUserAndUserAuthorization();
+ this.centralServerService.login(this.user).subscribe({
+ next: (result) => {
+ this.centralServerService.loginSucceeded(result.token);
+ this.centralServerService.downloadInvoice(this.invoiceID).subscribe({
+ next: (resultInvoice) => {
+ FileSaver.saveAs(resultInvoice, `${this.invoiceID + '.pdf'}`);
+ this.spinnerService.hide();
+ },
+ error: (error) => {
+ this.spinnerService.hide();
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'invoices.cannot_download_invoice';
+ }
+ });
+ },
+ error: (error) => {
+ this.spinnerService.hide();
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.unexpected_error_payment_intend';
+ }
+ });
+ } catch (error) {
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.unexpected_error_payment_intend';
+ } finally {
+ this.spinnerService.hide();
+ }
+ }
+}
diff --git a/src/app/authentication/scan-pay/scan-pay.component.html b/src/app/authentication/scan-pay/scan-pay.component.html
new file mode 100644
index 0000000000..4aefc956af
--- /dev/null
+++ b/src/app/authentication/scan-pay/scan-pay.component.html
@@ -0,0 +1,21 @@
+
+
+
diff --git a/src/app/authentication/scan-pay/scan-pay.component.ts b/src/app/authentication/scan-pay/scan-pay.component.ts
new file mode 100644
index 0000000000..0bb836d8b4
--- /dev/null
+++ b/src/app/authentication/scan-pay/scan-pay.component.ts
@@ -0,0 +1,25 @@
+import { Component, Input } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { AbstractTabComponent } from 'shared/component/abstract-tab/abstract-tab.component';
+
+import { AuthorizationService } from '../../services/authorization.service';
+import { ComponentService } from '../../services/component.service';
+import { WindowService } from '../../services/window.service';
+import { TenantComponents } from '../../types/Tenant';
+
+@Component({
+ selector: 'app-scan-pay',
+ templateUrl: 'scan-pay.component.html',
+})
+export class ScanPayComponent extends AbstractTabComponent{
+ @Input() public transactionID!: number;
+
+ public canListTransactions: boolean;
+ public canRefundTransaction: boolean;
+ public canListTransactionsInError: boolean;
+
+ public constructor(
+ activatedRoute: ActivatedRoute, windowService: WindowService) {
+ super(activatedRoute, windowService, ['all']);
+ }
+}
diff --git a/src/app/authentication/scan-pay/show-transaction/scan-pay-show-transaction.component.html b/src/app/authentication/scan-pay/show-transaction/scan-pay-show-transaction.component.html
new file mode 100644
index 0000000000..b0c095ae47
--- /dev/null
+++ b/src/app/authentication/scan-pay/show-transaction/scan-pay-show-transaction.component.html
@@ -0,0 +1,28 @@
+
+
+
diff --git a/src/app/authentication/scan-pay/show-transaction/scan-pay-show-transaction.component.ts b/src/app/authentication/scan-pay/show-transaction/scan-pay-show-transaction.component.ts
new file mode 100644
index 0000000000..dd38295b2d
--- /dev/null
+++ b/src/app/authentication/scan-pay/show-transaction/scan-pay-show-transaction.component.ts
@@ -0,0 +1,140 @@
+import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
+import { ActivatedRoute, Params } from '@angular/router';
+
+import { AuthorizationService } from '../../../services/authorization.service';
+import { CentralServerService } from '../../../services/central-server.service';
+import { SpinnerService } from '../../../services/spinner.service';
+import { TransactionHeaderComponent } from '../../../shared/dialogs/transaction/header/transaction-header.component';
+import { Transaction } from '../../../types/Transaction';
+import { User } from '../../../types/User';
+
+@Component({
+ selector: 'app-scan-pay-show-transaction',
+ templateUrl: 'scan-pay-show-transaction.component.html',
+})
+export class ScanPayShowTransactionComponent implements OnInit, OnDestroy {
+ @ViewChild('transactionHeader') public transactionHeader!: TransactionHeaderComponent;
+
+ @Input() public transactionID!: number;
+
+ public currentTransactionID!: number;
+ public transaction: Transaction;
+ public isSendClicked: boolean;
+ public token: string;
+ public user: Partial;
+ public headerClass = 'card-header-primary';
+ public title = 'settings.scan_pay.stop_title';
+ public message: string;
+ private refreshInterval;
+ private params: Params;
+
+ public constructor(
+ private spinnerService: SpinnerService,
+ private centralServerService: CentralServerService,
+ private authorizationService: AuthorizationService,
+ private activatedRoute: ActivatedRoute) {
+ this.params = this.activatedRoute?.snapshot?.params;
+ this.currentTransactionID = this.params?.transactionID;
+ this.token = this.params?.token;
+ this.user = { email: this.params?.email, verificationToken: this.token, password: this.token, acceptEula: true } as Partial;
+ }
+
+ public ngOnInit(): void {
+ this.login();
+ this.isSendClicked = false;
+ // Load
+ this.loadData();
+ }
+
+ public ngOnDestroy(): void {
+ // Destroy transaction refresh
+ this.destroyTransactionRefresh();
+ }
+
+ public loadData() {
+ this.spinnerService.show();
+ // clear User and UserAuthorization
+ this.authorizationService.cleanUserAndUserAuthorization();
+ this.centralServerService.login(this.user).subscribe({
+ next: (result) => {
+ this.centralServerService.loginSucceeded(result.token);
+ this.centralServerService.getTransaction(this.currentTransactionID).subscribe({
+ next: (transaction: Transaction) => {
+ this.spinnerService.hide();
+ this.transaction = transaction;
+ if (this.transaction?.stop) {
+ this.headerClass = 'card-header-success';
+ this.title = 'settings.scan_pay.stop_success';
+ }
+ },
+ error: (error) => {
+ this.isSendClicked = true;
+ this.spinnerService.hide();
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.load_transaction_error_message';
+ }
+ });
+ },
+ error: (error) => {
+ this.isSendClicked = true;
+ this.spinnerService.hide();
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.unexpected_error_payment_intend';
+ }
+ });
+ }
+
+ public scanPayStopTransaction() {
+ this.isSendClicked = true;
+ const data = {};
+ this.spinnerService.show();
+ data['email'] = this.transaction?.user?.email;
+ data['transactionId'] = this.currentTransactionID;
+ data['token'] = this.token;
+ // launch capture and stop transaction
+ this.centralServerService.chargingStationStopTransaction(this.transaction.chargeBoxID, this.currentTransactionID).subscribe({
+ next: (response) => {
+ this.spinnerService.hide();
+ this.headerClass = 'card-header-success';
+ this.title = 'settings.scan_pay.stop_success';
+ },
+ error: (error) => {
+ this.spinnerService.hide();
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.stop_error';
+ }
+ });
+ }
+
+ private login(): void {
+ this.spinnerService.show();
+ // clear User and UserAuthorization
+ this.authorizationService.cleanUserAndUserAuthorization();
+ // Login
+ this.centralServerService.login(this.user).subscribe({
+ next: (result) => {
+ this.spinnerService.hide();
+ this.centralServerService.loginSucceeded(result.token);
+ },
+ error: (error) => {
+ this.spinnerService.hide();
+ switch (error.status) {
+ default:
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.unexpected_error_payment_intend';
+ }
+ }
+ });
+ }
+
+ private destroyTransactionRefresh() {
+ if (this.refreshInterval) {
+ clearInterval(this.refreshInterval);
+ this.refreshInterval = null;
+ }
+ }
+}
diff --git a/src/app/authentication/scan-pay/stripe/scan-pay-stripe-payment-intent.component.html b/src/app/authentication/scan-pay/stripe/scan-pay-stripe-payment-intent.component.html
new file mode 100644
index 0000000000..f37a4bca25
--- /dev/null
+++ b/src/app/authentication/scan-pay/stripe/scan-pay-stripe-payment-intent.component.html
@@ -0,0 +1,29 @@
+
+
+
diff --git a/src/app/authentication/scan-pay/stripe/scan-pay-stripe-payment-intent.component.scss b/src/app/authentication/scan-pay/stripe/scan-pay-stripe-payment-intent.component.scss
new file mode 100644
index 0000000000..acae8ca62d
--- /dev/null
+++ b/src/app/authentication/scan-pay/stripe/scan-pay-stripe-payment-intent.component.scss
@@ -0,0 +1,21 @@
+@use '@angular/material' as mat;
+@import '../../../../assets/scss/core/variables/theme';
+@import '../../../../assets/scss/core/variables/brand';
+
+.stripe-payment-method-dialog-size {
+ max-width: 500px;
+ max-height: 550px;
+}
+
+.payment-method-user-consent {
+ padding: 1em 1.5em;
+ margin: 1.5em 0em;
+ background: $brand-warning;
+ color: mat.get-color-from-palette($app-grey, 50);
+ text-align: justify;
+}
+
+#prepaymentMessage {
+ font-size: 0.9em;
+ margin-top: 0.5em;
+}
\ No newline at end of file
diff --git a/src/app/authentication/scan-pay/stripe/scan-pay-stripe-payment-intent.component.ts b/src/app/authentication/scan-pay/stripe/scan-pay-stripe-payment-intent.component.ts
new file mode 100644
index 0000000000..4055c21205
--- /dev/null
+++ b/src/app/authentication/scan-pay/stripe/scan-pay-stripe-payment-intent.component.ts
@@ -0,0 +1,235 @@
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormGroup } from '@angular/forms';
+import { TranslateService } from '@ngx-translate/core';
+import { PaymentIntent, PaymentIntentResult, StripeElementLocale, StripeElements, StripeElementsOptions, StripePaymentElement } from '@stripe/stripe-js';
+import { AppCurrencyPipe } from 'shared/formatters/app-currency.pipe';
+
+import { AuthorizationService } from '../../../services/authorization.service';
+import { CentralServerService } from '../../../services/central-server.service';
+import { ComponentService } from '../../../services/component.service';
+import { Locale, LocaleService } from '../../../services/locale.service';
+import { SpinnerService } from '../../../services/spinner.service';
+import { StripeService } from '../../../services/stripe.service';
+import { WindowService } from '../../../services/window.service';
+import { HTTPError } from '../../../types/HTTPError';
+import { TenantComponents } from '../../../types/Tenant';
+import { User } from '../../../types/User';
+
+@Component({
+ selector: 'app-scan-pay-stripe-payment-intent',
+ templateUrl: 'scan-pay-stripe-payment-intent.component.html',
+ styleUrls: ['scan-pay-stripe-payment-intent.component.scss']
+})
+export class ScanPayStripePaymentIntentComponent implements OnInit {
+ public formGroup!: UntypedFormGroup;
+ public isBillingComponentActive: boolean;
+ public locale: Locale;
+ public email: string;
+ public name: string;
+ public firstName: string;
+ public chargingStationID: string;
+ public connectorID: number;
+ public token: string;
+ public isSendClicked = false;
+ public showButton = true;
+ public isTokenValid = true;
+ public headerClass = 'card-header-primary';
+ public title = 'settings.scan_pay.payment_intent.create_title';
+ public message: string;
+ // Stripe elements
+ public elements: StripeElements;
+ public paymentElement: StripePaymentElement;
+ public paymentIntent: PaymentIntent;
+ public prepaymentMessage: string;
+
+ public constructor(
+ private centralServerService: CentralServerService,
+ private componentService: ComponentService,
+ private spinnerService: SpinnerService,
+ private stripeService: StripeService,
+ private localeService: LocaleService,
+ public translateService: TranslateService,
+ public windowService: WindowService,
+ public authorizationService: AuthorizationService,
+ private appCurrencyPipe: AppCurrencyPipe) {
+ this.isBillingComponentActive = this.componentService.isActive(TenantComponents.BILLING);
+ }
+
+ public ngOnInit(): void {
+ this.token = this.windowService.getUrlParameterValue('VerificationToken');
+ this.email = this.windowService.getUrlParameterValue('email');
+ this.name = this.windowService.getUrlParameterValue('name');
+ this.firstName = this.windowService.getUrlParameterValue('firstName');
+ this.chargingStationID = this.windowService.getUrlParameterValue('chargingStationID');
+ this.connectorID = +this.windowService.getUrlParameterValue('connectorID');
+ this.localeService.getCurrentLocaleSubject().subscribe((locale) => {
+ this.locale = locale;
+ });
+ void this.initialize();
+ }
+
+ public linkCardToAccount() {
+ this.showButton = false;
+ void this.doConfirmPaymentIntent();
+ }
+
+ private initialize() {
+ // Allows to show an error if user delete subdomain in the url
+ if (!this.isBillingComponentActive) {
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.billing_not_properly_set';
+ this.isSendClicked = true;
+ }
+ try {
+ this.spinnerService.show();
+ const user = { email: this.email, password: this.token, acceptEula: true } as Partial;
+ // clear User and UserAuthorization
+ this.authorizationService.cleanUserAndUserAuthorization();
+ this.centralServerService.login(user).subscribe({
+ next: async (result) => {
+ this.centralServerService.loginSucceeded(result.token);
+ const stripeFacade = await this.stripeService.initializeStripe();
+ // Step #1 - Create A STRIPE Payment Intent to be able to initialize the payment elements
+ this.paymentIntent = await this.createPaymentIntent() as PaymentIntent;
+ if (!stripeFacade) {
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.billing_not_properly_set_title';
+ this.message = 'settings.scan_pay.billing_not_properly_set';
+ } else {
+ this.initializeElements(this.paymentIntent.client_secret);
+ const amountWithCurrency = this.appCurrencyPipe.transform(this.paymentIntent.amount / 100, this.paymentIntent.currency) + '';
+ this.prepaymentMessage = this.translateService.instant('settings.scan_pay.payment_intent.prepayment', {amount: amountWithCurrency});
+
+ }
+ },
+ error: (error) => {
+ this.spinnerService.hide();
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.billing_not_properly_set_title';
+ this.message = 'settings.scan_pay.billing_not_properly_set';
+ this.isSendClicked = true;
+ }
+ });
+ } catch (error) {
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.unexpected_error_payment_intend';
+ this.isSendClicked = true;
+ } finally {
+ this.spinnerService.hide();
+ }
+ }
+
+ private getStripeFacade() {
+ return this.stripeService.getStripeFacade();
+ }
+
+ private initializeElements(clientSecret: string) {
+ const options: StripeElementsOptions = {
+ locale: this.locale.language as StripeElementLocale,
+ clientSecret
+ };
+ this.elements = this.getStripeFacade().elements(options);
+ this.paymentElement = this.elements.create('payment');
+ this.paymentElement.mount('#payment-element');
+ }
+
+ private async doConfirmPaymentIntent(): Promise {
+ try {
+ this.isSendClicked = true;
+ // Step #2 - Confirm the STRIPE Payment Intent to carry out 3DS authentication (redirects to the bank authentication page)
+ const operationResult: PaymentIntentResult = await this.getStripeFacade().confirmPayment({
+ elements: this.elements,
+ redirect: 'if_required'
+ });
+ if (operationResult?.error) {
+ // 3DS authentication has been aborted or user was not able to authenticate
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.payment_intent.create_error_title';
+ this.message = 'settings.scan_pay.payment_intent.create_error';
+ } else {
+ // Operation succeeded - try to start transaction
+ const operationResultRetrieve: any = await this.retrievePaymentIntentAndStartTransaction();
+ if (operationResultRetrieve) {
+ this.headerClass = 'card-header-success';
+ this.title = 'settings.scan_pay.payment_intent.create_success_title';
+ this.message = 'settings.scan_pay.payment_intent.create_success';
+ }
+ }
+ } catch (error) {
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.unexpected_error_payment_intend';
+ }
+ }
+
+ private async createPaymentIntent() {
+ try {
+ this.spinnerService.show();
+ const response = await this.centralServerService.scanPayHandlePaymentIntent({
+ email: this.email,
+ locale: this.locale.currentLocaleJS,
+ chargingStationID: this.chargingStationID,
+ connectorID: this.connectorID,
+ verificationToken: this.token,
+ }).toPromise();
+ return response?.internalData;
+ } catch (error) {
+ this.spinnerService.hide();
+ switch (error.status) {
+ case HTTPError.INVALID_TOKEN_ERROR:
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.verify_email_token_not_valid_title';
+ this.message = 'settings.scan_pay.verify_email_token_not_valid';
+ this.isTokenValid = false;
+ break;
+ case HTTPError.SCAN_PAY_HOLD_AMOUNT_MISSING:
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.hold_amount_not_set_title';
+ this.message = 'settings.scan_pay.hold_amount_not_set';
+ this.isTokenValid = false;
+ this.isSendClicked = true;
+ break;
+ default:
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.unexpected_error_payment_intend';
+ this.isSendClicked = true;
+ }
+ } finally {
+ this.spinnerService.hide();
+ }
+ }
+
+ private async retrievePaymentIntentAndStartTransaction() {
+ try {
+ this.spinnerService.show();
+ const response = await this.centralServerService.scanPayHandlePaymentIntentRetrieve({
+ email: this.email,
+ locale: this.locale.currentLocaleJS,
+ paymentIntentID: this.paymentIntent?.id,
+ chargingStationID: this.chargingStationID,
+ connectorID: this.connectorID,
+ verificationToken: this.token,
+ }).toPromise();
+ return response;
+ } catch (error) {
+ this.spinnerService.hide();
+ switch (error.status) {
+ case HTTPError.CANNOT_REMOTE_START_CONNECTOR:
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.connector_not_available_title';
+ this.message = 'settings.scan_pay.connector_not_available';
+ break;
+ default:
+ this.headerClass = 'card-header-danger';
+ this.title = 'settings.scan_pay.unexpected_error_title';
+ this.message = 'settings.scan_pay.unexpected_error_payment_intend';
+ }
+ } finally {
+ this.spinnerService.hide();
+ this.isSendClicked = true;
+ }
+ }
+}
diff --git a/src/app/layouts/auth/auth-layout.component.html b/src/app/layouts/auth/auth-layout.component.html
index 5a597e506e..3a44728e72 100644
--- a/src/app/layouts/auth/auth-layout.component.html
+++ b/src/app/layouts/auth/auth-layout.component.html
@@ -12,7 +12,7 @@
{{"general.version" | translate}} {{version}}
-
+
-
-
diff --git a/src/app/pages/charging-stations/charging-station/parameters/connector/charging-station-connector.component.ts b/src/app/pages/charging-stations/charging-station/parameters/connector/charging-station-connector.component.ts
index 0535db4aa2..7666aeba86 100644
--- a/src/app/pages/charging-stations/charging-station/parameters/connector/charging-station-connector.component.ts
+++ b/src/app/pages/charging-stations/charging-station/parameters/connector/charging-station-connector.component.ts
@@ -2,16 +2,17 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angu
import { AbstractControl, UntypedFormArray, UntypedFormControl, UntypedFormGroup, ValidationErrors, Validators } from '@angular/forms';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { Router } from '@angular/router';
-import { CentralServerService } from 'services/central-server.service';
-import { ComponentService } from 'services/component.service';
-import { MessageService } from 'services/message.service';
-import { SpinnerService } from 'services/spinner.service';
-import { QrCodeDialogComponent } from 'shared/dialogs/qr-code/qr-code-dialog.component';
-import { CONNECTOR_TYPE_MAP } from 'shared/formatters/app-connector-type.pipe';
-import { ChargePoint, ChargingStation, Connector, CurrentType, OCPPPhase, Voltage } from 'types/ChargingStation';
-import { Image } from 'types/GlobalType';
-import { TenantComponents } from 'types/Tenant';
-import { Utils } from 'utils/Utils';
+
+import { CentralServerService } from '../../../../../services/central-server.service';
+import { ComponentService } from '../../../../../services/component.service';
+import { MessageService } from '../../../../../services/message.service';
+import { SpinnerService } from '../../../../../services/spinner.service';
+import { QrCodeDialogComponent } from '../../../../../shared/dialogs/qr-code/qr-code-dialog.component';
+import { CONNECTOR_TYPE_MAP } from '../../../../../shared/formatters/app-connector-type.pipe';
+import { ChargePoint, ChargingStation, Connector, CurrentType, OCPPPhase, Voltage } from '../../../../../types/ChargingStation';
+import { Image } from '../../../../../types/GlobalType';
+import { TenantComponents } from '../../../../../types/Tenant';
+import { Utils } from '../../../../../utils/Utils';
@Component({
selector: 'app-charging-station-connector',
@@ -28,6 +29,7 @@ export class ChargingStationConnectorComponent implements OnInit, OnChanges {
@Output() public connectorChanged = new EventEmitter();
public ocpiActive: boolean;
+ public scanPayActive: boolean;
public connectorTypeMap = CONNECTOR_TYPE_MAP;
public connectedPhaseMap = [
{ key: 1, description: 'chargers.single_phase' },
@@ -70,6 +72,7 @@ export class ChargingStationConnectorComponent implements OnInit, OnChanges {
private router: Router,
private messageService: MessageService) {
this.ocpiActive = this.componentService.isActive(TenantComponents.OCPI);
+ this.scanPayActive = this.componentService.isActive(TenantComponents.SCAN_PAY);
}
public ngOnInit() {
@@ -243,16 +246,14 @@ export class ChargingStationConnectorComponent implements OnInit, OnChanges {
this.refreshTotalAmperage();
}
- public generateQRCode() {
+ public generateQRCode(isScanPayQRCode: boolean) {
this.spinnerService.show();
- this.centralServerService.getConnectorQrCode(this.chargingStation.id, this.connector.connectorId).subscribe({
+ this.centralServerService.getConnectorQrCode(this.chargingStation.id, this.connector.connectorId, isScanPayQRCode).subscribe({
next: (qrCode: Image) => {
this.spinnerService.hide();
if (qrCode) {
// Create the dialog
const dialogConfig = new MatDialogConfig();
- dialogConfig.minWidth = '70vw';
- dialogConfig.minHeight = '70vh';
dialogConfig.disableClose = false;
dialogConfig.panelClass = 'transparent-dialog-container';
// Set data
@@ -260,6 +261,7 @@ export class ChargingStationConnectorComponent implements OnInit, OnChanges {
qrCode: qrCode.image,
connectorID: this.connector.connectorId,
chargingStationID: this.chargingStation.id,
+ isScanPayQRCode
};
// Disable outside click close
dialogConfig.disableClose = true;
diff --git a/src/app/pages/charging-stations/list/charging-stations-list-table-data-source.ts b/src/app/pages/charging-stations/list/charging-stations-list-table-data-source.ts
index 078bc98cb7..cdcf4bb41d 100644
--- a/src/app/pages/charging-stations/list/charging-stations-list-table-data-source.ts
+++ b/src/app/pages/charging-stations/list/charging-stations-list-table-data-source.ts
@@ -3,15 +3,16 @@ import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
-import { WindowService } from 'services/window.service';
import { CentralServerService } from '../../../services/central-server.service';
import { ComponentService } from '../../../services/component.service';
import { DialogService } from '../../../services/dialog.service';
import { MessageService } from '../../../services/message.service';
import { SpinnerService } from '../../../services/spinner.service';
+import { WindowService } from '../../../services/window.service';
import { PricingDefinitionsDialogComponent } from '../../../shared/pricing-definitions/pricing-definitions.dialog.component';
import { TableChargingStationGenerateQrCodeConnectorAction, TableChargingStationGenerateQrCodeConnectorActionDef } from '../../../shared/table/actions/charging-stations/table-charging-station-generate-qr-code-connector-action';
+import { TableChargingStationGenerateQrCodeScanPayConnectorAction, TableChargingStationGenerateQrCodeScanPayConnectorActionDef } from '../../../shared/table/actions/charging-stations/table-charging-station-generate-qr-code-scan-pay-connector-action';
import { TableChargingStationsClearCacheAction, TableChargingStationsClearCacheActionDef } from '../../../shared/table/actions/charging-stations/table-charging-stations-clear-cache-action';
import { TableChargingStationsForceAvailableStatusAction, TableChargingStationsForceAvailableStatusActionDef } from '../../../shared/table/actions/charging-stations/table-charging-stations-force-available-status-action';
import { TableChargingStationsForceUnavailableStatusAction, TableChargingStationsForceUnavailableStatusActionDef } from '../../../shared/table/actions/charging-stations/table-charging-stations-force-unavailable-status-action';
@@ -55,11 +56,13 @@ import { ChargingStationDialogComponent } from '../charging-station/charging-sta
export class ChargingStationsListTableDataSource extends TableDataSource {
private readonly isOrganizationComponentActive: boolean;
private readonly isPricingComponentActive: boolean;
+ private readonly isScanPayComponentActive: boolean;
private editAction = new TableEditChargingStationAction().getActionDef();
private viewAction = new TableViewChargingStationAction().getActionDef();
private smartChargingAction = new TableChargingStationsSmartChargingAction().getActionDef();
private deleteAction = new TableDeleteChargingStationAction().getActionDef();
private generateQrCodeConnectorAction = new TableChargingStationGenerateQrCodeConnectorAction().getActionDef();
+ private generateQrCodeScanPayConnectorAction = new TableChargingStationGenerateQrCodeScanPayConnectorAction().getActionDef();
private canExport = new TableExportChargingStationsAction().getActionDef();
private maintainPricingDefinitionsAction = new TableViewPricingDefinitionsAction().getActionDef();
private navigateToTransactionsAction = new TableNavigateToTransactionsAction().getActionDef();
@@ -85,6 +88,7 @@ export class ChargingStationsListTableDataSource extends TableDataSource {
private readonly isAssetComponentActive: boolean;
+ private readonly isScanPayComponentActive: boolean;
private editAction = new TableEditSiteAreaAction().getActionDef();
private assignChargingStationsToSiteAreaAction = new TableAssignChargingStationsToSiteAreaAction().getActionDef();
private assignAssetsToSiteAreaAction = new TableAssignAssetsToSiteAreaAction().getActionDef();
@@ -55,6 +57,7 @@ export class SiteAreasListTableDataSource extends TableDataSource {
private viewAssetsOfSiteArea = new TableViewAssignedAssetsOfSiteAreaAction().getActionDef();
private exportOCPPParamsAction = new TableExportOCPPParamsAction().getActionDef();
private siteAreaGenerateQrCodeConnectorAction = new TableSiteAreaGenerateQrCodeConnectorAction().getActionDef();
+ private siteAreaGenerateQrCodeScanPayConnectorAction = new TableSiteAreaGenerateQrCodeScanPayConnectorAction().getActionDef();
private createAction = new TableCreateSiteAreaAction().getActionDef();
private siteAreasAuthorizations: SiteAreasAuthorizations;
private smartChargingSessionParametersActive: boolean;
@@ -74,6 +77,7 @@ export class SiteAreasListTableDataSource extends TableDataSource {
super(spinnerService, translateService);
// Init
this.isAssetComponentActive = this.componentService.isActive(TenantComponents.ASSET);
+ this.isScanPayComponentActive = this.componentService.isActive(TenantComponents.SCAN_PAY);
this.setStaticFilters([{ WithSite: true }, { WithParentSiteArea: true },]);
this.initDataSource();
this.initUrlParams();
@@ -268,6 +272,9 @@ export class SiteAreasListTableDataSource extends TableDataSource {
if (siteArea.canGenerateQrCode) {
moreActions.addActionInMoreActions(this.siteAreaGenerateQrCodeConnectorAction);
}
+ if (this.isScanPayComponentActive && siteArea.canGenerateQrCodeScanPay) {
+ moreActions.addActionInMoreActions(this.siteAreaGenerateQrCodeScanPayConnectorAction);
+ }
if (siteArea.canAssignChargingStations || siteArea.canUnassignChargingStations) {
rowActions.push(this.assignChargingStationsToSiteAreaAction);
} else if (siteArea.canReadChargingStations) {
@@ -365,6 +372,14 @@ export class SiteAreasListTableDataSource extends TableDataSource {
);
}
break;
+ case ChargingStationButtonAction.GENERATE_QR_CODE_SCAN_PAY:
+ if (actionDef.action) {
+ (actionDef as TableSiteAreaGenerateQrCodeScanPayConnectorsActionDef).action(
+ siteArea, this.translateService, this.spinnerService,
+ this.messageService, this.centralServerService, this.router
+ );
+ }
+ break;
}
}
diff --git a/src/app/pages/organization/sites/list/sites-list-table-data-source.ts b/src/app/pages/organization/sites/list/sites-list-table-data-source.ts
index db4e111494..19bbba36f1 100644
--- a/src/app/pages/organization/sites/list/sites-list-table-data-source.ts
+++ b/src/app/pages/organization/sites/list/sites-list-table-data-source.ts
@@ -3,38 +3,39 @@ import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
-import { ComponentService } from 'services/component.service';
-import { PricingDefinitionsDialogComponent } from 'shared/pricing-definitions/pricing-definitions.dialog.component';
-import { TableSiteGenerateQrCodeConnectorAction, TableSiteGenerateQrCodeConnectorsActionDef } from 'shared/table/actions/sites/table-site-generate-qr-code-connector-action';
-import { TableViewPricingDefinitionsAction, TableViewPricingDefinitionsActionDef } from 'shared/table/actions/table-view-pricing-definitions-action';
-import { SitesAuthorizations } from 'types/Authorization';
-import { PricingButtonAction, PricingEntity } from 'types/Pricing';
-import { TenantComponents } from 'types/Tenant';
import { CentralServerService } from '../../../../services/central-server.service';
+import { ComponentService } from '../../../../services/component.service';
import { DialogService } from '../../../../services/dialog.service';
import { MessageService } from '../../../../services/message.service';
import { SpinnerService } from '../../../../services/spinner.service';
import { AppDatePipe } from '../../../../shared/formatters/app-date.pipe';
+import { PricingDefinitionsDialogComponent } from '../../../../shared/pricing-definitions/pricing-definitions.dialog.component';
import { TableExportOCPPParamsAction, TableExportOCPPParamsActionDef } from '../../../../shared/table/actions/charging-stations/table-export-ocpp-params-action';
import { TableAssignUsersToSiteAction, TableAssignUsersToSiteActionDef } from '../../../../shared/table/actions/sites/table-assign-users-to-site-action';
import { TableViewAssignedUsersOfSiteAction, TableViewAssignedUsersOfSiteActionDef } from '../../../../shared/table/actions/sites/table-assign-view-users-of-site-action';
import { TableCreateSiteAction, TableCreateSiteActionDef } from '../../../../shared/table/actions/sites/table-create-site-action';
import { TableDeleteSiteAction, TableDeleteSiteActionDef } from '../../../../shared/table/actions/sites/table-delete-site-action';
import { TableEditSiteAction, TableEditSiteActionDef } from '../../../../shared/table/actions/sites/table-edit-site-action';
+import { TableSiteGenerateQrCodeConnectorAction, TableSiteGenerateQrCodeConnectorsActionDef } from '../../../../shared/table/actions/sites/table-site-generate-qr-code-connector-action';
+import { TableSiteGenerateQrCodeScanPayConnectorAction, TableSiteGenerateQrCodeScanPayConnectorsActionDef } from '../../../../shared/table/actions/sites/table-site-generate-qr-code-scan-pay-connector-action';
import { TableViewSiteAction, TableViewSiteActionDef } from '../../../../shared/table/actions/sites/table-view-site-action';
import { TableAutoRefreshAction } from '../../../../shared/table/actions/table-auto-refresh-action';
import { TableMoreAction } from '../../../../shared/table/actions/table-more-action';
import { TableOpenInMapsAction } from '../../../../shared/table/actions/table-open-in-maps-action';
import { TableRefreshAction } from '../../../../shared/table/actions/table-refresh-action';
+import { TableViewPricingDefinitionsAction, TableViewPricingDefinitionsActionDef } from '../../../../shared/table/actions/table-view-pricing-definitions-action';
import { CompanyTableFilter } from '../../../../shared/table/filters/company-table-filter';
import { IssuerFilter } from '../../../../shared/table/filters/issuer-filter';
import { TableDataSource } from '../../../../shared/table/table-data-source';
+import { SitesAuthorizations } from '../../../../types/Authorization';
import { ChargingStationButtonAction } from '../../../../types/ChargingStation';
import { DataResult } from '../../../../types/DataResult';
import { ButtonAction } from '../../../../types/GlobalType';
+import { PricingButtonAction, PricingEntity } from '../../../../types/Pricing';
import { Site, SiteButtonAction } from '../../../../types/Site';
import { TableActionDef, TableColumnDef, TableDef, TableFilterDef } from '../../../../types/Table';
+import { TenantComponents } from '../../../../types/Tenant';
import { User } from '../../../../types/User';
import { Utils } from '../../../../utils/Utils';
import { SiteUsersDialogComponent } from '../site-users/site-users-dialog.component';
@@ -43,6 +44,7 @@ import { SiteDialogComponent } from '../site/site-dialog.component';
@Injectable()
export class SitesListTableDataSource extends TableDataSource {
private readonly isPricingComponentActive: boolean;
+ private readonly isScanPayComponentActive: boolean;
private editAction = new TableEditSiteAction().getActionDef();
private assignUsersToSite = new TableAssignUsersToSiteAction().getActionDef();
private viewUsersOfSite = new TableViewAssignedUsersOfSiteAction().getActionDef();
@@ -50,6 +52,7 @@ export class SitesListTableDataSource extends TableDataSource {
private viewAction = new TableViewSiteAction().getActionDef();
private exportOCPPParamsAction = new TableExportOCPPParamsAction().getActionDef();
private siteGenerateQrCodeConnectorAction = new TableSiteGenerateQrCodeConnectorAction().getActionDef();
+ private siteGenerateQrCodeScanPayConnectorAction = new TableSiteGenerateQrCodeScanPayConnectorAction().getActionDef();
private createAction = new TableCreateSiteAction().getActionDef();
private maintainPricingDefinitionsAction = new TableViewPricingDefinitionsAction().getActionDef();
private companyFilter: TableFilterDef;
@@ -68,6 +71,7 @@ export class SitesListTableDataSource extends TableDataSource {
private componentService: ComponentService) {
super(spinnerService, translateService);
this.isPricingComponentActive = this.componentService.isActive(TenantComponents.PRICING);
+ this.isScanPayComponentActive = this.componentService.isActive(TenantComponents.SCAN_PAY);
this.setStaticFilters([{ WithCompany: true }]);
this.initDataSource();
}
@@ -228,6 +232,9 @@ export class SitesListTableDataSource extends TableDataSource {
if (site.canGenerateQrCode) {
moreActions.addActionInMoreActions(this.siteGenerateQrCodeConnectorAction);
}
+ if (this.isScanPayComponentActive && site.canGenerateQrCodeScanPay) {
+ moreActions.addActionInMoreActions(this.siteGenerateQrCodeScanPayConnectorAction);
+ }
moreActions.addActionInMoreActions(openInMaps);
if (site.canDelete) {
moreActions.addActionInMoreActions(this.deleteAction);
@@ -248,6 +255,7 @@ export class SitesListTableDataSource extends TableDataSource {
}
}
+ // eslint-disable-next-line complexity
public rowActionTriggered(actionDef: TableActionDef, site: Site) {
switch (actionDef.id) {
case SiteButtonAction.EDIT_SITE:
@@ -301,6 +309,14 @@ export class SitesListTableDataSource extends TableDataSource {
);
}
break;
+ case ChargingStationButtonAction.GENERATE_QR_CODE_SCAN_PAY:
+ if (actionDef.action) {
+ (actionDef as TableSiteGenerateQrCodeScanPayConnectorsActionDef).action(
+ site, this.translateService, this.spinnerService,
+ this.messageService, this.centralServerService, this.router
+ );
+ }
+ break;
case PricingButtonAction.VIEW_PRICING_DEFINITIONS:
if (actionDef.action) {
(actionDef as TableViewPricingDefinitionsActionDef).action(PricingDefinitionsDialogComponent, this.dialog, {
diff --git a/src/app/pages/settings-integration/billing/scan-pay/settings-scan-pay.component.html b/src/app/pages/settings-integration/billing/scan-pay/settings-scan-pay.component.html
new file mode 100644
index 0000000000..4d1eb48089
--- /dev/null
+++ b/src/app/pages/settings-integration/billing/scan-pay/settings-scan-pay.component.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+ {{'settings.billing.deactivated_setting_message' | translate}}
+
+
+
+
+
+ {{"settings.scan_pay.pattern_error" | translate}}
+
+
+
+
+
+
diff --git a/src/app/pages/settings-integration/billing/scan-pay/settings-scan-pay.component.ts b/src/app/pages/settings-integration/billing/scan-pay/settings-scan-pay.component.ts
new file mode 100644
index 0000000000..05d4abfab3
--- /dev/null
+++ b/src/app/pages/settings-integration/billing/scan-pay/settings-scan-pay.component.ts
@@ -0,0 +1,139 @@
+import { Component, Input, OnChanges, OnInit } from '@angular/core';
+import { AbstractControl, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+import { StatusCodes } from 'http-status-codes';
+
+import { CentralServerService } from '../../../../services/central-server.service';
+import { ComponentService } from '../../../../services/component.service';
+import { MessageService } from '../../../../services/message.service';
+import { SpinnerService } from '../../../../services/spinner.service';
+import { SettingAuthorizationActions } from '../../../../types/Authorization';
+import { RestResponse } from '../../../../types/GlobalType';
+import { ScanPaySettings, ScanPaySettingsType } from '../../../../types/Setting';
+import { TenantComponents } from '../../../../types/Tenant';
+import { Utils } from '../../../../utils/Utils';
+
+@Component({
+ selector: 'app-settings-scan-pay',
+ templateUrl: 'settings-scan-pay.component.html',
+})
+export class SettingsScanPayComponent implements OnInit, OnChanges {
+ @Input() public formGroup!: UntypedFormGroup;
+ @Input() public authorizations!: SettingAuthorizationActions;
+ @Input() public scanPaySettings!: ScanPaySettings;
+ @Input() public isBillingActive: boolean;
+ @Input() public isPricingActive: boolean;
+
+ public scanPay!: UntypedFormGroup;
+ public amount!: AbstractControl;
+ public isScanPayActive: boolean;
+
+ public constructor(
+ private componentService: ComponentService,
+ private centralServerService: CentralServerService,
+ private spinnerService: SpinnerService,
+ private router: Router,
+ private messageService: MessageService
+ ) {
+ this.isScanPayActive = this.componentService.isActive(TenantComponents.SCAN_PAY);
+ }
+
+ public ngOnInit() {
+ if (this.isScanPayActive && this.isBillingActive && this.isPricingActive) {
+ this.loadScanPayConfiguration();
+ }
+ this.scanPay = new UntypedFormGroup({
+ amount: new UntypedFormControl('',
+ Validators.compose([
+ Validators.pattern(/^[+]?[0-9]+$/),
+ Validators.required
+ ])
+ )
+ });
+ this.formGroup.addControl('scanPay', this.scanPay);
+ this.amount = this.scanPay.controls['amount'];
+ }
+
+ public ngOnChanges() {
+ this.updateFormData();
+ }
+
+ public loadScanPayConfiguration() {
+ this.spinnerService.show();
+ this.componentService.getScanPaySettings().subscribe({
+ next: (settings) => {
+ this.spinnerService.hide();
+ // Keep
+ this.scanPaySettings = settings;
+ // Init form
+ this.formGroup.markAsPristine();
+ // Set data
+ this.updateFormData();
+ },
+ error: (error) => {
+ this.spinnerService.hide();
+ switch (error.status) {
+ case StatusCodes.NOT_FOUND:
+ this.messageService.showErrorMessage('settings.scan_pay.not_found');
+ break;
+ default:
+ Utils.handleHttpError(error, this.router, this.messageService,
+ this.centralServerService, 'general.unexpected_error_backend');
+ }
+ }
+ });
+ }
+
+ public save(newSettings: any) {
+ this.scanPaySettings.type = ScanPaySettingsType.SCAN_PAY;
+ if (newSettings?.scanPay?.amount) {
+ this.scanPaySettings.content.scanPay.amount = newSettings.scanPay.amount;
+ } else {
+ this.messageService.showErrorMessage('settings.scan_pay.not_found');
+ }
+ // Save
+ this.spinnerService.show();
+ // Update scan & pay settings
+ this.componentService.saveScanPaySettings(this.scanPaySettings).subscribe({
+ next: (response) => {
+ this.spinnerService.hide();
+ if (response.status === RestResponse.SUCCESS) {
+ this.messageService.showSuccessMessage(
+ (!this.scanPaySettings.id ? 'settings.scan_pay.create_success' : 'settings.scan_pay.update_success'));
+ this.refresh();
+ } else {
+ Utils.handleError(JSON.stringify(response),
+ this.messageService, (!this.scanPaySettings.id ? 'settings.scan_pay.create_error' : 'settings.scan_pay.update_error'));
+ }
+ },
+ error: (error) => {
+ this.spinnerService.hide();
+ switch (error.status) {
+ case StatusCodes.NOT_FOUND:
+ this.messageService.showErrorMessage('settings.scan_pay.not_found');
+ break;
+ default:
+ Utils.handleHttpError(error, this.router, this.messageService, this.centralServerService,
+ (!this.scanPaySettings.id ? 'settings.scan_pay.create_error' : 'settings.scan_pay.update_error'));
+ }
+ }
+ });
+ }
+
+ public refresh() {
+ this.loadScanPayConfiguration();
+ }
+
+ private updateFormData() {
+ if (!Utils.isEmptyObject(this.scanPaySettings?.content?.scanPay) && !Utils.isEmptyObject(this.formGroup.value)) {
+ const scanPaySetting = this.scanPaySettings.content.scanPay;
+ this.amount.setValue(scanPaySetting.amount || '');
+ this.amount.markAsTouched();
+ }
+ // Read only
+ if(!this.authorizations?.canSetScanPayAmount) {
+ // Async call for letting the sub form groups to init
+ setTimeout(() => this.formGroup.disable(), 0);
+ }
+ }
+}
diff --git a/src/app/pages/settings-integration/billing/settings-billing.component.html b/src/app/pages/settings-integration/billing/settings-billing.component.html
index 656f10499a..c2d53430d0 100644
--- a/src/app/pages/settings-integration/billing/settings-billing.component.html
+++ b/src/app/pages/settings-integration/billing/settings-billing.component.html
@@ -42,6 +42,15 @@ {{'settings.information' | translate}}
[authorizations]="authorizations"
[formGroup]="formGroup">
+
+
+
+
diff --git a/src/app/pages/settings-integration/billing/settings-billing.component.ts b/src/app/pages/settings-integration/billing/settings-billing.component.ts
index a9f81eef1e..12952bf0ed 100644
--- a/src/app/pages/settings-integration/billing/settings-billing.component.ts
+++ b/src/app/pages/settings-integration/billing/settings-billing.component.ts
@@ -1,26 +1,31 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, ViewChild } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { StatusCodes } from 'http-status-codes';
-import { SettingAuthorizationActions } from 'types/Authorization';
import { CentralServerService } from '../../../services/central-server.service';
import { ComponentService } from '../../../services/component.service';
import { DialogService } from '../../../services/dialog.service';
import { MessageService } from '../../../services/message.service';
import { SpinnerService } from '../../../services/spinner.service';
+import { SettingAuthorizationActions } from '../../../types/Authorization';
import { ButtonAction, RestResponse } from '../../../types/GlobalType';
-import { BillingSetting, BillingSettings, BillingSettingsType, StripeBillingSetting } from '../../../types/Setting';
+import { BillingSetting, BillingSettings, BillingSettingsType, ScanPaySettings, StripeBillingSetting } from '../../../types/Setting';
import { TenantComponents } from '../../../types/Tenant';
import { Utils } from '../../../utils/Utils';
+import { SettingsScanPayComponent } from './scan-pay/settings-scan-pay.component';
@Component({
selector: 'app-settings-billing',
templateUrl: 'settings-billing.component.html',
})
export class SettingsBillingComponent implements OnInit {
+ @ViewChild('settingsScanPayComponent') public settingsScanPayComponent!: SettingsScanPayComponent;
+
public isBillingActive = false;
+ public isPricingActive = false;
+ public isScanPayActive = false;
public authorizations: SettingAuthorizationActions;
public isBillingPlatformActive = false;
public isBillingTransactionEnabled = false;
@@ -28,6 +33,7 @@ export class SettingsBillingComponent implements OnInit {
public formGroup!: FormGroup;
public billingSettings!: BillingSettings;
+ public scanPaySettings!: ScanPaySettings;
public transactionBillingActivated: boolean; // ##CR - reverting some changes
public isClearTestDataVisible = false;
@@ -42,6 +48,8 @@ export class SettingsBillingComponent implements OnInit {
) {
this.isBillingActive = this.componentService.isActive(TenantComponents.BILLING);
this.isBillingPlatformActive = this.componentService.isActive(TenantComponents.BILLING_PLATFORM);
+ this.isPricingActive = this.componentService.isActive(TenantComponents.PRICING);
+ this.isScanPayActive = this.componentService.isActive(TenantComponents.SCAN_PAY);
}
public ngOnInit(): void {
@@ -49,11 +57,11 @@ export class SettingsBillingComponent implements OnInit {
this.formGroup = new FormGroup({});
// Load the conf
if (this.isBillingActive) {
- this.loadConfiguration();
+ this.loadBillingConfiguration();
}
}
- public loadConfiguration() {
+ public loadBillingConfiguration() {
this.spinnerService.show();
this.componentService.getBillingSettings().subscribe({
next: (settings) => {
@@ -63,6 +71,7 @@ export class SettingsBillingComponent implements OnInit {
canUpdate: Utils.convertToBoolean(settings.canUpdate),
canCheckBillingConnection: Utils.convertToBoolean(settings.canCheckBillingConnection),
canActivateBilling: Utils.convertToBoolean(settings.canUpdate), // Using update auth
+ canSetScanPayAmount: Utils.convertToBoolean(settings.canUpdate), // Using update auth
};
// Keep
this.billingSettings = settings;
@@ -123,10 +132,12 @@ export class SettingsBillingComponent implements OnInit {
}
}
});
+ this.settingsScanPayComponent.save(newSettings);
}
public refresh() {
- this.loadConfiguration();
+ this.loadBillingConfiguration();
+ this.settingsScanPayComponent.loadScanPayConfiguration();
}
public checkConnection(activateTransactionBilling = false) {
diff --git a/src/app/pages/settings-integration/settings-integration.module.ts b/src/app/pages/settings-integration/settings-integration.module.ts
index 12f3fd1fb4..41d6d3d09c 100644
--- a/src/app/pages/settings-integration/settings-integration.module.ts
+++ b/src/app/pages/settings-integration/settings-integration.module.ts
@@ -27,6 +27,7 @@ import { SchneiderAssetConnectionComponent } from './asset/connection/schneider/
import { WitAssetConnectionComponent } from './asset/connection/wit/wit-asset-connection.component';
import { SettingsAssetConnectionEditableTableDataSource } from './asset/settings-asset-connections-list-table-data-source';
import { SettingsAssetComponent } from './asset/settings-asset.component';
+import { SettingsScanPayComponent } from './billing/scan-pay/settings-scan-pay.component';
import { SettingsBillingComponent } from './billing/settings-billing.component';
import { SettingsStripeComponent } from './billing/stripe/settings-stripe.component';
import { CarConnectorConnectionComponent } from './car-connector/connection/car-connector-connection.component';
@@ -95,6 +96,7 @@ import { SettingsSmartChargingComponent } from './smart-charging/settings-smart-
SettingsSimplePricingComponent,
SettingsBillingComponent,
SettingsStripeComponent,
+ SettingsScanPayComponent,
SettingsAnalyticsComponent,
SettingsSacComponent,
SettingsSmartChargingComponent,
diff --git a/src/app/pages/tenants/tenant/components/tenant-components.component.html b/src/app/pages/tenants/tenant/components/tenant-components.component.html
index 2b57283437..559e4a59da 100644
--- a/src/app/pages/tenants/tenant/components/tenant-components.component.html
+++ b/src/app/pages/tenants/tenant/components/tenant-components.component.html
@@ -102,6 +102,14 @@
+
+
+
+ {{"scanPay.title" | translate}}
+ {{"scanPay.description" | translate}}
+
+
+
diff --git a/src/app/pages/tenants/tenant/tenant.component.ts b/src/app/pages/tenants/tenant/tenant.component.ts
index 459e025943..f326ae5cab 100644
--- a/src/app/pages/tenants/tenant/tenant.component.ts
+++ b/src/app/pages/tenants/tenant/tenant.component.ts
@@ -114,6 +114,7 @@ export class TenantComponent extends AbstractTabComponent implements OnInit {
let carConnectorActive = false;
let ocpiActive = false;
let oicpActive = false;
+ let scanPayActive = false;
for (const component in tenant.components) {
if (Utils.objectHasProperty(tenant.components, component)) {
if (!tenant.components[component].active) {
@@ -152,6 +153,9 @@ export class TenantComponent extends AbstractTabComponent implements OnInit {
if (component === TenantComponents.OICP) {
oicpActive = tenant.components[component].active;
}
+ if (component === TenantComponents.SCAN_PAY) {
+ scanPayActive = tenant.components[component].active;
+ }
}
}
if (oicpActive && ocpiActive) {
@@ -182,6 +186,10 @@ export class TenantComponent extends AbstractTabComponent implements OnInit {
this.messageService.showErrorMessage('tenants.save_error_car_connector');
return;
}
+ if (scanPayActive && (!billingActive || !pricingActive)) {
+ this.messageService.showErrorMessage('tenants.save_error_scan_pay');
+ return;
+ }
if (this.tenant) {
this.updateTenant(tenant);
} else {
diff --git a/src/app/pages/users/user/payment-methods/payment-method/stripe/stripe-payment-method.component.ts b/src/app/pages/users/user/payment-methods/payment-method/stripe/stripe-payment-method.component.ts
index 7df2e179a8..9bd27d73ca 100644
--- a/src/app/pages/users/user/payment-methods/payment-method/stripe/stripe-payment-method.component.ts
+++ b/src/app/pages/users/user/payment-methods/payment-method/stripe/stripe-payment-method.component.ts
@@ -145,7 +145,7 @@ export class StripePaymentMethodComponent implements OnInit {
this.isSaveClicked = false;
} else {
// Operation succeeded
- this.messageService.showSuccessMessage('settings.billing.payment_methods_create_success', { last4: operationResult.internalData.card.last4 });
+ this.messageService.showSuccessMessage('settings.billing.payment_methods_create_success');
this.close(true);
}
}
@@ -192,7 +192,6 @@ export class StripePaymentMethodComponent implements OnInit {
try {
this.spinnerService.show();
const response: BillingOperationResult = await this.centralServerService.setupPaymentMethod({
- setupIntentId: operationResult.setupIntent?.id,
paymentMethodId: operationResult.setupIntent?.payment_method,
userID: this.userID
}).toPromise();
diff --git a/src/app/services/central-server.service.ts b/src/app/services/central-server.service.ts
index 1f31715cad..397814e2ce 100644
--- a/src/app/services/central-server.service.ts
+++ b/src/app/services/central-server.service.ts
@@ -25,14 +25,14 @@ import { OicpEndpoint } from '../types/oicp/OICPEndpoint';
import PricingDefinition from '../types/Pricing';
import { RefundReport } from '../types/Refund';
import { RegistrationToken } from '../types/RegistrationToken';
-import { RESTServerRoute, ServerAction } from '../types/Server';
+import { RESTServerRoute } from '../types/Server';
import { BillingSettings, SettingDB } from '../types/Setting';
import { Site } from '../types/Site';
import { SiteArea, SiteAreaConsumption, SubSiteAreaAction } from '../types/SiteArea';
import { Tag } from '../types/Tag';
import { Tenant } from '../types/Tenant';
import { OcpiData, Transaction } from '../types/Transaction';
-import { User, UserSessionContext, UserToken } from '../types/User';
+import { User, UserRole, UserSessionContext, UserToken } from '../types/User';
import { Constants } from '../utils/Constants';
import { Utils } from '../utils/Utils';
import { ConfigService } from './config.service';
@@ -770,14 +770,15 @@ export class CentralServerService {
);
}
- public getConnectorQrCode(chargingStationID: string, connectorID: number): Observable {
+ public getConnectorQrCode(chargingStationID: string, connectorID: number, isScanPayQRCode: boolean): Observable {
// Verify init
this.checkInit();
if (!chargingStationID || connectorID < 0) {
return EMPTY;
}
+ const urlPattern: RESTServerRoute = isScanPayQRCode ? RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_GENERATE_SCAN_PAY : RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_GENERATE;
// Execute the REST service
- return this.httpClient.get(this.buildRestEndpointUrl(RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_GENERATE, { id: chargingStationID, connectorId: connectorID}),
+ return this.httpClient.get(this.buildRestEndpointUrl(urlPattern, { id: chargingStationID, connectorId: connectorID }),
{
headers: this.buildHttpHeaders(),
})
@@ -881,7 +882,7 @@ export class CentralServerService {
public getUserSessionContext(userID: string, chargingStationID: string, connectorID: number): Observable {
// Verify init
this.checkInit();
- return this.httpClient.get(this.buildRestEndpointUrl(RESTServerRoute.REST_USER_SESSION_CONTEXT, { id: userID } ),
+ return this.httpClient.get(this.buildRestEndpointUrl(RESTServerRoute.REST_USER_SESSION_CONTEXT, { id: userID }),
{
headers: this.buildHttpHeaders(),
params: {
@@ -1601,6 +1602,41 @@ export class CentralServerService {
);
}
+ public scanPayHandlePaymentIntent(parameters: any): Observable {
+ this.checkInit();
+ // Build the URL
+ const url = this.buildRestEndpointUrl(RESTServerRoute.REST_SCAN_PAY_PAYMENT_INTENT_SETUP);
+ // Execute the REST service
+ return this.httpClient.post(url, {
+ email: parameters.email,
+ locale: parameters.locale,
+ chargingStationID: parameters.chargingStationID,
+ connectorID: parameters.connectorID,
+ verificationToken: parameters.verificationToken,
+ }, {
+ headers: this.buildHttpHeaders(),
+ }).pipe(
+ catchError(this.handleHttpError),
+ );
+ }
+
+ public scanPayHandlePaymentIntentRetrieve(parameters: any): Observable {
+ this.checkInit();
+ // Execute the REST service
+ return this.httpClient.post(this.buildRestEndpointUrl(RESTServerRoute.REST_SCAN_PAY_PAYMENT_INTENT_RETRIEVE), {
+ email: parameters.email,
+ locale: parameters.locale,
+ paymentIntentID: parameters.paymentIntentID,
+ chargingStationID: parameters.chargingStationID,
+ connectorID: parameters.connectorID,
+ verificationToken: parameters.verificationToken,
+ }, {
+ headers: this.buildHttpHeaders(),
+ }).pipe(
+ catchError(this.handleHttpError),
+ );
+ }
+
public updateBillingSettings(billingSettings: BillingSettings): Observable {
// Verify init
this.checkInit();
@@ -1722,7 +1758,7 @@ export class CentralServerService {
// Verify init
this.checkInit();
// Execute
- return this.httpClient.post(this.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_ACCOUNTS), account,{
+ return this.httpClient.post(this.buildRestEndpointUrl(RESTServerRoute.REST_BILLING_ACCOUNTS), account, {
headers: this.buildHttpHeaders(),
}).pipe(
catchError(this.handleHttpError),
@@ -1836,11 +1872,12 @@ export class CentralServerService {
);
}
- public downloadSiteQrCodes(siteID: string): Observable {
+ public downloadSiteQrCodes(siteID: string, isScanPayQRCode: boolean): Observable {
this.checkInit();
const params: { [param: string]: string } = {};
params['SiteID'] = siteID;
- return this.httpClient.get(this.buildRestEndpointUrl(RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_DOWNLOAD),
+ const urlPattern: RESTServerRoute = isScanPayQRCode ? RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_SCAN_PAY_DOWNLOAD : RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_DOWNLOAD;
+ return this.httpClient.get(this.buildRestEndpointUrl(urlPattern),
{
headers: this.buildHttpHeaders(),
params,
@@ -1851,11 +1888,12 @@ export class CentralServerService {
);
}
- public downloadSiteAreaQrCodes(siteAreaID?: string): Observable {
+ public downloadSiteAreaQrCodes(siteAreaID: string, isScanPayQRCode: boolean): Observable {
this.checkInit();
const params: { [param: string]: string } = {};
params['SiteAreaID'] = siteAreaID;
- return this.httpClient.get(this.buildRestEndpointUrl(RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_DOWNLOAD),
+ const urlPattern: RESTServerRoute = isScanPayQRCode ? RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_SCAN_PAY_DOWNLOAD : RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_DOWNLOAD;
+ return this.httpClient.get(this.buildRestEndpointUrl(urlPattern),
{
headers: this.buildHttpHeaders(),
params,
@@ -1866,14 +1904,16 @@ export class CentralServerService {
);
}
- public downloadChargingStationQrCodes(chargingStationID: string, connectorID?: number): Observable {
+ public downloadChargingStationQrCodes(chargingStationID: string, isScanPayQRCode: boolean, connectorID?: number): Observable {
this.checkInit();
const params: { [param: string]: string } = {};
params['ChargingStationID'] = chargingStationID;
if (connectorID) {
params['ConnectorID'] = connectorID.toString();
}
- return this.httpClient.get(this.buildRestEndpointUrl(RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_DOWNLOAD),
+ const urlPattern: RESTServerRoute = isScanPayQRCode ? RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_SCAN_PAY_DOWNLOAD : RESTServerRoute.REST_CHARGING_STATIONS_QRCODE_DOWNLOAD;
+ // Execute the REST service
+ return this.httpClient.get(this.buildRestEndpointUrl(urlPattern),
{
headers: this.buildHttpHeaders(),
params,
@@ -2051,7 +2091,7 @@ export class CentralServerService {
}
public isAuthenticated(): boolean {
- return this.getLoggedUserToken() && !new JwtHelperService().isTokenExpired(this.getLoggedUserToken());
+ return this.getLoggedUserToken() && !new JwtHelperService().isTokenExpired(this.getLoggedUserToken()) && this.getLoggedUser().role !== UserRole.EXTERNAL;
}
public getCurrentUserSubject(): BehaviorSubject {
@@ -2527,7 +2567,7 @@ export class CentralServerService {
this.checkInit();
// Execute
return this.httpClient.put(
- this.buildRestEndpointUrl(RESTServerRoute.REST_OICP_ENDPOINT_SEND_EVSES, {id: oicpEndpoint.id }), {},
+ this.buildRestEndpointUrl(RESTServerRoute.REST_OICP_ENDPOINT_SEND_EVSES, { id: oicpEndpoint.id }), {},
{
headers: this.buildHttpHeaders(),
})
@@ -3150,8 +3190,7 @@ export class CentralServerService {
);
}
- public getChargingStationCompositeSchedule(id: string, connectorId: number, duration: number, unit: string):
- Observable {
+ public getChargingStationCompositeSchedule(id: string, connectorId: number, duration: number, unit: string): Observable {
// Verify init
this.checkInit();
// build request
@@ -3379,7 +3418,7 @@ export class CentralServerService {
public getPricingDefinitions(params: FilterParams,
paging: Paging = Constants.DEFAULT_PAGING, ordering: Ordering[] = [],
- context?: { entityID: string; entityType: string}): Observable {
+ context?: { entityID: string; entityType: string }): Observable {
// Verify init
this.checkInit();
// Build Paging
@@ -3516,6 +3555,22 @@ export class CentralServerService {
);
}
+ public scanPayVerifyEmail(data: any): Observable {
+ // Verify init
+ this.checkInit();
+ // Set the tenant
+ data['tenant'] = this.windowService.getSubdomain();
+ // Execute
+ return this.httpClient.post(`${this.restServerAuthURL}/${RESTServerRoute.REST_SCAN_PAY_VERIFY_EMAIL}`,
+ data,
+ {
+ headers: this.buildHttpHeaders(),
+ })
+ .pipe(
+ catchError(this.handleHttpError),
+ );
+ }
+
public buildImportTagsUsersHttpHeaders(
autoActivateUserAtImport?: boolean, autoActivateTagAtImport?: boolean): { name: string; value: string }[] {
// Build File Header
@@ -3631,7 +3686,7 @@ export class CentralServerService {
// We might use a remote logging infrastructure
const errorInfo = { status: 0, message: '', details: null };
// Handle redirection of Tenant
- if ( error.status === StatusCodes.MOVED_TEMPORARILY && error.error.size > 0) {
+ if (error.status === StatusCodes.MOVED_TEMPORARILY && error.error.size > 0) {
return new Observable(observer => {
const reader = new FileReader();
reader.readAsText(error.error); // convert blob to Text
@@ -3647,7 +3702,7 @@ export class CentralServerService {
errorInfo.status = StatusCodes.REQUEST_TIMEOUT;
errorInfo.message = error.message;
errorInfo.details = null;
- } else {
+ } else {
errorInfo.status = error.status;
errorInfo.message = error.message ?? error.toString();
errorInfo.details = error.error ?? null;
diff --git a/src/app/services/component.service.ts b/src/app/services/component.service.ts
index bd22f309ab..696c076a99 100644
--- a/src/app/services/component.service.ts
+++ b/src/app/services/component.service.ts
@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
-import { Constants } from 'utils/Constants';
import { ActionResponse, BillingAccountDataResult, Ordering, Paging } from '../types/DataResult';
-import { AnalyticsSettings, AssetConnectionType, AssetSettings, AssetSettingsType, BillingSettings, BillingSettingsType, CarConnectorConnectionType, CarConnectorSettings, CarConnectorSettingsType, CryptoSettings, PricingSettings, PricingSettingsType, RefundSettings, RefundSettingsType, RoamingSettings, SettingDB, SmartChargingSettings, SmartChargingSettingsType, TechnicalSettings, UserSettings, UserSettingsType } from '../types/Setting';
+import { AnalyticsSettings, AssetConnectionType, AssetSettings, AssetSettingsType, BillingSettings, BillingSettingsType, CarConnectorConnectionType, CarConnectorSettings, CarConnectorSettingsType, CryptoSettings, PricingSettings, PricingSettingsType, RefundSettings, RefundSettingsType, RoamingSettings, ScanPaySettings, ScanPaySettingsType, SmartChargingSettings, SmartChargingSettingsType, TechnicalSettings, UserSettings, UserSettingsType } from '../types/Setting';
import { TenantComponents } from '../types/Tenant';
+import { Constants } from '../utils/Constants';
import { Utils } from '../utils/Utils';
import { CentralServerService } from './central-server.service';
@@ -83,6 +83,15 @@ export class ComponentService {
return this.centralServerService.updateSetting(settingsToSave);
}
+ public saveScanPaySettings(settings: ScanPaySettings): Observable {
+ // Check the type
+ if (!settings.type) {
+ settings.type = ScanPaySettingsType.SCAN_PAY;
+ }
+ // Save
+ return this.centralServerService.updateSetting(settings);
+ }
+
public saveBillingSettings(settings: BillingSettings): Observable {
// Check the type
if (!settings.type) {
@@ -271,6 +280,21 @@ export class ComponentService {
});
}
+ public getScanPaySettings(): Observable {
+ return new Observable((observer) => {
+ // Get the Scan & Pay settings
+ this.centralServerService.getSetting(TenantComponents.SCAN_PAY).subscribe({
+ next: (settings) => {
+ observer.next(settings as ScanPaySettings);
+ observer.complete();
+ },
+ error: (error) => {
+ observer.error(error);
+ }
+ });
+ });
+ }
+
public getBillingAccounts(paging: Paging = Constants.DEFAULT_PAGING,
ordering: Ordering[] = []): Observable {
return new Observable((observer) => {
diff --git a/src/app/shared/dialogs/dialogs.module.ts b/src/app/shared/dialogs/dialogs.module.ts
index 7ddd11b646..e50c838a20 100644
--- a/src/app/shared/dialogs/dialogs.module.ts
+++ b/src/app/shared/dialogs/dialogs.module.ts
@@ -90,6 +90,7 @@ import { UsersDialogComponent } from './users/users-dialog.component';
exports: [
CarMakersDialogComponent,
TransactionComponent,
+ TransactionHeaderComponent,
TransactionDialogComponent,
SitesDialogComponent,
UsersDialogComponent,
diff --git a/src/app/shared/dialogs/qr-code/qr-code-dialog.component.html b/src/app/shared/dialogs/qr-code/qr-code-dialog.component.html
index 3132570611..81dd326fa3 100644
--- a/src/app/shared/dialogs/qr-code/qr-code-dialog.component.html
+++ b/src/app/shared/dialogs/qr-code/qr-code-dialog.component.html
@@ -1,33 +1,30 @@
-
-
-
-
-
-
-
-
- {{chargingStationID}} / {{'chargers.connector' | translate}} {{connectorID | appConnectorId}}
-
-
-
-
-