diff --git a/src/app/app.module.ts b/src/app/app.module.ts index f7197b8..06874cd 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -12,6 +12,7 @@ import { SocketIoConfig, SocketIoModule } from 'ngx-socket-io'; import { HttpClientModule } from '@angular/common/http'; import { AppRoutingModule } from 'src/routes'; import { NotFoundComponent } from './not-found/not-found.component'; +import { CartComponent } from './cart/cart.component'; const config: SocketIoConfig = { url: 'http://localhost:2009', options: {} }; @@ -22,7 +23,8 @@ const config: SocketIoConfig = { url: 'http://localhost:2009', options: {} }; PaymentComponent, PayComponent, HelloComponent, - NotFoundComponent + NotFoundComponent, + CartComponent ], imports: [ BrowserModule, diff --git a/src/app/backend.service.ts b/src/app/backend.service.ts index ea0ef07..466164f 100644 --- a/src/app/backend.service.ts +++ b/src/app/backend.service.ts @@ -1,3 +1,4 @@ +import { getCurrencySymbol } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Socket } from 'ngx-socket-io'; @@ -26,8 +27,9 @@ export interface ICart { } export interface IPaymentMethod { - method: any; + method: CryptoUnits; amount: number; + exRate: number; } export enum PaymentStatus { @@ -43,19 +45,20 @@ export enum PaymentStatus { export interface IInvoice { selector: string; paymentMethods: IPaymentMethod[]; - receiveAddress: string; - paidWith?: CryptoUnits; - paid?: number; + paymentMethod?: CryptoUnits; + receiveAddress?: string; transcationHash?: string; + confirmation?: number; cart?: ICart[]; totalPrice?: number; currency: string; - dueBy: number; + dueBy: Date; status?: PaymentStatus; email?: string; successUrl: string; cancelUrl: string; createdAt?: number; + } @Injectable({ @@ -70,9 +73,8 @@ export class BackendService { selector: '', paymentMethods: [], receiveAddress: '', - paid: 0, currency: 'USD', - dueBy: Date.now(), + dueBy: new Date(), successUrl: '', cancelUrl: '' }; @@ -102,6 +104,9 @@ export class BackendService { return this.socket; } + /** + * Subscribe to the real-time status of the selected invoice. + */ subscribeTo(selector: string): void { this.socket.on('subscribe', (status: boolean) => { if (status) { @@ -118,12 +123,18 @@ export class BackendService { this.socket.emit('subscribe', { selector }); } + /** + * This will update the current invoice + */ updateInvoice(): void { if (this.invoice !== undefined || this.invoice !== null) { this.setInvoice(this.invoice.selector); } } + /** + * This will set the current selected invoice by the `selector`. + */ setInvoice(selector: string): Promise { return new Promise(async (resolve, reject) => { if (selector === undefined || selector === 'undefined' || selector === '') { @@ -144,6 +155,9 @@ export class BackendService { }); } + /** + * This will notify the backend that the user just cancelled the payment. + */ cancelInvoice(): Promise { return new Promise(async (resolve, reject) => { if (this.invoice.selector === '') { @@ -162,6 +176,9 @@ export class BackendService { }); } + /** + * This will set the payment method of the selected invoice. + */ setPaymentMethod(method: CryptoUnits): Promise { return new Promise(async (resolve, reject) => { if (this.invoice === null) { reject('Invoice is not set!'); return; } @@ -170,12 +187,20 @@ export class BackendService { responseType: 'json' }).toPromise().then(() => { this.setInvoice(this.invoice.selector); + resolve(); }).catch(err => { reject(err); }); }); } + currencyPrefix(): string { + return getCurrencySymbol(this.invoice.currency, 'narrow'); + } + + /** + * Can be used if the socket connection is broken or as initial call. + */ getConfirmation(): Promise { return new Promise(async (resolve, reject) => { if (this.invoice === null || this.invoice.status !== PaymentStatus.UNCONFIRMED) { @@ -197,7 +222,7 @@ export class BackendService { } /** - * @returns Path to icon + * @returns Path to icon in assets folder */ getIcon(unit: CryptoUnits): string { switch (unit) { @@ -226,12 +251,30 @@ export class BackendService { return null; } + /** + * @returns The price to pay by cryptocurrency; + */ getAmount(): string | undefined { return this.invoice?.paymentMethods.find(item => { - return item.method === CryptoUnits.BITCOIN; + return item.method === this.invoice.paymentMethod; })?.amount.toFixed(8); } + /** + * Calculate the price in crypto of a specifc product. + * @param prodcut Index of product in cart + */ + calculateCryptoPrice(productNr: number): number { + if (this.invoice.cart === undefined) return 0; + if (this.invoice.paymentMethod === undefined) return 0; + + const product = this.invoice.cart[productNr]; + const exRate = this.invoice.paymentMethods.find(method => { return method.method === this.invoice.paymentMethod })?.exRate; + if (exRate === undefined) return 0; + + return product.quantity * product.price / exRate; + } + getStatus(): string { switch (this.invoice?.status) { case PaymentStatus.PENDING: diff --git a/src/app/cart/cart.component.css b/src/app/cart/cart.component.css new file mode 100644 index 0000000..ea5aeff --- /dev/null +++ b/src/app/cart/cart.component.css @@ -0,0 +1,50 @@ +.cart { + margin: 0; + padding: 0; + width: 100%; + height: 400px; + background-color: #1c1c1c; + border-radius: 8px; + transform: translateY(-8px); + color: #fff; +} + +.cart ul { + list-style: none; + margin: 0; + padding: 20px; + overflow-y: auto; + height: 359px; +} + +.cart ul li { + display: grid; + padding: 1rem; + grid-template-columns: 64px 1fr auto; + grid-column-gap: 1rem; + border-radius: 12px; +} + +.cart ul li:nth-child(even) { + background-color: #292929; +} + +.image { + height: 64px; + width: 64px; + padding: 0; + margin: 0; + border-radius: 8px; +} + +.name { + font-size: 11pt; + width: 80%; + margin: auto 0; + line-break: loose; +} + +.price { + text-align: right; + margin: auto; +} \ No newline at end of file diff --git a/src/app/cart/cart.component.html b/src/app/cart/cart.component.html new file mode 100644 index 0000000..862ef10 --- /dev/null +++ b/src/app/cart/cart.component.html @@ -0,0 +1,10 @@ +
+
    +
  • + +
    {{ item.name }}
    + {{ item.price.toFixed(2) }} {{ this.backend.currencyPrefix() }}
    + {{ this.backend.calculateCryptoPrice(indexOfelement).toFixed(8) }} {{ this.backend.invoice.paymentMethod }}
    +
  • +
+
\ No newline at end of file diff --git a/src/app/cart/cart.component.spec.ts b/src/app/cart/cart.component.spec.ts new file mode 100644 index 0000000..4888049 --- /dev/null +++ b/src/app/cart/cart.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CartComponent } from './cart.component'; + +describe('CartComponent', () => { + let component: CartComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CartComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/cart/cart.component.ts b/src/app/cart/cart.component.ts new file mode 100644 index 0000000..66c29d4 --- /dev/null +++ b/src/app/cart/cart.component.ts @@ -0,0 +1,20 @@ +import { Component, OnInit } from '@angular/core'; +import { BackendService } from '../backend.service'; +import { StateService } from '../state.service'; + +@Component({ + selector: 'app-cart', + templateUrl: './cart.component.html', + styleUrls: ['./cart.component.css'] +}) +export class CartComponent implements OnInit { + + constructor( + public backend: BackendService, + public state: StateService + ) { } + + ngOnInit(): void { + } + +} diff --git a/src/app/header/header.component.css b/src/app/header/header.component.css index b8e54e4..dd474b3 100644 --- a/src/app/header/header.component.css +++ b/src/app/header/header.component.css @@ -1,6 +1,7 @@ .header { display: grid; - grid-template-columns: 1fr auto; + grid-template-columns: auto 1fr auto 1fr auto; + grid-template-rows: 1fr; margin: 0; padding: 0; background-color: #fff; @@ -12,23 +13,43 @@ border-bottom-right-radius: 0; } -.header img { - height: 3rem; +.header .logo { margin-top: 0.5rem; margin-bottom: 1rem; - grid-column: 1; + grid-column: 3; } -.header a { +.header .logo img { + height: 3rem; +} + +/* Cancel button */ +.cancel { color: red; border: 1px solid red; border-block-end-width: 1px; border-radius: 5px; padding: 0.8rem; - margin: auto 0; - height: fit-content; - float: right; + margin: auto; cursor: pointer; - grid-column: 2; + width: fit-content !important; + grid-column: 5; transition: .2s ease; +} + +#cart { + display: flex; + grid-column: 1; + vertical-align: middle; + margin: auto auto; + padding-left: 1rem; + cursor: pointer; + border-radius: 8px; + border: 1px solid #000; + padding: 0.8rem; +} +#cart span { + margin: auto auto; + padding-left: .5rem; + font-weight: bold; } \ No newline at end of file diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index 25ca8e1..ebc7130 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -1,4 +1,24 @@ \ No newline at end of file diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index a3d509d..0cb15d2 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { BackendService } from '../backend.service'; +import { StateService } from '../state.service'; @Component({ selector: 'app-header', @@ -12,7 +13,10 @@ export class HeaderComponent implements OnInit { cancelProgress = 0; cancelProgressStyle = ""; // This is the style of the cancel button - constructor(public backend: BackendService) { } + constructor( + public backend: BackendService, + public state: StateService + ) { } ngOnInit(): void { } @@ -21,9 +25,10 @@ export class HeaderComponent implements OnInit { if (!this.startCancelling) { this.startCancelling = true; const animation = setInterval(() => { - this.cancelProgress += 0.3; + this.cancelProgress += 0.5; this.cancelProgressStyle = ` background-image: linear-gradient(90deg, rgba(255,120,120,0.3) ${this.cancelProgress.toFixed(1)}%, rgba(255,255,255,0) ${this.cancelProgress.toFixed(1)}%); + margin-left: 16px; transform: scale(1.1); `; diff --git a/src/app/pay/pay.component.ts b/src/app/pay/pay.component.ts index 521577e..aa0d71d 100644 --- a/src/app/pay/pay.component.ts +++ b/src/app/pay/pay.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { BackendService } from '../backend.service'; +import { StateService } from '../state.service'; @Component({ selector: 'app-pay', @@ -8,7 +9,10 @@ import { BackendService } from '../backend.service'; }) export class PayComponent implements OnInit { - constructor(public backend: BackendService) { } + constructor( + public backend: BackendService, + public state: StateService + ) { } ngOnInit(): void { } diff --git a/src/app/payment/payment.component.html b/src/app/payment/payment.component.html index b7dcc52..b6e0eda 100644 --- a/src/app/payment/payment.component.html +++ b/src/app/payment/payment.component.html @@ -3,7 +3,7 @@ -->

Choose your
payment method

-

{{ this.backend.invoice.totalPrice!.toFixed(2) }} {{ currencyPrefix() }}

+

{{ this.backend.invoice.totalPrice!.toFixed(2) }} {{ this.backend.currencyPrefix() }}

  • @@ -19,10 +19,11 @@ -
    -
    +
    - Send to + Send to

    {{ this.backend.invoice?.receiveAddress }}

    - Amount -

    {{ this.backend.getAmount() }} BTC | {{ this.backend.invoice.totalPrice!.toFixed(2) }} {{ currencyPrefix() }}

    + Amount +

    {{ this.backend.getAmount() }} BTC | {{ this.backend.invoice.totalPrice!.toFixed(2) }} {{ this.backend.currencyPrefix() }}

    - Status + Status

    {{ status }}
    @@ -66,6 +67,11 @@

    + + + diff --git a/src/app/payment/payment.component.ts b/src/app/payment/payment.component.ts index 11934ac..1b43b55 100644 --- a/src/app/payment/payment.component.ts +++ b/src/app/payment/payment.component.ts @@ -1,8 +1,8 @@ -import { getCurrencySymbol } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { BackendService, CryptoUnits } from '../backend.service'; +import { StateService } from '../state.service'; @Component({ selector: 'app-payment', @@ -13,15 +13,21 @@ export class PaymentComponent implements OnInit { paymentSelector = ''; confirmations = 0; - choosenPaymentMethod = CryptoUnits.BITCOIN; status: string; ready = false; + // XYZ class (will be xyz-out if cart is shown for example) + xyzClass: string; + hideMain: boolean; + constructor( public backend: BackendService, + public state: StateService, private route: ActivatedRoute ) { this.status = this.backend.getStatus(); + this.hideMain = false; + this.xyzClass = 'xyz-in'; } ngOnInit(): void { @@ -31,6 +37,20 @@ export class PaymentComponent implements OnInit { this.get(); }); + this.state.showCart.subscribe(cartStatus => { + if (cartStatus) { + this.xyzClass = 'xyz-out'; + setTimeout(() => { + this.hideMain = true; + }, 700); + } else { + setTimeout(() => { + this.hideMain = false; + this.xyzClass = 'xyz-in'; + }, 600); + } + }) + this.backend.invoiceUpdate.subscribe(newInvoice => { this.status = this.backend.getStatus(); }); @@ -40,10 +60,6 @@ export class PaymentComponent implements OnInit { this.backend.setPaymentMethod(coin); } - currencyPrefix(): string { - return getCurrencySymbol(this.backend.invoice.currency, 'narrow'); - } - async get(): Promise { await this.backend.setInvoice(this.paymentSelector); this.backend.getConfirmation().catch(); diff --git a/src/app/state.service.spec.ts b/src/app/state.service.spec.ts new file mode 100644 index 0000000..405ec5f --- /dev/null +++ b/src/app/state.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { StateService } from './state.service'; + +describe('StateService', () => { + let service: StateService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(StateService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/state.service.ts b/src/app/state.service.ts new file mode 100644 index 0000000..6fb8c69 --- /dev/null +++ b/src/app/state.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { BackendService, PaymentStatus } from './backend.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * The state service is responsible to exchange data between components. + */ +export class StateService { + + showCart: BehaviorSubject; + + constructor(private backend: BackendService) { + this.showCart = new BehaviorSubject(false); + + this.backend.invoiceUpdate.subscribe(invoice => { + this.showCart.next(false); // Hide cart if status changes + }); + } + + toggleCart() { + this.showCart.next(!this.showCart.value); + } +} diff --git a/src/styles.css b/src/styles.css index 4772b3e..869a5d1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -11,4 +11,14 @@ body, html { :host{ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; +} + +::-webkit-scrollbar { + background-color: transparent; + width: 8px; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255); + border-radius: 8px; } \ No newline at end of file