Merge branch 'dbexport' for WooCommerce integration

This commit is contained in:
Rene Vergara 2023-01-09 10:24:11 -06:00
commit e171406769
Signed by: pitmutt
GPG key ID: 65122AD495A7F5B2
10 changed files with 404 additions and 108 deletions

View file

@ -3,7 +3,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [1.4.0]
### Added
- Support for WooComerce:
- New tab in Settings to generate authentication token.
- Display of WooCommerce credentials for configuration.
- New service to interact with WooCommerce-related API endpoints.
- A "Return To Shop" button added to ZGo Invoice component
## [1.3.2] - 2022-10-11 ## [1.3.2] - 2022-10-11

View file

@ -39,8 +39,8 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "500kb", "maximumWarning": "5mb",
"maximumError": "1mb" "maximumError": "10mb"
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",

View file

@ -1,6 +1,6 @@
{ {
"name": "zgo", "name": "zgo",
"version": "1.3.2", "version": "1.4.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",

View file

@ -120,9 +120,31 @@
<button style="margin-top: 20px; <button style="margin-top: 20px;
font-weight: 700; font-weight: 700;
background-color: lightgray;" background-color: lightgray;"
mat-raised-button mat-raised-button
(click)="copyMemo()">Copy Memo</button> (click)="copyMemo()" *ngIf="!isWCOrder">Copy Memo</button>
<div style="display: flex;
justify-content: space-between;"
*ngIf="isWCOrder">
<button style="margin-top: 20px;
font-weight: 700;
background-color: lightgray;"
mat-raised-button
(click)="copyMemo()">Copy Memo</button>
<button style="margin-top: 20px;
font-weight: 700;
background-color: lightcyan;"
mat-raised-button
(click)="backToShop()" >
<fa-icon style="color: #FB4F14;
margin-bottom: -2px;
margin-right: 5px;
font-size: 20px;
cursor: pointer;"
[icon]="faArrowUpRightFromSquare"> </fa-icon>
Return to Shop</button>
</div>
</div> </div>

View file

@ -4,7 +4,7 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ReceiptService } from '../receipt.service'; import { ReceiptService } from '../receipt.service';
import { Order} from '../order/order.model'; import { Order} from '../order/order.model';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { faCheck, faHourglass } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faHourglass, faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons';
import { NotifierService } from '../notifier.service'; import { NotifierService } from '../notifier.service';
@ -17,21 +17,22 @@ var Buffer = require('buffer/').Buffer;
templateUrl: './invoice.component.html', templateUrl: './invoice.component.html',
styleUrls: ['./invoice.component.css'] styleUrls: ['./invoice.component.css']
}) })
export class InvoiceComponent implements OnInit { export class InvoiceComponent implements OnInit {
faCheck = faCheck; faCheck = faCheck;
faHourglass = faHourglass; faHourglass = faHourglass;
faArrowUpRightFromSquare = faArrowUpRightFromSquare;
orderId; orderId;
public orderUpdate: Observable<Order>; public orderUpdate: Observable<Order>;
public nameUpdate: Observable<string>; public nameUpdate: Observable<string>;
name: string = ''; name: string = '';
error: boolean = false; error: boolean = false;
codeString: string = 'Test'; codeString: string = 'Test';
public isWCOrder : boolean = false;
zcashUrl: SafeUrl = ''; zcashUrl: SafeUrl = '';
externalURL: string = '';
order:Order = { order:Order = {
_id: '', _id: '',
address: '', address: '',
session: '', session: '',
timestamp: '', timestamp: '',
@ -55,11 +56,12 @@ export class InvoiceComponent implements OnInit {
constructor( constructor(
private _ActiveRoute:ActivatedRoute, private _ActiveRoute:ActivatedRoute,
private router: Router, private router: Router,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
public receiptService: ReceiptService, public receiptService: ReceiptService,
private notifierService : NotifierService private notifierService : NotifierService
) { ) {
this.orderId = this._ActiveRoute.snapshot.paramMap.get("orderId"); this.orderId = this._ActiveRoute.snapshot.paramMap.get("orderId");
console.log('constructor - orderId -> ' + this.orderId);
this.orderUpdate = receiptService.orderUpdate; this.orderUpdate = receiptService.orderUpdate;
this.nameUpdate = receiptService.nameUpdate; this.nameUpdate = receiptService.nameUpdate;
receiptService.getOrderById(this.orderId!).subscribe(response => { receiptService.getOrderById(this.orderId!).subscribe(response => {
@ -83,9 +85,11 @@ export class InvoiceComponent implements OnInit {
}); });
this.orderUpdate.subscribe(order => { this.orderUpdate.subscribe(order => {
this.order = order; this.order = order;
this.codeString = `zcash:${this.order.address}?amount=${this.order.totalZec.toFixed(8)}&memo=${URLSafeBase64.encode(Buffer.from('ZGo Order::'.concat(this.orderId!)))}`; if ( order.session.substring(0,1) == 'W') {
this.zcashUrl = this.sanitizer.bypassSecurityTrustUrl(this.codeString); this.isWCOrder = true;
}
this.codeString = `zcash:${this.order.address}?amount=${this.order.totalZec.toFixed(8)}&memo=${URLSafeBase64.encode(Buffer.from('ZGo Order::'.concat(this.orderId!)))}`;
this.zcashUrl = this.sanitizer.bypassSecurityTrustUrl(this.codeString);
}); });
this.nameUpdate.subscribe(name => { this.nameUpdate.subscribe(name => {
this.name = name; this.name = name;
@ -93,6 +97,25 @@ export class InvoiceComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
}
backToShop() {
if ( this.isWCOrder ) {
// console.log('External Invoice -> ' + this.order.externalInvoice );
const b64URL:string = this.order.externalInvoice.substring(0,this.order.externalInvoice.indexOf("-"));
// console.log('encodedURL -> ' + b64URL );
const shopURL: string = Buffer.from(b64URL, 'base64').toString();
const tmp_orderid = this.order.externalInvoice.substring(this.order.externalInvoice.indexOf('-')+1);
const wc_order_key = tmp_orderid.substring(tmp_orderid.indexOf('-')+1);
const wc_orderid = tmp_orderid.substring(0,tmp_orderid.indexOf('-'));
// console.log('wc_order_id -> ' + wc_orderid);
// console.log('wc_order_key -> ' + wc_order_key);
// console.log('new URL -> ' + shopURL + '/checkout/order-received/' + wc_orderid + '/?key=' + wc_order_key);
if ( shopURL ) {
// console.log('Opening URL....' + shopURL);
window.open( shopURL + '/checkout/order-received/' + wc_orderid + '/?key=' + wc_order_key,"_blank");
}
}
} }
getIconStyle(order : Order) { getIconStyle(order : Order) {
@ -102,7 +125,7 @@ export class InvoiceComponent implements OnInit {
} }
copyAddress() { copyAddress() {
if (!navigator.clipboard) { if (!navigator.clipboard) {
// alert("Copy functionality not supported"); // alert("Copy functionality not supported");
this.notifierService this.notifierService
@ -115,8 +138,9 @@ export class InvoiceComponent implements OnInit {
.showNotification("Error copying address","Close","error"); .showNotification("Error copying address","Close","error");
// console.error("Error", err); // console.error("Error", err);
} }
} }
copyAmount() {
copyAmount() {
if (!navigator.clipboard) { if (!navigator.clipboard) {
// alert("Copy functionality not supported"); // alert("Copy functionality not supported");
this.notifierService this.notifierService
@ -129,9 +153,9 @@ export class InvoiceComponent implements OnInit {
.showNotification("Error while copying ammount","Close","error"); .showNotification("Error while copying ammount","Close","error");
// console.error("Error", err); // console.error("Error", err);
} }
} }
copyMemo() { copyMemo() {
if (!navigator.clipboard) { if (!navigator.clipboard) {
// alert("Copy functionality not supported"); // alert("Copy functionality not supported");
this.notifierService this.notifierService
@ -144,6 +168,6 @@ export class InvoiceComponent implements OnInit {
.showNotification("Error while copying Memo","Close","error"); .showNotification("Error while copying Memo","Close","error");
// console.error("Error", err); // console.error("Error", err);
} }
} }
} }

View file

@ -45,4 +45,15 @@
color: dodgerblue; color: dodgerblue;
} }
.small {
font-size: 12px;
background: #dddddd;
}
.heading {
padding-top: 10px;
}
.toolbar {
padding: 12px;
}

View file

@ -77,98 +77,187 @@
<mat-tab *ngIf="proVersion" <mat-tab *ngIf="proVersion"
label="Integrations" label="Integrations"
style="align-items: center;"> style="align-items: center;">
<div class="container" style="margin-bottom: 20px;"> <mat-tab-group mat-tab-align-tabs="start">
<mat-dialog-content [formGroup]="accCodForm"> <mat-tab label="Xero">
<div style="height: 10px; <div class="container" style="margin-bottom: 20px;">
margin-top: 10px;"> <mat-dialog-content [formGroup]="accCodForm">
<div style="height: 10px;
margin-top: 10px;">
</div>
<div class="container"
style="height: 300;">
<p style="text-align:center">
<a mat-raised-button
color="primary"
href="{{this.xeroLink}}">
{{ linkMsg }}
</a>
</p>
<table *ngIf="linked2Xero"
[style.width.%]="100"
style="margin-top: 10px;">
<thead style="width: 100%;">
<tr>
<th class="urlLabel"
style="text-align: left;"
width="94%">Payment Service URL:
</th>
<th></th>
</tr>
</thead>
<tbody>
<td class="urlDetail"
style="text-align: left;"
width="94%">
<div>
<textarea disabled
style="font-size: 10px !important;
border: none;
outline: none;
min-height: 150px;
width: 95%;"
cdkTextareaAutosize
cdkAutosizeMinRows="6"
cdkAutosizeMaxRows="10">{{ pmtServiceURL }}
</textarea>
</div>
</td>
<td class="urlCopyBtn">
<a (click)='copyUrl()' >
<fa-icon [icon]="faCopy"
class="copy-button">
</fa-icon>
</a>
</td>
</tbody>
</table>
<div style="height: 10px;
margin-top: 10px;">
</div>
<mat-form-field *ngIf="linked2Xero"
class="settings-field"
[style.width.%]="100">
<mat-label>Account Code</mat-label>
<input matInput
width="100%"
placeholder="9999999999"
formControlName="xAcc"
(keyup)="checkStatus($event)">
</mat-form-field>
</div>
</mat-dialog-content>
</div> </div>
<div class="container" <div class="container"
style="height: 300;"> style="display: flex;
<p style="text-align:center"> justify-content: space-between;
<a mat-raised-button align-items: center;">
color="primary" <button mat-raised-button
href="{{this.xeroLink}}"> (click)="closeIntegration()">
{{ linkMsg }} Close
</a> </button>
</p> <button *ngIf="saveAccOk"
mat-raised-button
<table *ngIf="linked2Xero" color="primary"
[style.width.%]="100" (click)="saveAccCod()">
style="margin-top: 10px;"> Save Code
<thead style="width: 100%;"> </button>
<tr> </div>
<th class="urlLabel" <div style="height: 20px;
style="text-align: left;"
width="94%">Payment Service URL:
</th>
<th></th>
</tr>
</thead>
<tbody>
<td class="urlDetail"
style="text-align: left;"
width="94%">
<div>
<textarea disabled
style="font-size: 10px !important;
border: none;
outline: none;
min-height: 150px;
width: 95%;"
cdkTextareaAutosize
cdkAutosizeMinRows="6"
cdkAutosizeMaxRows="10">{{ pmtServiceURL }}
</textarea>
</div>
</td>
<td class="urlCopyBtn">
<a (click)='copyUrl()' >
<fa-icon [icon]="faCopy"
class="copy-button">
</fa-icon>
</a>
</td>
</tbody>
</table>
<div style="height: 10px;
margin-top: 10px;"> margin-top: 10px;">
</div> </div>
<mat-form-field *ngIf="linked2Xero" </mat-tab>
class="settings-field" <mat-tab label="WooCommerce">
[style.width.%]="100"> <div >
<mat-label>Account Code</mat-label> <div *ngIf="wooOwner == ''" align="center">
<input matInput <button mat-raised-button color="primary" (click)="generateWooToken()">
width="100%" Generate Token
placeholder="9999999999" </button>
formControlName="xAcc" </div>
(keyup)="checkStatus($event)"> <table *ngIf="wooOwner != ''">
<!-- <tbody>
(change)="xeroAccCodChanged($event)"> <tr>
--> <td class="heading" style="width: 100%;">Owner:</td>
</mat-form-field> </tr>
<tr>
<td>
<div>
<textarea disabled
style="border: none;
outline: none;
min-height: 150px;
width: 94%;"
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="3">{{wooOwner}}
</textarea>
</div>
</td>
<td class="urlCopyBtn">
<a (click)='copyWooOwner()' >
<fa-icon [icon]="faCopy"
class="copy-button">
</fa-icon>
</a>
</td>
</tr>
<tr>
<td class="heading" style="width: 60%;">Token:</td>
</tr>
<tr>
<td>
<div>
<textarea disabled
style="border: none;
outline: none;
min-height: 150px;
width: 94%;"
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="3">{{wooToken}}
</textarea>
</div>
</td>
<td class="urlCopyBtn">
<a (click)='copyWooToken()' >
<fa-icon [icon]="faCopy"
class="copy-button">
</fa-icon>
</a>
</td>
</tr>
<tr>
<td class="heading" style="width: 60%;">URL:</td>
</tr>
<tr>
<td>
<div>
<textarea disabled
style="border: none;
outline: none;
min-height: 150px;
width: 94%;"
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="3">{{wooUrl}}
</textarea>
</div>
</td>
</tr>
</tbody>
</table>
<div class="toolbar" align="center">
<button mat-raised-button
(click)="close()">
Close
</button>
</div>
</div> </div>
</mat-dialog-content> </mat-tab>
</div> </mat-tab-group>
<div class="container"
style="display: flex;
justify-content: space-between;
align-items: center;">
<button mat-raised-button
(click)="closeIntegration()">
Close
</button>
<button *ngIf="saveAccOk"
mat-raised-button
color="primary"
(click)="saveAccCod()">
Save Code
</button>
</div>
<div style="height: 20px;
margin-top: 10px;">
</div>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>

View file

@ -5,6 +5,7 @@ import { UntypedFormBuilder, Validators, UntypedFormGroup, FormControl } from '@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Owner } from '../owner.model'; import { Owner } from '../owner.model';
import { XeroService } from '../xero.service'; import { XeroService } from '../xero.service';
import { WoocommerceService } from '../woocommerce.service';
import { NotifierService } from '../notifier.service'; import { NotifierService } from '../notifier.service';
import { faCopy } from '@fortawesome/free-solid-svg-icons'; import { faCopy } from '@fortawesome/free-solid-svg-icons';
@ -56,6 +57,13 @@ export class SettingsComponent implements OnInit {
xeroLink: string = ''; xeroLink: string = '';
localToken: string = ''; localToken: string = '';
clientId: string = ''; clientId: string = '';
wooOwner: string = '';
wooToken: string = '';
wooUrl: string = '';
wooOwnerUpdate: Observable<string>;
wooTokenUpdate: Observable<string>;
wooUrlUpdate: Observable<string>;
clientIdUpdate: Observable<string>; clientIdUpdate: Observable<string>;
accCodeUpdate: Observable<string>; accCodeUpdate: Observable<string>;
linked2Xero : boolean = false; linked2Xero : boolean = false;
@ -65,6 +73,7 @@ export class SettingsComponent implements OnInit {
private notifierService : NotifierService, private notifierService : NotifierService,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
public xeroService: XeroService, public xeroService: XeroService,
public wooService: WoocommerceService,
private dialogRef: MatDialogRef<SettingsComponent>, private dialogRef: MatDialogRef<SettingsComponent>,
@Inject(MAT_DIALOG_DATA) public data: Owner) { @Inject(MAT_DIALOG_DATA) public data: Owner) {
this.useZats = data.zats; this.useZats = data.zats;
@ -102,6 +111,19 @@ export class SettingsComponent implements OnInit {
console.log("xeroAccCod -> [" + this.xeroAccCod + "]"); console.log("xeroAccCod -> [" + this.xeroAccCod + "]");
this.accCodForm.get('xAcc')!.setValue(this.xeroAccCod); this.accCodForm.get('xAcc')!.setValue(this.xeroAccCod);
}); });
this.wooOwnerUpdate = wooService.ownerUpdate;
this.wooTokenUpdate = wooService.tokenUpdate;
this.wooUrlUpdate = wooService.siteurlUpdate;
wooService.getWooToken(this.owner._id!);
this.wooOwnerUpdate.subscribe(owData => {
this.wooOwner = owData;
});
this.wooTokenUpdate.subscribe(tkData => {
this.wooToken = tkData;
});
this.wooUrlUpdate.subscribe(uData => {
this.wooUrl = uData;
});
} }
ngOnInit() { ngOnInit() {
@ -128,7 +150,7 @@ export class SettingsComponent implements OnInit {
closeIntegration() { closeIntegration() {
if ( (this.xeroAccCod == '') && (this.linked2Xero) ) if ( (this.xeroAccCod == '') && (this.linked2Xero) )
this.notifierService this.notifierService
.showNotification("Payment confirmation disabled!!","Close",'warning'); .showNotification("Xero Payment confirmation disabled!!","Close",'warning');
this.dialogRef.close(); this.dialogRef.close();
} }
@ -179,6 +201,45 @@ export class SettingsComponent implements OnInit {
} }
} }
copyWooOwner(){
try {
navigator.clipboard.writeText(this.wooOwner);
this.notifierService.showNotification("Owner ID copied to clipboard", "Close", "success");
} catch (err) {
this.notifierService.showNotification("Copying not available in your browser", "Close", "error");
}
}
copyWooToken(){
try {
navigator.clipboard.writeText(this.wooToken);
this.notifierService.showNotification("WooCommerce Token copied to clipboard", "Close", "success");
} catch (err) {
this.notifierService.showNotification("Copying not available in your browser", "Close", "error");
}
}
generateWooToken(){
this.wooService.createWooToken(this.owner._id!).subscribe(responseData => {
if (responseData.status == 202) {
this.notifierService.showNotification("WooCommerce Token generated!", "Close", "success");
this.wooService.getWooToken(this.owner._id!);
this.wooOwnerUpdate.subscribe(owData => {
this.wooOwner = owData;
});
this.wooTokenUpdate.subscribe(tkData => {
this.wooToken = tkData;
});
this.wooUrlUpdate.subscribe(uData => {
this.wooUrl = uData;
});
close();
} else {
this.notifierService.showNotification("WooCommerce Token generation failed.", "Close", "error");
}
});
}
saveAccCod() { saveAccCod() {
this.xeroAccCod = this.accCodForm.value.xAcc; this.xeroAccCod = this.accCodForm.value.xAcc;

View file

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { WoocommerceService } from './woocommerce.service';
describe('WoocommerceService', () => {
let service: WoocommerceService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(WoocommerceService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View file

@ -0,0 +1,65 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { ConfigData } from './configdata';
var Buffer = require('buffer/').Buffer;
@Injectable({
providedIn: 'root'
})
export class WoocommerceService {
beUrl = ConfigData.Be_URL;
private reqHeaders: HttpHeaders;
private ownerId: string = '';
private token: string = '';
private siteurl: string = '';
private _ownerIdUpdated: BehaviorSubject<string> = new BehaviorSubject(this.ownerId);
private _tokenUpdated: BehaviorSubject<string> = new BehaviorSubject(this.token);
private _siteurlUpdated: BehaviorSubject<string> = new BehaviorSubject(this.siteurl);
public readonly ownerUpdate: Observable<string> = this._ownerIdUpdated.asObservable();
public readonly tokenUpdate: Observable<string> = this._tokenUpdated.asObservable();
public readonly siteurlUpdate: Observable<string> = this._siteurlUpdated.asObservable();
constructor(
private http: HttpClient
) {
var auth = 'Basic ' + Buffer.from(ConfigData.UsrPwd).toString('base64');
this.reqHeaders = new HttpHeaders().set('Authorization', auth);
this._ownerIdUpdated.next(Object.assign({}, this).ownerId);
this._tokenUpdated.next(Object.assign({}, this).token);
this._siteurlUpdated.next(Object.assign({}, this).siteurl);
}
getWooToken(ownerId: string) {
const params = new HttpParams().append('ownerid', ownerId);
let obs = this.http.get<{ownerid: string, token: string, siteurl: string}>(this.beUrl + 'api/wootoken', {headers: this.reqHeaders, params: params, observe: 'response'});
obs.subscribe(tokenResponse => {
if (tokenResponse.status == 200) {
this.ownerId = tokenResponse.body!.ownerid;
this.token = tokenResponse.body!.token;
this.siteurl = tokenResponse.body!.siteurl;
this._ownerIdUpdated.next(Object.assign({}, this).ownerId);
this._tokenUpdated.next(Object.assign({}, this).token);
this._siteurlUpdated.next(Object.assign({}, this).siteurl);
} else {
console.log('No WooCommerce token found');
}
});
return obs;
}
createWooToken(ownerId: string) {
const params = new HttpParams().append('ownerid', ownerId);
let obs = this.http.post(this.beUrl+'api/wootoken', {}, {headers: this.reqHeaders, params: params, observe: 'response'});
obs.subscribe(responseData => {
if (responseData.status == 202) {
console.log('WooToken created.');
} else {
console.log('WooToken creation failed.');
}
});
return obs;
}
}