Add payment methods screen

This commit is contained in:
2020-12-29 00:10:44 +01:00
parent d997da79ba
commit 84e4910c70
15 changed files with 326 additions and 55 deletions

View File

@@ -1,6 +1,7 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Socket } from 'ngx-socket-io'; import { Socket } from 'ngx-socket-io';
import { BehaviorSubject } from 'rxjs';
/* /*
* The following interfaces are copied from the backend. * The following interfaces are copied from the backend.
@@ -26,17 +27,16 @@ export interface ICart {
export interface IPaymentMethod { export interface IPaymentMethod {
method: any; method: any;
amount: number amount: number;
} }
export enum PaymentStatus { export enum PaymentStatus {
CANCELLED = -2,
REQUESTED = -1,
PENDING = 0, PENDING = 0,
PARTIALLY = 1, UNCONFIRMED = 1,
UNCONFIRMED = 2, DONE = 2,
DONE = 3,
CANCELLED = 4
} }
export interface IInvoice { export interface IInvoice {
selector: string; selector: string;
paymentMethods: IPaymentMethod[]; paymentMethods: IPaymentMethod[];
@@ -62,33 +62,132 @@ export class BackendService {
SERVER_URL = 'http://localhost:2009'; SERVER_URL = 'http://localhost:2009';
invoice: IInvoice | null = null;
invoiceUpdate: BehaviorSubject<IInvoice | null>;
constructor( constructor(
private socket: Socket, private socket: Socket,
private http: HttpClient private http: HttpClient
) { ) {
this.invoiceUpdate = new BehaviorSubject<IInvoice | null>(null);
this.socket.on('status', (data: any) => { this.socket.on('status', (data: any) => {
console.log('Status has been updated to: ', data); console.log('Status has been updated to: ', data);
}); });
this.socket.on('subscribe', (success: boolean) => { this.socket.on('subscribe', (success: boolean) => {
if (success) { console.log('We\'re getting the progress of this invoice!'); } if (success) { console.log('We\'re getting the progress of this invoice!'); }
else { console.log('Subscription failed'); } else { console.log('Subscribtion failed'); }
}); });
} }
subscribeTo(selector: string) { getSocket(): Socket {
return this.socket;
}
subscribeTo(selector: string): void {
this.socket.on('subscribe', (status: boolean) => {
if (status) {
this.updateInvoice();
console.log('Successfully subscribed to this invoice');
}
else { console.log('Failed to subscribe'); }
});
this.socket.emit('subscribe', { selector }); this.socket.emit('subscribe', { selector });
} }
getInvoice(selector: string): Promise<IInvoice> { updateInvoice(): void {
if (this.invoice !== undefined || this.invoice !== null) {
this.setInvoice(this.invoice?.selector!);
}
}
setInvoice(selector: string): Promise<IInvoice> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
this.http.get(this.SERVER_URL + '/invoice/' + selector, { this.http.get(this.SERVER_URL + '/invoice/' + selector, {
observe: 'body', observe: 'body',
responseType: 'json' responseType: 'json'
}).toPromise().then((invoice) => { }).toPromise().then((invoice) => {
resolve(invoice as IInvoice); this.invoice = invoice as IInvoice;
this.invoiceUpdate.next(this.invoice);
resolve(this.invoice);
}).catch(err => { }).catch(err => {
reject(err); reject(err);
}); });
}); });
} }
setPaymentMethod(method: CryptoUnits): Promise<void> {
return new Promise(async (resolve, reject) => {
this.http.post(`${this.SERVER_URL}/invoice/${this.invoice?.selector}/setmethod`, { method }, {
responseType: 'json'
}).toPromise().then(() => {
this.setInvoice(this.invoice!!.selector);
}).catch(err => {
reject(err);
});
});
}
/**
* @returns Path to icon
*/
getIcon(unit: CryptoUnits): string {
switch (unit) {
case CryptoUnits.BITCOIN:
return 'assets/Bitcoin.svg';
case CryptoUnits.BITCOINCASH:
return 'assets/BitcoinCash.svg';
case CryptoUnits.DOGECOIN:
return 'assets/Dogecoin.png';
case CryptoUnits.ETHEREUM:
return 'assets/Ethereum.svg';
case CryptoUnits.LITECOIN:
return 'assets/Litecoin.svg';
case CryptoUnits.MONERO:
return 'assets/Monero.svg';
}
}
findCryptoBySymbol(symbol: string): string | null {
for (const coin in CryptoUnits) {
// @ts-ignore: This actually works but I thing it's too hacky for TS. Allow me this one, please?
if (CryptoUnits[coin] === symbol.toUpperCase()) {
return coin.charAt(0).toUpperCase() + coin.toLowerCase().slice(1);
}
}
return null;
}
getAmount(): string | undefined {
return this.invoice?.paymentMethods.find(item => {
return item.method === CryptoUnits.BITCOIN;
})?.amount.toFixed(8);
}
getStatus(): string {
switch (this.invoice?.status) {
case PaymentStatus.PENDING:
return 'Pending';
case PaymentStatus.UNCONFIRMED:
return 'Unconfirmed';
case PaymentStatus.DONE:
return 'Paid';
case PaymentStatus.CANCELLED:
return 'Cancelled';
default:
return 'Unknown';
}
}
isInvoiceDone(): boolean {
return this.invoice?.status === PaymentStatus.DONE;
}
isInvoicePending(): boolean {
return this.invoice?.status === PaymentStatus.PENDING;
}
isInvoiceRequested(): boolean {
return this.invoice?.status === PaymentStatus.REQUESTED;
}
} }

View File

@@ -1,4 +1,4 @@
<div class="header"> <div class="header">
<h2>LibrePay</h2> <h2>LibrePay</h2>
<a>Cancel payment</a> <a *ngIf="this.backend.isInvoicePending()">Cancel payment</a>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { BackendService } from '../backend.service';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
@@ -7,7 +8,7 @@ import { Component, OnInit } from '@angular/core';
}) })
export class HeaderComponent implements OnInit { export class HeaderComponent implements OnInit {
constructor() { } constructor(public backend: BackendService) { }
ngOnInit(): void { ngOnInit(): void {
} }

View File

@@ -19,6 +19,17 @@
/* box-shadow: 1px 1px 112px 23px rgba(0,0,0,0.75); */ /* box-shadow: 1px 1px 112px 23px rgba(0,0,0,0.75); */
} }
.smaller {
min-width: 200px;
width: 40vw !important;
}
@media (max-width: 800px) {
.content {
width: 100vw !important;
}
}
.content * { .content * {
width: 100%; width: 100%;
} }

View File

@@ -6,7 +6,7 @@
<div class="cube"></div> <div class="cube"></div>
<div class="cube"></div> <div class="cube"></div>
</div> </div>
<div class="content"> <div class="content" [ngClass]="{smaller: this.backend.isInvoiceRequested()}">
<app-header></app-header> <app-header></app-header>
<app-payment></app-payment> <app-payment></app-payment>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { BackendService } from '../backend.service';
@Component({ @Component({
selector: 'app-pay', selector: 'app-pay',
@@ -7,7 +8,7 @@ import { Component, OnInit } from '@angular/core';
}) })
export class PayComponent implements OnInit { export class PayComponent implements OnInit {
constructor() { } constructor(public backend: BackendService) { }
ngOnInit(): void { ngOnInit(): void {
} }

View File

@@ -1,15 +1,23 @@
.payment { .payment {
display: grid;
grid-template-columns: 1fr 1fr;
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 400px; height: 500px;
background-color: hsl(0, 0%, 11%); background-color: hsl(0, 0%, 11%);
border-radius: 8px; border-radius: 8px;
transform: translateY(-8px); transform: translateY(-8px);
} }
.request {
transform: translateY(-40px);
}
.main {
display: grid;
height: 400px;
grid-template-columns: 1fr 1fr;
}
.qr { .qr {
position: relative; position: relative;
transform: translateY(25%); transform: translateY(25%);
@@ -42,12 +50,12 @@
} }
/* Data */ /* Data */
.data { .main .data {
display: grid; display: grid;
grid-template-columns: 1fr 50px; grid-template-columns: 1fr 50px;
grid-template-rows: 1fr 4rem 4rem 4rem 1fr; grid-template-rows: 1fr 4rem 4rem 4rem 1fr;
} }
#target { .main #target {
grid-row: 2; grid-row: 2;
} }
#amount { #amount {
@@ -57,6 +65,65 @@
grid-row: 4; grid-row: 4;
} }
h3 { .main h3 {
line-height: 0; line-height: 0;
} }
#title {
padding-top: 2rem;
line-height: 1;
font-size: 20pt;
font-weight: normal;
text-align: center;
}
#price {
text-align: center;
font-size: 16pt;
}
#list {
overflow-y: scroll;
scroll-behavior: smooth;
-ms-overflow-style: none;
scrollbar-width: none;
margin: 0;
padding: 0;
}
#list::-webkit-scrollbar {
display: none;
}
#list li {
padding-top: 10px;
padding-left: 5%;
padding-right: 5%;
border-radius: 8px;
display: grid;
cursor: pointer;
grid-template-columns: 75px 1fr;
grid-row: 1fr;
list-style: none;
padding-bottom: 1rem;
transition: .3s ease;
line-height: 0;
}
#list li:hover {
box-shadow: 0px 0px 61px 3px rgba(0,0,0,0.3) inset;
}
#list li img {
height: 42px;
width: 42px;
margin: 0 auto;
padding-top: 12px;
grid-row-start: 1;
grid-row-end: 2;
}
#list li p {
grid-row: 1;
grid-column: 2;
}
#list li h4 {
grid-row: 2;
grid-column: 2;
}

View File

@@ -1,9 +1,23 @@
<div class="payment"> <div class="payment request" *ngIf="this.backend.isInvoiceRequested()">
<h3 id="title">Choose your<br>payment method</h3>
<p id="price">{{ this.backend.invoice!!.totalPrice!!.toFixed(2) }} €</p>
<ul id="list">
<li *ngFor="let coin of this.backend.invoice!!.paymentMethods" (click)="chooseMethod(coin.method)">
<img [src]="this.backend.getIcon(coin.method)">
<div>
<h4>{{ this.backend.findCryptoBySymbol(coin.method) }}</h4>
<p>{{ coin.amount }} {{ coin.method }}</p>
</div>
</li>
</ul>
</div>
<div class="payment main" *ngIf="!this.backend.isInvoiceRequested() && ready">
<div class="qrWrapper"> <div class="qrWrapper">
<div class="qr"> <div class="qr">
<img src="assets/Bitcoin.svg"> <img src="assets/Bitcoin.svg">
<qrcode <qrcode
[qrdata]="'bitcoin:' + invoice!!.receiveAddress" [qrdata]="'bitcoin:' + this.backend.invoice!!.receiveAddress"
[width]="256" [width]="256"
[errorCorrectionLevel]="'M'" [errorCorrectionLevel]="'M'"
[elementType]="'svg'" [elementType]="'svg'"
@@ -12,16 +26,16 @@
</div> </div>
</div> </div>
<div class="data" *ngIf="ready"> <div class="data">
<!-- Payment data --> <!-- Payment data -->
<span id="target">Send to <span id="target">Send to
<h3>{{ invoice!!.receiveAddress }}</h3> <h3>{{ this.backend.invoice!.receiveAddress }}</h3>
</span> </span>
<span id="amount">Amount <span id="amount">Amount
<h3>{{ getAmount() }} BTC</h3> <h3>{{ this.backend.getAmount() }} BTC</h3>
</span> </span>
<span id="status">Status <span id="status">Status
<h3>{{ getStatus() }}</h3> <h3>{{ this.backend.getStatus() }}</h3>
</span> </span>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { BackendService, IInvoice, CryptoUnits, PaymentStatus } from '../backend.service'; import { BackendService, IInvoice, CryptoUnits, PaymentStatus, IPaymentMethod } from '../backend.service';
@Component({ @Component({
selector: 'app-payment', selector: 'app-payment',
@@ -12,10 +12,9 @@ export class PaymentComponent implements OnInit {
paymentSelector = ''; paymentSelector = '';
choosenPaymentMethod = CryptoUnits.BITCOIN; choosenPaymentMethod = CryptoUnits.BITCOIN;
ready = false; ready = false;
invoice: IInvoice | null = null;
constructor( constructor(
private backend: BackendService, public backend: BackendService,
private route: ActivatedRoute private route: ActivatedRoute
) { } ) { }
@@ -27,32 +26,13 @@ export class PaymentComponent implements OnInit {
}); });
} }
async get() { chooseMethod(coin: CryptoUnits) {
this.invoice = await this.backend.getInvoice(this.paymentSelector); this.backend.setPaymentMethod(coin);
}
async get(): Promise<void> {
await this.backend.setInvoice(this.paymentSelector);
this.ready = true; this.ready = true;
} }
getAmount() {
return this.invoice?.paymentMethods.find(item => {
return item.method === CryptoUnits.BITCOIN;
})?.amount.toFixed(8);
}
getStatus() {
switch (this.invoice?.status) {
case PaymentStatus.PENDING:
return 'Pending';
case PaymentStatus.PARTIALLY:
return 'Partly';
case PaymentStatus.UNCONFIRMED:
return 'Unconfirmed';
case PaymentStatus.DONE:
return 'Paid';
case PaymentStatus.CANCELLED:
return 'Cancelled';
default:
return 'Unknown';
}
}
} }

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="64"
width="64"
version="1.1"
id="svg905"
sodipodi:docname="BitcoinCash.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<metadata
id="metadata911">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs909" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1024"
id="namedview907"
showgrid="false"
inkscape:zoom="8.859375"
inkscape:cx="32"
inkscape:cy="32"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g903" />
<g
transform="translate(0.00630876,-0.00301984)"
id="g903">
<path
fill="#f7931a"
d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"
id="path899"
style="fill:#679a1b;fill-opacity:1" />
<path
fill="#FFF"
d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"
id="path901" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
src/assets/Dogecoin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

11
src/assets/Ethereum.svg Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="417px" viewBox="0 0 256 417" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<polygon fill="#343434" points="127.9611 0 125.1661 9.5 125.1661 285.168 127.9611 287.958 255.9231 212.32"/>
<polygon fill="#8C8C8C" points="127.962 0 0 212.32 127.962 287.959 127.962 154.158"/>
<polygon fill="#3C3C3B" points="127.9611 312.1866 126.3861 314.1066 126.3861 412.3056 127.9611 416.9066 255.9991 236.5866"/>
<polygon fill="#8C8C8C" points="127.962 416.9052 127.962 312.1852 0 236.5852"/>
<polygon fill="#141414" points="127.9611 287.9577 255.9211 212.3207 127.9611 154.1587"/>
<polygon fill="#393939" points="0.0009 212.3208 127.9609 287.9578 127.9609 154.1588"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 840 B

1
src/assets/Litecoin.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" viewBox="0.847 0.876 329.254 329.256"><title>Litecoin</title><path d="M330.102 165.503c0 90.922-73.705 164.629-164.626 164.629C74.554 330.132.848 256.425.848 165.503.848 74.582 74.554.876 165.476.876c90.92 0 164.626 73.706 164.626 164.627" fill="#bebebe"/><path d="M295.15 165.505c0 71.613-58.057 129.675-129.674 129.675-71.616 0-129.677-58.062-129.677-129.675 0-71.619 58.061-129.677 129.677-129.677 71.618 0 129.674 58.057 129.674 129.677" fill="#bebebe"/><path d="M155.854 209.482l10.693-40.264 25.316-9.249 6.297-23.663-.215-.587-24.92 9.104 17.955-67.608h-50.921l-23.481 88.23-19.605 7.162-6.478 24.395 19.59-7.156-13.839 51.998h135.521l8.688-32.362h-84.601" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 750 B

18
src/assets/Monero.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="monero" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 200 200" style="enable-background:new 0 0 200 200;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#F16822;}
.st2{fill:#4D4D4D;}
</style>
<g>
<path class="st0" d="M197.5,100c0,53.8-43.7,97.5-97.5,97.5c-53.8,0-97.5-43.7-97.5-97.5C2.5,46.1,46.1,2.5,100,2.5
C153.8,2.5,197.5,46.1,197.5,100z"/>
<path id="_149931032_1_" class="st1" d="M100,2.5C46.2,2.5,2.4,46.2,2.5,100c0,10.8,1.7,21.1,4.9,30.8h29.2v-82l63.4,63.4
l63.4-63.4v82h29.2c3.2-9.7,4.9-20,5-30.8C197.6,46.2,153.8,2.5,100,2.5L100,2.5z"/>
<path id="_149931160_1_" class="st2" d="M85.4,126.7L57.8,99v51.6H47.2H36.6l-20,0c17.1,28.1,48,46.9,83.3,46.9
c35.3,0,66.2-18.8,83.3-46.9l-20,0h-18.9h-2.2V99l-27.7,27.7L100,141.3L85.4,126.7L85.4,126.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1020 B

View File

@@ -1,8 +1,13 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
*{ * {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
} }
body, html {
margin: 0;
padding: 0;
}
:host{ :host{
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;