Merge branch 'master' into spencer/censor-pgp-passphrase

# Conflicts:
#	package-lock.json
#	src/app/_helpers/http-getter.ts
#	src/app/_services/auth.service.ts
#	src/app/auth/auth.component.ts
#	src/assets/js/hoba-pgp.js
#	src/styles.scss
This commit is contained in:
Spencer Ofwiti 2021-06-07 18:44:26 +03:00
commit 159cedc86e
204 changed files with 57813 additions and 5969 deletions

1
.husky/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_

30
.husky/_/husky.sh Normal file
View File

@ -0,0 +1,30 @@
#!/bin/sh
if [ -z "$husky_skip_init" ]; then
debug () {
[ "$HUSKY_DEBUG" = "1" ] && echo "husky (debug) - $1"
}
readonly hook_name="$(basename "$0")"
debug "starting $hook_name..."
if [ "$HUSKY" = "0" ]; then
debug "HUSKY env variable is set to 0, skipping hook"
exit 0
fi
if [ -f ~/.huskyrc ]; then
debug "sourcing ~/.huskyrc"
. ~/.huskyrc
fi
export readonly husky_skip_init=1
sh -e "$0" "$@"
exitCode="$?"
if [ $exitCode != 0 ]; then
echo "husky - $hook_name hook exited with code $exitCode (error)"
exit $exitCode
fi
exit 0
fi

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
package.json
package-lock.json
yarn.lock
dist

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"useTabs": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"semi": true,
"bracketSpacing": true,
"arrowParens": "always"
}

View File

@ -10,7 +10,9 @@ Run `npm install -g @angular/cli` to install the angular CLI.
## Development server ## Development server
Run `npm run start:dev` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. Run `ng serve` for a local server, `npm run start:dev` for a dev server and `npm run start:prod` for a prod server..
Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding ## Code scaffolding
@ -22,11 +24,21 @@ Run `ng generate module module-name --route module-name --module app.module` to
## Build ## Build
Run `npm run build:dev` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `build:prod` script for a production build. Run `ng build` to build the project using local configurations.
The build artifacts will be stored in the `dist/` directory.
Use the `npm run build:dev` script for a development build and the `npm run build:prod` script for a production build.
## PWA
The app supports Progressive Web App capabilities.
Run `npm run start:pwa` to run the project in PWA mode.
PWA mode works using production configurations.
## Running unit tests ## Running unit tests
Run `npm run test:dev` to execute the unit tests via [Karma](https://karma-runner.github.io). Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests ## Running end-to-end tests
@ -34,11 +46,19 @@ Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protrac
## Environment variables ## Environment variables
Environment variables are contained in the `.env` file. See `.env.example` for a template. Default environment variables are located in the `src/environments/` directory.
Custom environment variables are contained in the `.env` file. See `.env.example` for a template.
Default environment variables are set in the `set-env.ts` file. Custom environment variables are set via the `set-env.ts` file.
Once loaded they will be populated in the directory `src/environments/`. Once loaded they will be populated in the directory `src/environments/`.
It contains environment variables for development on `environment.ts` and production on `environment.prod.ts`. It contains environment variables for development on `environment.dev.ts` and production on `environment.prod.ts`.
## Code formatting
The system has automated code formatting using [Prettier](https://prettier.io/) and [TsLint](https://palantir.github.io/tslint/).
To view the styling rules set, check out `.prettierrc` and `tslint.json`.
Run `npm run format:lint` To perform formatting and linting of the codebase.
## Further help ## Further help

View File

@ -26,19 +26,17 @@
"aot": true, "aot": true,
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
"src/assets" "src/assets",
"src/manifest.webmanifest"
], ],
"styles": [ "styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss", "src/styles.scss",
"node_modules/datatables.net-dt/css/jquery.dataTables.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css" "node_modules/bootstrap/dist/css/bootstrap.min.css"
], ],
"scripts": [ "scripts": [
"node_modules/jquery/dist/jquery.js", "node_modules/jquery/dist/jquery.js",
"node_modules/datatables.net/js/jquery.dataTables.js", "node_modules/bootstrap/dist/js/bootstrap.js"
"node_modules/bootstrap/dist/js/bootstrap.js",
"node_modules/block-syncer/dist/worker_ondemand.js"
] ]
}, },
"configurations": { "configurations": {
@ -68,7 +66,9 @@
"maximumWarning": "6kb", "maximumWarning": "6kb",
"maximumError": "10kb" "maximumError": "10kb"
} }
] ],
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json"
}, },
"dev": { "dev": {
"fileReplacements": [ "fileReplacements": [
@ -110,7 +110,8 @@
"codeCoverage": true, "codeCoverage": true,
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
"src/assets" "src/assets",
"src/manifest.webmanifest"
], ],
"styles": [ "styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",

31
ngsw-config.json Normal file
View File

@ -0,0 +1,31 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js",
"/assets/*.png"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
]
}

5507
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,10 +8,17 @@
"start:prod": "ng serve --prod", "start:prod": "ng serve --prod",
"build:dev": "ng build -c dev", "build:dev": "ng build -c dev",
"build:prod": "ng build --prod", "build:prod": "ng build --prod",
"test:dev": "ng test", "start:pwa": "npm run build:prod && http-server -p 4200 dist/cic-staff-client",
"test": "ng test",
"format:check": "prettier --config ./.prettierrc --list-different \"src/{app,environments,assets}/**/*.{ts,js,json,css,scss}\"",
"format:refactor": "prettier --config ./.prettierrc --write \"src/{app,environments,assets}/**/*.{ts,js,json,css,scss}\"",
"format:fix": "pretty-quick --staged",
"format:lint": "npm run format:refactor && npm run lint",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e", "e2e": "ng e2e",
"postinstall": "node patch-webpack.js" "precommit": "npm run format:fix && npm run lint",
"postinstall": "node patch-webpack.js",
"prepare": "husky install"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@ -25,28 +32,20 @@
"@angular/platform-browser": "~10.2.0", "@angular/platform-browser": "~10.2.0",
"@angular/platform-browser-dynamic": "~10.2.0", "@angular/platform-browser-dynamic": "~10.2.0",
"@angular/router": "~10.2.0", "@angular/router": "~10.2.0",
"@angular/service-worker": "~10.2.0",
"@cicnet/schemas-data-validator": "*",
"@popperjs/core": "^2.5.4", "@popperjs/core": "^2.5.4",
"angular-datatables": "^9.0.2",
"block-syncer": "^0.2.4",
"bootstrap": "^4.5.3", "bootstrap": "^4.5.3",
"chart.js": "^2.9.4", "cic-client": "0.1.4",
"cic-client": "^0.1.1", "cic-client-meta": "0.0.7-alpha.6",
"cic-client-meta": "0.0.7-alpha.3",
"datatables.net": "^1.10.22",
"datatables.net-dt": "^1.10.22",
"ethers": "^5.0.31", "ethers": "^5.0.31",
"http-server": "^0.12.3",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"mocha": "^8.2.1",
"moolb": "^0.1.0",
"ng2-charts": "^2.4.2",
"ngx-logger": "^4.2.1", "ngx-logger": "^4.2.1",
"openpgp": "^4.10.10",
"popper.js": "^1.16.1",
"rxjs": "~6.6.0", "rxjs": "~6.6.0",
"sha3": "^2.1.4", "sha3": "^2.1.4",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"vcard-parser": "^1.0.0", "vcard-parser": "^1.0.0",
"vcards-js": "^2.10.0",
"web3": "^1.3.0", "web3": "^1.3.0",
"zone.js": "~0.10.2" "zone.js": "~0.10.2"
}, },
@ -61,6 +60,7 @@
"@types/node": "^12.20.6", "@types/node": "^12.20.6",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"husky": "^6.0.0",
"jasmine-core": "~3.6.0", "jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0", "karma": "~5.0.0",
@ -69,11 +69,21 @@
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"karma-junit-reporter": "^2.0.1", "karma-junit-reporter": "^2.0.1",
"prettier": "^2.3.0",
"pretty-quick": "^3.1.0",
"protractor": "~7.0.0", "protractor": "~7.0.0",
"secp256k1": "^4.0.2", "secp256k1": "^4.0.2",
"ts-node": "~8.3.0", "ts-node": "~8.3.0",
"tslint": "~6.1.0", "tslint": "~6.1.0",
"tslint-angular": "^3.0.3",
"tslint-config-prettier": "^1.18.0",
"tslint-jasmine-rules": "^1.6.1",
"typescript": "~4.0.2", "typescript": "~4.0.2",
"yargs": "^13.3.2" "yargs": "^13.3.2"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged & ng lint"
}
} }
} }

View File

@ -6,7 +6,7 @@ require('dotenv').config();
const environment = argv.environment; const environment = argv.environment;
const isProduction = environment === 'prod'; const isProduction = environment === 'prod';
const targetPath = isProduction ? `./src/environments/environment.prod.ts` : `./src/environments/environment.ts`; const targetPath = isProduction ? `./src/environments/environment.prod.ts` : `./src/environments/environment.dev.ts`;
const environmentVars = `import {NgxLoggerLevel} from 'ngx-logger'; const environmentVars = `import {NgxLoggerLevel} from 'ngx-logger';
@ -15,13 +15,13 @@ export const environment = {
bloxbergChainId: ${process.env.CIC_CHAIN_ID || 8996}, bloxbergChainId: ${process.env.CIC_CHAIN_ID || 8996},
logLevel: ${process.env.LOG_LEVEL || 'NgxLoggerLevel.ERROR'}, logLevel: ${process.env.LOG_LEVEL || 'NgxLoggerLevel.ERROR'},
serverLogLevel: ${process.env.SERVER_LOG_LEVEL || 'NgxLoggerLevel.OFF'}, serverLogLevel: ${process.env.SERVER_LOG_LEVEL || 'NgxLoggerLevel.OFF'},
loggingUrl: '${process.env.CIC_LOGGING_URL || 'http://localhost:8000'}', loggingUrl: '${process.env.CIC_LOGGING_URL || ''}',
cicMetaUrl: '${process.env.CIC_META_URL || 'https://meta.dev.grassrootseconomics.net'}', cicMetaUrl: '${process.env.CIC_META_URL || 'https://meta.dev.grassrootseconomics.net'}',
publicKeysUrl: '${process.env.CIC_KEYS_URL || 'http://localhost:8000/keys.asc'}', publicKeysUrl: '${process.env.CIC_KEYS_URL || 'https://dev.grassrootseconomics.net/.well-known/publickeys'}',
cicCacheUrl: '${process.env.CIC_CACHE_URL || 'https://cache.dev.grassrootseconomics.net'}', cicCacheUrl: '${process.env.CIC_CACHE_URL || 'https://cache.dev.grassrootseconomics.net'}',
web3Provider: '${process.env.CIC_WEB3_PROVIDER || 'ws://localhost:63546'}', web3Provider: '${process.env.CIC_WEB3_PROVIDER || 'wss://bloxberg-ws.dev.grassrootseconomics.net'}',
cicUssdUrl: '${process.env.CIC_USSD_URL || 'https://ussd.dev.grassrootseconomics.net'}', cicUssdUrl: '${process.env.CIC_USSD_URL || 'https://ussd.dev.grassrootseconomics.net'}',
registryAddress: '${process.env.CIC_REGISTRY_ADDRESS || '0x6Ca3cB14aA6F761712E1C18646AfBA4d5Ae249E8'}', registryAddress: '${process.env.CIC_REGISTRY_ADDRESS || '0xea6225212005e86a4490018ded4bf37f3e772161'}',
trustedDeclaratorAddress: '${process.env.CIC_TRUSTED_ADDRESS || '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C'}' trustedDeclaratorAddress: '${process.env.CIC_TRUSTED_ADDRESS || '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C'}'
}; };
`; `;

View File

@ -1,10 +1,8 @@
// @ts-ignore
import * as accountIndex from '@src/assets/js/block-sync/data/AccountRegistry.json';
import { environment } from '@src/environments/environment'; import { environment } from '@src/environments/environment';
const Web3 = require('web3'); import Web3 from 'web3';
const web3 = new Web3(environment.web3Provider); const abi: Array<any> = require('@src/assets/js/block-sync/data/AccountsIndex.json');
const abi = accountIndex.default; const web3: Web3 = new Web3(environment.web3Provider);
export class AccountIndex { export class AccountIndex {
contractAddress: string; contractAddress: string;
@ -22,32 +20,31 @@ export class AccountIndex {
} }
public async totalAccounts(): Promise<number> { public async totalAccounts(): Promise<number> {
return await this.contract.methods.count().call(); return await this.contract.methods.entryCount().call();
} }
public async haveAccount(address: string): Promise<boolean> { public async haveAccount(address: string): Promise<boolean> {
return await this.contract.methods.accountIndex(address).call() !== 0; return (await this.contract.methods.have(address).call()) !== 0;
} }
public async last(numberOfAccounts: number): Promise<Array<string>> { public async last(numberOfAccounts: number): Promise<Array<string>> {
const count = await this.totalAccounts(); const count: number = await this.totalAccounts();
let lowest = count - numberOfAccounts - 1; let lowest: number = count - numberOfAccounts;
if (lowest < 0) { if (lowest < 0) {
lowest = 0; lowest = 0;
} }
let accounts = []; const accounts: Array<string> = [];
for (let i = count - 1; i > lowest; i--) { for (let i = count - 1; i >= lowest; i--) {
const account = await this.contract.methods.accounts(i).call(); const account: string = await this.contract.methods.entry(i).call();
accounts.push(account); accounts.push(account);
} }
return accounts; return accounts;
} }
public async addToAccountRegistry(address: string): Promise<boolean> { public async addToAccountRegistry(address: string): Promise<boolean> {
if (!await this.haveAccount(address)) { if (!(await this.haveAccount(address))) {
return await this.contract.methods.add(address).send({ from: this.signerAddress }); return await this.contract.methods.add(address).send({ from: this.signerAddress });
} else { }
return await this.haveAccount(address); return true;
}
} }
} }

View File

@ -1,3 +1,2 @@
export * from '@app/_eth/accountIndex'; export * from '@app/_eth/accountIndex';
export * from '@app/_eth/registry';
export * from '@app/_eth/token-registry'; export * from '@app/_eth/token-registry';

View File

@ -1,8 +0,0 @@
import { Registry } from '@app/_eth/registry';
import {environment} from '@src/environments/environment';
describe('Registry', () => {
it('should create an instance', () => {
expect(new Registry(environment.registryAddress)).toBeTruthy();
});
});

View File

@ -1,32 +0,0 @@
// @ts-ignore
import * as registry from '@src/assets/js/block-sync/data/Registry.json';
import {environment} from '@src/environments/environment';
const Web3 = require('web3');
const web3 = new Web3(environment.web3Provider);
const abi = registry.default;
export class Registry {
contractAddress: string;
signerAddress: string;
contract: any;
constructor(contractAddress: string, signerAddress?: string) {
this.contractAddress = contractAddress;
this.contract = new web3.eth.Contract(abi, contractAddress);
if (signerAddress) {
this.signerAddress = signerAddress;
} else {
this.signerAddress = web3.eth.accounts[0];
}
}
public async owner(): Promise<string> {
return await this.contract.methods.owner().call();
}
public async addressOf(identifier: string): Promise<string> {
const id = '0x' + web3.utils.padRight(new Buffer(identifier).toString('hex'), 64);
return await this.contract.methods.addressOf(id).call();
}
}

View File

@ -1,10 +1,8 @@
// @ts-ignore
import * as registryClient from '@src/assets/js/block-sync/data/RegistryClient.json';
import Web3 from 'web3'; import Web3 from 'web3';
import { environment } from '@src/environments/environment'; import { environment } from '@src/environments/environment';
const web3 = new Web3(environment.web3Provider); const abi: Array<any> = require('@src/assets/js/block-sync/data/TokenUniqueSymbolIndex.json');
const abi = registryClient.default; const web3: Web3 = new Web3(environment.web3Provider);
export class TokenRegistry { export class TokenRegistry {
contractAddress: string; contractAddress: string;
@ -22,7 +20,7 @@ export class TokenRegistry {
} }
public async totalTokens(): Promise<number> { public async totalTokens(): Promise<number> {
return await this.contract.methods.registryCount().call(); return await this.contract.methods.entryCount().call();
} }
public async entry(serial: number): Promise<string> { public async entry(serial: number): Promise<string> {
@ -30,7 +28,7 @@ export class TokenRegistry {
} }
public async addressOf(identifier: string): Promise<string> { public async addressOf(identifier: string): Promise<string> {
const id = '0x' + web3.utils.padRight(new Buffer(identifier).toString('hex'), 64); const id: string = web3.eth.abi.encodeParameter('bytes32', web3.utils.toHex(identifier));
return await this.contract.methods.addressOf(id).call(); return await this.contract.methods.addressOf(id).call();
} }
} }

View File

@ -1,23 +1,27 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router} from '@angular/router'; import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
UrlTree,
Router,
} from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
constructor(private router: Router) {} constructor(private router: Router) {}
canActivate( canActivate(
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { state: RouterStateSnapshot
if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) { ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) {
return true; return true;
} }
this.router.navigate(['/auth']); this.router.navigate(['/auth']);
return false; return false;
} }
} }

View File

@ -1,17 +1,23 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router} from '@angular/router'; import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
UrlTree,
Router,
} from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class RoleGuard implements CanActivate { export class RoleGuard implements CanActivate {
constructor(private router: Router) {} constructor(private router: Router) {}
canActivate( canActivate(
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const currentUser = JSON.parse(localStorage.getItem(atob('CICADA_USER'))); const currentUser = JSON.parse(localStorage.getItem(atob('CICADA_USER')));
if (currentUser) { if (currentUser) {
if (route.data.roles && route.data.roles.indexOf(currentUser.role) === -1) { if (route.data.roles && route.data.roles.indexOf(currentUser.role) === -1) {
@ -24,5 +30,4 @@ export class RoleGuard implements CanActivate {
this.router.navigate(['/auth'], { queryParams: { returnUrl: state.url } }); this.router.navigate(['/auth'], { queryParams: { returnUrl: state.url } });
return false; return false;
} }
} }

View File

@ -1,5 +1,5 @@
export class ArraySum { function arraySum(arr: Array<number>): number {
static arraySum(arr: any[]): number {
return arr.reduce((accumulator, current) => accumulator + current, 0); return arr.reduce((accumulator, current) => accumulator + current, 0);
} }
}
export { arraySum };

View File

@ -0,0 +1,51 @@
function copyToClipboard(text: any): boolean {
// create our hidden div element
const hiddenCopy: HTMLDivElement = document.createElement('div');
// set the innerHTML of the div
hiddenCopy.innerHTML = text;
// set the position to be absolute and off the screen
hiddenCopy.classList.add('clipboard');
// check and see if the user had a text selection range
let currentRange: Range | boolean;
if (document.getSelection().rangeCount > 0) {
// the user has a text selection range, store it
currentRange = document.getSelection().getRangeAt(0);
// remove the current selection
window.getSelection().removeRange(currentRange);
} else {
// they didn't have anything selected
currentRange = false;
}
// append the div to the body
document.body.appendChild(hiddenCopy);
// create a selection range
const copyRange: Range = document.createRange();
// set the copy range to be the hidden div
copyRange.selectNode(hiddenCopy);
// add the copy range
window.getSelection().addRange(copyRange);
// since not all browsers support this, use a try block
try {
// copy the text
document.execCommand('copy');
} catch (err) {
window.alert('Your Browser Does not support this! Error : ' + err);
return false;
}
// remove the selection range (Chrome throws a warning if we don't.)
window.getSelection().removeRange(copyRange);
// remove the hidden div
document.body.removeChild(hiddenCopy);
// return the old selection range
if (currentRange) {
window.getSelection().addRange(currentRange);
}
return true;
}
export { copyToClipboard };

View File

@ -3,7 +3,7 @@ import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
export class CustomErrorStateMatcher implements ErrorStateMatcher { export class CustomErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted; const isSubmitted: boolean = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted)); return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
} }
} }

View File

@ -15,7 +15,7 @@ export class CustomValidator {
return null; return null;
} }
const valid = regex.test(control.value); const valid: boolean = regex.test(control.value);
return valid ? null : error; return valid ? null : error;
}; };
} }

View File

@ -1,9 +1,11 @@
function exportCsv(arrayData: any[], filename: string, delimiter = ','): void { function exportCsv(arrayData: Array<any>, filename: string, delimiter: string = ','): void {
if (arrayData === undefined) { return; } if (arrayData === undefined || arrayData.length === 0) {
const header = Object.keys(arrayData[0]).join(delimiter) + '\n'; alert('No data to be exported!');
let csv = header; return;
arrayData.forEach(obj => { }
let row = []; let csv: string = Object.keys(arrayData[0]).join(delimiter) + '\n';
arrayData.forEach((obj) => {
const row: Array<any> = [];
for (const key in obj) { for (const key in obj) {
if (obj.hasOwnProperty(key)) { if (obj.hasOwnProperty(key)) {
row.push(obj[key]); row.push(obj[key]);
@ -12,11 +14,10 @@ function exportCsv(arrayData: any[], filename: string, delimiter = ','): void {
csv += row.join(delimiter) + '\n'; csv += row.join(delimiter) + '\n';
}); });
const csvData = new Blob([csv], {type: 'text/csv'}); const csvData: Blob = new Blob([csv], { type: 'text/csv' });
const csvUrl = URL.createObjectURL(csvData); const csvUrl: string = URL.createObjectURL(csvData);
// csvUrl = 'data:text/csv;charset=utf-8,' + encodeURI(csv);
let downloadLink = document.createElement('a'); const downloadLink: HTMLAnchorElement = document.createElement('a');
downloadLink.href = csvUrl; downloadLink.href = csvUrl;
downloadLink.target = '_blank'; downloadLink.target = '_blank';
downloadLink.download = filename + '.csv'; downloadLink.download = filename + '.csv';
@ -32,6 +33,4 @@ function removeSpecialChar(str: string): string {
return str.replace(/[^a-zA-Z0-9 ]/g, ''); return str.replace(/[^a-zA-Z0-9 ]/g, '');
} }
export { export { exportCsv };
exportCsv
};

View File

@ -3,28 +3,35 @@ import {LoggingService} from '@app/_services/logging.service';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
// A generalized http response error
export class HttpError extends Error {
public status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
this.name = 'HttpError';
}
}
@Injectable() @Injectable()
export class GlobalErrorHandler extends ErrorHandler { export class GlobalErrorHandler extends ErrorHandler {
private sentencesForWarningLogging: string[] = []; private sentencesForWarningLogging: Array<string> = [];
constructor( constructor(private loggingService: LoggingService, private router: Router) {
private loggingService: LoggingService,
private router: Router
) {
super(); super();
} }
handleError(error: any): void { handleError(error: Error): void {
this.logError(error); this.logError(error);
const message = error.message ? error.message : error.toString(); const message: string = error.message ? error.message : error.toString();
if (error.status) { // if (error.status) {
error = new Error(message); // error = new Error(message);
} // }
const errorTraceString = `Error message:\n${message}.\nStack trace: ${error.stack}`; const errorTraceString: string = `Error message:\n${message}.\nStack trace: ${error.stack}`;
const isWarning = this.isWarning(errorTraceString); const isWarning: boolean = this.isWarning(errorTraceString);
if (isWarning) { if (isWarning) {
this.loggingService.sendWarnLevelMessage(errorTraceString, { error }); this.loggingService.sendWarnLevelMessage(errorTraceString, { error });
} else { } else {
@ -35,27 +42,43 @@ export class GlobalErrorHandler extends ErrorHandler {
} }
logError(error: any): void { logError(error: any): void {
const route = this.router.url; const route: string = this.router.url;
if (error instanceof HttpErrorResponse) { if (error instanceof HttpErrorResponse) {
this.loggingService.sendErrorLevelMessage( this.loggingService.sendErrorLevelMessage(
`There was an HTTP error on route ${route}.\n${error.message}.\nStatus code: ${(error as HttpErrorResponse).status}`, `There was an HTTP error on route ${route}.\n${error.message}.\nStatus code: ${
this, {error}); (error as HttpErrorResponse).status
}`,
this,
{ error }
);
} else if (error instanceof TypeError) { } else if (error instanceof TypeError) {
this.loggingService.sendErrorLevelMessage(`There was a Type error on route ${route}.\n${error.message}`, this, {error}); this.loggingService.sendErrorLevelMessage(
`There was a Type error on route ${route}.\n${error.message}`,
this,
{ error }
);
} else if (error instanceof Error) { } else if (error instanceof Error) {
this.loggingService.sendErrorLevelMessage(`There was a general error on route ${route}.\n${error.message}`, this, {error}); this.loggingService.sendErrorLevelMessage(
`There was a general error on route ${route}.\n${error.message}`,
this,
{ error }
);
} else { } else {
this.loggingService.sendErrorLevelMessage(`Nobody threw an error but something happened on route ${route}!`, this, {error}); this.loggingService.sendErrorLevelMessage(
`Nobody threw an error but something happened on route ${route}!`,
this,
{ error }
);
} }
} }
private isWarning(errorTraceString: string): boolean { private isWarning(errorTraceString: string): boolean {
let isWarning = true; let isWarning: boolean = true;
if (errorTraceString.includes('/src/app/')) { if (errorTraceString.includes('/src/app/')) {
isWarning = false; isWarning = false;
} }
this.sentencesForWarningLogging.forEach((whiteListSentence) => { this.sentencesForWarningLogging.forEach((whiteListSentence: string) => {
if (errorTraceString.includes(whiteListSentence)) { if (errorTraceString.includes(whiteListSentence)) {
isWarning = true; isWarning = true;
} }
@ -64,3 +87,10 @@ export class GlobalErrorHandler extends ErrorHandler {
return isWarning; return isWarning;
} }
} }
export function rejectBody(error): { status: any; statusText: any } {
return {
status: error.status,
statusText: error.statusText,
};
}

View File

@ -1,16 +1,17 @@
import { rejectBody } from '@app/_helpers/global-error-handler';
function HttpGetter(): void {} function HttpGetter(): void {}
HttpGetter.prototype.get = filename => new Promise((resolve, reject) => { HttpGetter.prototype.get = (filename) =>
fetch(filename).then(response => { new Promise((resolve, reject) => {
fetch(filename).then((response) => {
if (response.ok) { if (response.ok) {
resolve(response.json()); resolve(response.text());
} else { } else {
reject(`failed with status ${response.status} : ${response.statusText}`); reject(rejectBody(response));
} }
return; return;
}); });
}); });
export { export { HttpGetter };
HttpGetter
};

View File

@ -6,3 +6,5 @@ export * from '@app/_helpers/http-getter';
export * from '@app/_helpers/global-error-handler'; export * from '@app/_helpers/global-error-handler';
export * from '@app/_helpers/export-csv'; export * from '@app/_helpers/export-csv';
export * from '@app/_helpers/read-csv'; export * from '@app/_helpers/read-csv';
export * from '@app/_helpers/clipboard-copy';
export * from '@app/_helpers/schema-validation';

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,28 @@
let objCsv = { const objCsv: { size: number; dataFile: any } = {
size: 0, size: 0,
dataFile: [] dataFile: [],
}; };
function readCsv(input: any): any { function readCsv(input: any): Array<any> | void {
if (input.files && input.files[0]) { if (input.files && input.files[0]) {
let reader = new FileReader(); const reader: FileReader = new FileReader();
reader.readAsBinaryString(input.files[0]); reader.readAsBinaryString(input.files[0]);
reader.onload = event => { reader.onload = (event) => {
objCsv.size = event.total; objCsv.size = event.total;
// @ts-ignore
objCsv.dataFile = event.target.result; objCsv.dataFile = event.target.result;
return parseData(objCsv.dataFile); return parseData(objCsv.dataFile);
}; };
} }
} }
function parseData(data: any): any { function parseData(data: any): Array<any> {
let csvData = []; const csvData: Array<any> = [];
const lineBreak = data.split('\n'); const lineBreak: Array<any> = data.split('\n');
lineBreak.forEach(res => { lineBreak.forEach((res) => {
csvData.push(res.split(',')); csvData.push(res.split(','));
}); });
console.table(csvData); console.table(csvData);
return csvData; return csvData;
} }
export { export { readCsv };
readCsv
};

View File

@ -0,0 +1,19 @@
import { validatePerson, validateVcard } from '@cicnet/schemas-data-validator';
async function personValidation(person: any): Promise<void> {
const personValidationErrors: any = await validatePerson(person);
if (personValidationErrors) {
personValidationErrors.map((error) => console.error(`${error.message}`));
}
}
async function vcardValidation(vcard: any): Promise<void> {
const vcardValidationErrors: any = await validateVcard(vcard);
if (vcardValidationErrors) {
vcardValidationErrors.map((error) => console.error(`${error.message}`));
}
}
export { personValidation, vcardValidation };

View File

@ -3,11 +3,11 @@ import { TestBed } from '@angular/core/testing';
import { ErrorInterceptor } from '@app/_interceptors/error.interceptor'; import { ErrorInterceptor } from '@app/_interceptors/error.interceptor';
describe('ErrorInterceptor', () => { describe('ErrorInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({ beforeEach(() =>
providers: [ TestBed.configureTestingModule({
ErrorInterceptor providers: [ErrorInterceptor],
] })
})); );
it('should be created', () => { it('should be created', () => {
const interceptor: ErrorInterceptor = TestBed.inject(ErrorInterceptor); const interceptor: ErrorInterceptor = TestBed.inject(ErrorInterceptor);

View File

@ -1,18 +1,18 @@
import {Injectable, isDevMode} from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
HttpRequest, HttpRequest,
HttpHandler, HttpHandler,
HttpEvent, HttpEvent,
HttpInterceptor, HttpErrorResponse HttpInterceptor,
HttpErrorResponse,
} from '@angular/common/http'; } from '@angular/common/http';
import { Observable, throwError } from 'rxjs'; import { Observable, throwError } from 'rxjs';
import {catchError, retry} from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { ErrorDialogService, LoggingService } from '@app/_services'; import { ErrorDialogService, LoggingService } from '@app/_services';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@Injectable() @Injectable()
export class ErrorInterceptor implements HttpInterceptor { export class ErrorInterceptor implements HttpInterceptor {
constructor( constructor(
private errorDialogService: ErrorDialogService, private errorDialogService: ErrorDialogService,
private loggingService: LoggingService, private loggingService: LoggingService,
@ -22,14 +22,16 @@ export class ErrorInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe( return next.handle(request).pipe(
catchError((err: HttpErrorResponse) => { catchError((err: HttpErrorResponse) => {
let errorMessage; let errorMessage: string;
if (err.error instanceof ErrorEvent) { if (err.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly. // A client-side or network error occurred. Handle it accordingly.
errorMessage = `An error occurred: ${err.error.message}`; errorMessage = `An error occurred: ${err.error.message}`;
} else { } else {
// The backend returned an unsuccessful response code. // The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong. // The response body may contain clues as to what went wrong.
errorMessage = `Backend returned code ${err.status}, body was: ${JSON.stringify(err.error)}`; errorMessage = `Backend returned code ${err.status}, body was: ${JSON.stringify(
err.error
)}`;
} }
this.loggingService.sendErrorLevelMessage(errorMessage, this, { error: err }); this.loggingService.sendErrorLevelMessage(errorMessage, this, { error: err });
switch (err.status) { switch (err.status) {
@ -37,7 +39,7 @@ export class ErrorInterceptor implements HttpInterceptor {
this.router.navigateByUrl('/auth').then(); this.router.navigateByUrl('/auth').then();
break; break;
case 403: // forbidden case 403: // forbidden
location.reload(true); alert('Access to resource is not allowed!');
break; break;
} }
// Return an observable with a user-facing error message. // Return an observable with a user-facing error message.

View File

@ -3,11 +3,11 @@ import { TestBed } from '@angular/core/testing';
import { HttpConfigInterceptor } from './http-config.interceptor'; import { HttpConfigInterceptor } from './http-config.interceptor';
describe('HttpConfigInterceptor', () => { describe('HttpConfigInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({ beforeEach(() =>
providers: [ TestBed.configureTestingModule({
HttpConfigInterceptor providers: [HttpConfigInterceptor],
] })
})); );
it('should be created', () => { it('should be created', () => {
const interceptor: HttpConfigInterceptor = TestBed.inject(HttpConfigInterceptor); const interceptor: HttpConfigInterceptor = TestBed.inject(HttpConfigInterceptor);

View File

@ -1,23 +1,17 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@Injectable() @Injectable()
export class HttpConfigInterceptor implements HttpInterceptor { export class HttpConfigInterceptor implements HttpInterceptor {
constructor() {} constructor() {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const token = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); // const token: string = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
if (token) { // if (token) {
request = request.clone({headers: request.headers.set('Authorization', 'Bearer ' + token)}); // request = request.clone({headers: request.headers.set('Authorization', 'Bearer ' + token)});
} // }
return next.handle(request); return next.handle(request);
} }

View File

@ -3,11 +3,11 @@ import { TestBed } from '@angular/core/testing';
import { LoggingInterceptor } from './logging.interceptor'; import { LoggingInterceptor } from './logging.interceptor';
describe('LoggingInterceptor', () => { describe('LoggingInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({ beforeEach(() =>
providers: [ TestBed.configureTestingModule({
LoggingInterceptor providers: [LoggingInterceptor],
] })
})); );
it('should be created', () => { it('should be created', () => {
const interceptor: LoggingInterceptor = TestBed.inject(LoggingInterceptor); const interceptor: LoggingInterceptor = TestBed.inject(LoggingInterceptor);

View File

@ -4,7 +4,7 @@ import {
HttpHandler, HttpHandler,
HttpEvent, HttpEvent,
HttpInterceptor, HttpInterceptor,
HttpResponse HttpResponse,
} from '@angular/common/http'; } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { LoggingService } from '@app/_services/logging.service'; import { LoggingService } from '@app/_services/logging.service';
@ -12,26 +12,24 @@ import {finalize, tap} from 'rxjs/operators';
@Injectable() @Injectable()
export class LoggingInterceptor implements HttpInterceptor { export class LoggingInterceptor implements HttpInterceptor {
constructor(private loggingService: LoggingService) {}
constructor(
private loggingService: LoggingService
) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
this.loggingService.sendInfoLevelMessage(request); return next.handle(request);
const startTime = Date.now(); // this.loggingService.sendInfoLevelMessage(request);
let status: string; // const startTime: number = Date.now();
// let status: string;
return next.handle(request).pipe(tap(event => { //
status = ''; // return next.handle(request).pipe(tap(event => {
if (event instanceof HttpResponse) { // status = '';
status = 'succeeded'; // if (event instanceof HttpResponse) {
} // status = 'succeeded';
}, error => status = 'failed'), // }
finalize(() => { // }, error => status = 'failed'),
const elapsedTime = Date.now() - startTime; // finalize(() => {
const message = `${request.method} request for ${request.urlWithParams} ${status} in ${elapsedTime} ms`; // const elapsedTime: number = Date.now() - startTime;
this.loggingService.sendInfoLevelMessage(message); // const message: string = `${request.method} request for ${request.urlWithParams} ${status} in ${elapsedTime} ms`;
})); // this.loggingService.sendInfoLevelMessage(message);
// }));
} }
} }

View File

@ -1,8 +1,9 @@
export interface AccountDetails { interface AccountDetails {
date_registered: number; date_registered: number;
gender: string; gender: string;
age?: string; age?: string;
type?: string; type?: string;
balance?: number;
identities: { identities: {
evm: { evm: {
'bloxberg:8996': string[]; 'bloxberg:8996': string[];
@ -19,46 +20,56 @@ export interface AccountDetails {
products: string[]; products: string[];
category?: string; category?: string;
vcard: { vcard: {
email: [{ email: [
{
value: string; value: string;
}]; }
fn: [{ ];
fn: [
{
value: string; value: string;
}]; }
n: [{ ];
n: [
{
value: string[]; value: string[];
}]; }
tel: [{ ];
tel: [
{
meta: { meta: {
TYP: string[]; TYP: string[];
}, };
value: string; value: string;
}], }
version: [{ ];
version: [
{
value: string; value: string;
}]; }
];
}; };
} }
export interface Signature { interface Signature {
algo: string; algo: string;
data: string; data: string;
digest: string; digest: string;
engine: string; engine: string;
} }
export interface Meta { interface Meta {
data: AccountDetails; data: AccountDetails;
id: string; id: string;
signature: Signature; signature: Signature;
} }
export interface MetaResponse { interface MetaResponse {
id: string; id: string;
m: Meta; m: Meta;
} }
export const defaultAccount: AccountDetails = { const defaultAccount: AccountDetails = {
date_registered: Date.now(), date_registered: Date.now(),
gender: 'other', gender: 'other',
identities: { identities: {
@ -74,23 +85,35 @@ export const defaultAccount: AccountDetails = {
}, },
products: [], products: [],
vcard: { vcard: {
email: [{ email: [
{
value: '', value: '',
}], },
fn: [{ ],
fn: [
{
value: 'Sarafu Contract', value: 'Sarafu Contract',
}], },
n: [{ ],
n: [
{
value: ['Sarafu', 'Contract'], value: ['Sarafu', 'Contract'],
}], },
tel: [{ ],
tel: [
{
meta: { meta: {
TYP: [], TYP: [],
}, },
value: '', value: '',
}], },
version: [{ ],
version: [
{
value: '3.0', value: '3.0',
}], },
],
}, },
}; };
export { AccountDetails, Signature, Meta, MetaResponse, defaultAccount };

View File

@ -1,4 +1,6 @@
export * from '@app/_models/transaction'; export * from '@app/_models/transaction';
export * from '@app/_models/settings'; export * from '@app/_models/settings';
export * from '@app/_models/user';
export * from '@app/_models/account'; export * from '@app/_models/account';
export * from '@app/_models/staff';
export * from '@app/_models/token';
export * from '@app/_models/mappings';

View File

@ -0,0 +1,24 @@
interface Action {
id: number;
user: string;
role: string;
action: string;
approval: boolean;
}
interface Category {
name: string;
products: Array<string>;
}
interface AreaName {
name: string;
locations: Array<string>;
}
interface AreaType {
name: string;
area: Array<string>;
}
export { Action, Category, AreaName, AreaType };

View File

@ -1,4 +1,4 @@
export class Settings { class Settings {
w3: W3 = { w3: W3 = {
engine: undefined, engine: undefined,
provider: undefined, provider: undefined,
@ -12,7 +12,9 @@ export class Settings {
} }
} }
export class W3 { class W3 {
engine: any; engine: any;
provider: any; provider: any;
} }
export { Settings, W3 };

View File

@ -1,7 +1,9 @@
export interface Staff { interface Staff {
comment: string; comment: string;
email: string; email: string;
name: string; name: string;
tag: number; tag: number;
userid: string; userid: string;
} }
export { Staff };

View File

@ -1,4 +1,4 @@
export interface Token { interface Token {
name: string; name: string;
symbol: string; symbol: string;
address: string; address: string;
@ -8,8 +8,10 @@ export interface Token {
'0xa686005CE37Dce7738436256982C3903f2E4ea8E'?: { '0xa686005CE37Dce7738436256982C3903f2E4ea8E'?: {
weight: string; weight: string;
balance: string; balance: string;
} };
}; };
reserveRatio?: string; reserveRatio?: string;
owner?: string; owner?: string;
} }
export { Token };

View File

@ -1,6 +1,6 @@
import {User} from '@app/_models/user'; import { AccountDetails } from '@app/_models/account';
export class BlocksBloom { class BlocksBloom {
low: number; low: number;
blockFilter: string; blockFilter: string;
blocktxFilter: string; blocktxFilter: string;
@ -8,13 +8,13 @@ export class BlocksBloom {
filterRounds: number; filterRounds: number;
} }
export class Token { class TxToken {
address: string; address: string;
name: string; name: string;
symbol: string; symbol: string;
} }
export class Tx { class Tx {
block: number; block: number;
success: boolean; success: boolean;
timestamp: number; timestamp: number;
@ -22,22 +22,25 @@ export class Tx {
txIndex: number; txIndex: number;
} }
export class Transaction { class Transaction {
from: string; from: string;
sender: User; sender: AccountDetails;
to: string; to: string;
recipient: User; recipient: AccountDetails;
token: Token; token: TxToken;
tx: Tx; tx: Tx;
value: number; value: number;
type?: string;
} }
export class Conversion { class Conversion {
destinationToken: Token; destinationToken: TxToken;
fromValue: number; fromValue: number;
sourceToken: Token; sourceToken: TxToken;
toValue: number; toValue: number;
trader: string; trader: string;
user: User; user: AccountDetails;
tx: Tx; tx: Tx;
} }
export { BlocksBloom, TxToken, Tx, Transaction, Conversion };

View File

@ -1,22 +0,0 @@
export class User {
dateRegistered: number;
vcard: {
fn: string;
version: string;
tel: [{
meta: {
TYP: string;
};
value: string[];
}];
};
key: {
ethereum: string[];
};
location: {
latitude: string;
longitude: string;
external: {};
};
selling: string[];
}

View File

@ -1,5 +1,5 @@
import { KeyStore } from 'cic-client-meta'; import { KeyStore } from 'cic-client-meta';
// TODO should we put this on the mutalble key store object // TODO should we put this on the mutable key store object
import * as openpgp from 'openpgp'; import * as openpgp from 'openpgp';
const keyring = new openpgp.Keyring(); const keyring = new openpgp.Keyring();
@ -32,7 +32,6 @@ interface MutableKeyStore extends KeyStore {
} }
class MutablePgpKeyStore implements MutableKeyStore { class MutablePgpKeyStore implements MutableKeyStore {
async loadKeyring(): Promise<void> { async loadKeyring(): Promise<void> {
await keyring.load(); await keyring.load();
await keyring.store(); await keyring.store();
@ -76,15 +75,14 @@ class MutablePgpKeyStore implements MutableKeyStore{
} }
async isValidKey(key): Promise<boolean> { async isValidKey(key): Promise<boolean> {
// There is supposed to be an opengpg.readKey() method but I can't find it? // There is supposed to be an openpgp.readKey() method but I can't find it?
const _key = await openpgp.key.readArmored(key); const testKey = await openpgp.key.readArmored(key);
return !_key.err; return !testKey.err;
} }
async isEncryptedPrivateKey(privateKey: any): Promise<boolean> { async isEncryptedPrivateKey(privateKey: any): Promise<boolean> {
const imported = await openpgp.key.readArmored(privateKey); const imported = await openpgp.key.readArmored(privateKey);
for (let i = 0; i < imported.keys.length; i++) { for (const key of imported.keys) {
const key = imported.keys[i];
if (key.isDecrypted()) { if (key.isDecrypted()) {
return false; return false;
} }
@ -94,8 +92,12 @@ class MutablePgpKeyStore implements MutableKeyStore{
getFingerprint(): string { getFingerprint(): string {
// TODO Handle multiple keys // TODO Handle multiple keys
return keyring.privateKeys && keyring.privateKeys.keys[0] && keyring.privateKeys.keys[0].keyPacket && return (
keyring.privateKeys.keys[0].keyPacket.fingerprint; keyring.privateKeys &&
keyring.privateKeys.keys[0] &&
keyring.privateKeys.keys[0].keyPacket &&
keyring.privateKeys.keys[0].keyPacket.fingerprint
);
} }
getKeyId(key: any): string { getKeyId(key: any): string {
@ -104,7 +106,11 @@ class MutablePgpKeyStore implements MutableKeyStore{
getPrivateKeyId(): string { getPrivateKeyId(): string {
// TODO is there a library that comes with angular for doing this? // TODO is there a library that comes with angular for doing this?
return keyring.privateKeys && keyring.privateKeys.keys[0] && keyring.privateKeys.keys[0].getKeyId().toHex(); return (
keyring.privateKeys &&
keyring.privateKeys.keys[0] &&
keyring.privateKeys.keys[0].getKeyId().toHex()
);
} }
getKeysForId(keyId: string): Array<any> { getKeysForId(keyId: string): Array<any> {
@ -159,7 +165,4 @@ class MutablePgpKeyStore implements MutableKeyStore{
} }
} }
export { export { MutablePgpKeyStore, MutableKeyStore };
MutablePgpKeyStore,
MutableKeyStore
};

View File

@ -7,12 +7,12 @@ interface Signable {
digest(): string; digest(): string;
} }
type Signature = { interface Signature {
engine: string engine: string;
algo: string algo: string;
data: string data: string;
digest: string; digest: string;
}; }
interface Signer { interface Signer {
onsign(signature: Signature): void; onsign(signature: Signature): void;
@ -24,7 +24,6 @@ interface Signer {
} }
class PGPSigner implements Signer { class PGPSigner implements Signer {
engine = 'pgp'; engine = 'pgp';
algo = 'sha256'; algo = 'sha256';
dgst: string; dgst: string;
@ -50,7 +49,9 @@ class PGPSigner implements Signer {
} }
public verify(digest: string, signature: Signature): void { public verify(digest: string, signature: Signature): void {
openpgp.signature.readArmored(signature.data).then((sig) => { openpgp.signature
.readArmored(signature.data)
.then((sig) => {
const opts = { const opts = {
message: openpgp.cleartext.fromText(digest), message: openpgp.cleartext.fromText(digest),
publicKeys: this.keyStore.getTrustedKeys(), publicKeys: this.keyStore.getTrustedKeys(),
@ -65,10 +66,15 @@ class PGPSigner implements Signer {
return; return;
} }
} }
this.loggingService.sendErrorLevelMessage(`Checked ${i} signature(s) but none valid`, this, {error: '404 Not found!'}); this.loggingService.sendErrorLevelMessage(
`Checked ${i} signature(s) but none valid`,
this,
{ error: '404 Not found!' }
);
this.onverify(false); this.onverify(false);
}); });
}).catch((e) => { })
.catch((e) => {
this.loggingService.sendErrorLevelMessage(e.message, this, { error: e }); this.loggingService.sendErrorLevelMessage(e.message, this, { error: e });
this.onverify(false); this.onverify(false);
}); });
@ -86,7 +92,9 @@ class PGPSigner implements Signer {
privateKeys: [pk], privateKeys: [pk],
detached: true, detached: true,
}; };
openpgp.sign(opts).then((s) => { openpgp
.sign(opts)
.then((s) => {
this.signature = { this.signature = {
engine: this.engine, engine: this.engine,
algo: this.algo, algo: this.algo,
@ -95,16 +103,12 @@ class PGPSigner implements Signer {
digest, digest,
}; };
this.onsign(this.signature); this.onsign(this.signature);
}).catch((e) => { })
.catch((e) => {
this.loggingService.sendErrorLevelMessage(e.message, this, { error: e }); this.loggingService.sendErrorLevelMessage(e.message, this, { error: e });
this.onsign(undefined); this.onsign(undefined);
}); });
} }
} }
export { export { Signable, Signature, Signer, PGPSigner };
Signable,
Signature,
Signer,
PGPSigner
};

View File

@ -6,27 +6,33 @@ import {LoggingService} from '@app/_services/logging.service';
import { MutableKeyStore, MutablePgpKeyStore } from '@app/_pgp'; import { MutableKeyStore, MutablePgpKeyStore } from '@app/_pgp';
import { ErrorDialogService } from '@app/_services/error-dialog.service'; import { ErrorDialogService } from '@app/_services/error-dialog.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import {Observable } from 'rxjs'; import { HttpError, rejectBody } from '@app/_helpers/global-error-handler';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class AuthService { export class AuthService {
sessionToken: any; sessionToken: any;
privateKey: any; privateKey: any;
mutableKeyStore: MutableKeyStore = new MutablePgpKeyStore(); mutableKeyStore: MutableKeyStore;
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
private loggingService: LoggingService, private loggingService: LoggingService,
private errorDialogService: ErrorDialogService private errorDialogService: ErrorDialogService
) { ) {
// TODO setting these together shoulds be atomic this.mutableKeyStore = new MutablePgpKeyStore();
}
async init(): Promise<void> {
await this.mutableKeyStore.loadKeyring();
// TODO setting these together should be atomic
if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) { if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) {
this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
} }
if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) { if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) {
this.privateKey = localStorage.getItem(btoa('CICADA_PRIVATE_KEY')); this.privateKey = localStorage.getItem(btoa('CICADA_PRIVATE_KEY'));
await this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY')));
} }
} }
@ -34,62 +40,60 @@ export class AuthService {
document.getElementById('state').innerHTML = s; document.getElementById('state').innerHTML = s;
} }
getWithToken(): void { getWithToken(): Promise<boolean> {
return new Promise((resolve, reject) => {
const headers = { const headers = {
Authorization: 'Bearer ' + this.sessionToken, Authorization: 'Bearer ' + this.sessionToken,
'Content-Type': 'application/json;charset=utf-8', 'Content-Type': 'application/json;charset=utf-8',
'x-cic-automerge': 'none' 'x-cic-automerge': 'none',
}; };
const options = { const options = {
headers, headers,
}; };
fetch(environment.cicMetaUrl, options).then(response => { fetch(environment.cicMetaUrl, options).then((response) => {
if (response.status === 401) { if (response.status === 401) {
return Promise.reject({ return reject(rejectBody(response));
status: response.status,
statusText: response.statusText
});
} }
return; return resolve(true);
});
}); });
} }
sendResponse(hobaResponseEncoded): void { // TODO rename to send signed challenge and set session. Also separate these responsibilities
sendResponse(hobaResponseEncoded: any): Promise<boolean> {
return new Promise((resolve, reject) => {
const headers = { const headers = {
Authorization: 'HOBA ' + hobaResponseEncoded, Authorization: 'HOBA ' + hobaResponseEncoded,
'Content-Type': 'application/json;charset=utf-8', 'Content-Type': 'application/json;charset=utf-8',
'x-cic-automerge': 'none' 'x-cic-automerge': 'none',
}; };
const options = { const options = {
headers, headers,
}; };
fetch(environment.cicMetaUrl, options).then(response => { fetch(environment.cicMetaUrl, options).then((response) => {
if (response.status === 401) { if (response.status === 401) {
return Promise.reject({ return reject(rejectBody(response));
status: response.status,
statusText: response.statusText
});
} }
this.sessionToken = response.headers.get('Token'); this.sessionToken = response.headers.get('Token');
sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken); sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken);
this.setState('Click button to log in'); this.setState('Click button to log in');
return; return resolve(true);
});
}); });
} }
async getChallenge(): Promise<any> { getChallenge(): Promise<any> {
return fetch(environment.cicMetaUrl).then(async response => { return new Promise((resolve, reject) => {
fetch(environment.cicMetaUrl).then(async (response) => {
if (response.status === 401) { if (response.status === 401) {
const authHeader = response.headers.get('WWW-Authenticate'); const authHeader: string = response.headers.get('WWW-Authenticate');
return hobaParseChallengeHeader(authHeader); return resolve(hobaParseChallengeHeader(authHeader));
} }
if (!response.ok) { if (!response.ok) {
return Promise.reject({ return reject(rejectBody(response));
status: response.status,
statusText: response.statusText
});
} }
}); });
});
} }
async passwordLogin(password: string): Promise<boolean> { async passwordLogin(password: string): Promise<boolean> {
@ -104,7 +108,7 @@ export class AuthService {
return false; return false;
} }
login(): boolean { async login(): Promise<boolean> {
if (this.sessionToken !== undefined) { if (this.sessionToken !== undefined) {
try { try {
this.getWithToken(); this.getWithToken();
@ -117,18 +121,39 @@ export class AuthService {
return false; return false;
} }
async loginResponse(o: { challenge: string; realm: any }): Promise<any> {
async loginResponse(o: any, password: string): Promise<any> { return new Promise(async (resolve, reject) => {
try { try {
const r = await signChallenge(o.challenge, o.realm, environment.cicMetaUrl, this.mutableKeyStore, password); const r = await signChallenge(
this.sendResponse(r); o.challenge,
o.realm,
environment.cicMetaUrl,
this.mutableKeyStore
);
const response: boolean = await this.sendResponse(r);
resolve(response);
} catch (error) { } catch (error) {
this.errorDialogService.openDialog({message: 'Incorrect key passphrase.'}); if (error instanceof HttpError) {
return Promise.reject({ if (error.status === 403) {
status: error.status, this.errorDialogService.openDialog({
statusText: error.statusText message: 'You are not authorized to use this system',
});
} else if (error.status === 401) {
this.errorDialogService.openDialog({
message:
'Unable to authenticate with the service. ' +
'Please speak with the staff at Grassroots ' +
'Economics for requesting access ' +
'staff@grassrootseconomics.net.',
}); });
} }
} else {
// TODO define this error
this.errorDialogService.openDialog({ message: 'Incorrect key passphrase.' });
}
resolve(false);
}
});
} }
async setKey(privateKeyArmored): Promise<boolean> { async setKey(privateKeyArmored): Promise<boolean> {
@ -144,7 +169,11 @@ export class AuthService {
const key = await this.mutableKeyStore.importPrivateKey(privateKeyArmored); const key = await this.mutableKeyStore.importPrivateKey(privateKeyArmored);
localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored); localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored);
} catch (err) { } catch (err) {
this.loggingService.sendErrorLevelMessage(`Failed to set key: ${err.message || err.statusText}`, this, {error: err}); this.loggingService.sendErrorLevelMessage(
`Failed to set key: ${err.message || err.statusText}`,
this,
{ error: err }
);
this.errorDialogService.openDialog({ this.errorDialogService.openDialog({
message: `Failed to set key: ${err.message || err.statusText}`, message: `Failed to set key: ${err.message || err.statusText}`,
}); });
@ -155,23 +184,30 @@ export class AuthService {
logout(): void { logout(): void {
sessionStorage.removeItem(btoa('CICADA_SESSION_TOKEN')); sessionStorage.removeItem(btoa('CICADA_SESSION_TOKEN'));
localStorage.removeItem(btoa('CICADA_PRIVATE_KEY'));
this.sessionToken = undefined; this.sessionToken = undefined;
window.location.reload(true); window.location.reload();
} }
getTrustedUsers(): any { getTrustedUsers(): any {
let trustedUsers = []; const trustedUsers: Array<any> = [];
this.mutableKeyStore.getPublicKeys().forEach(key => trustedUsers.push(key.users[0].userId)); this.mutableKeyStore.getPublicKeys().forEach((key) => trustedUsers.push(key.users[0].userId));
return trustedUsers; return trustedUsers;
} }
getPublicKeys(): Observable<any> { async getPublicKeys(): Promise<any> {
return this.httpClient.get(`${environment.publicKeysUrl}`, {responseType: 'text'}); return new Promise((resolve, reject) => {
fetch(environment.publicKeysUrl).then((res) => {
if (!res.ok) {
// TODO does angular recommend an error interface?
return reject(rejectBody(res));
}
return resolve(res.text());
});
});
} }
async getPrivateKeys(): Promise<void> { getPrivateKey(): any {
if (this.privateKey !== undefined) { return this.mutableKeyStore.getPrivateKey();
await this.mutableKeyStore.importPrivateKey(this.privateKey);
}
} }
} }

View File

@ -9,9 +9,7 @@ describe('BlockSyncService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [{ provide: TransactionService, useClass: TransactionServiceStub }],
{ provide: TransactionService, useClass: TransactionServiceStub }
]
}); });
service = TestBed.inject(BlockSyncService); service = TestBed.inject(BlockSyncService);
}); });

View File

@ -1,36 +1,32 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Settings } from '@app/_models'; import { Settings } from '@app/_models';
import Web3 from 'web3'; import { TransactionHelper } from 'cic-client';
import {CICRegistry, TransactionHelper} from 'cic-client';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { TransactionService } from '@app/_services/transaction.service'; import { TransactionService } from '@app/_services/transaction.service';
import { environment } from '@src/environments/environment'; import { environment } from '@src/environments/environment';
import {HttpGetter} from '@app/_helpers';
import { LoggingService } from '@app/_services/logging.service'; import { LoggingService } from '@app/_services/logging.service';
import { RegistryService } from '@app/_services/registry.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class BlockSyncService { export class BlockSyncService {
readyStateTarget: number = 2; readyStateTarget: number = 2;
readyState: number = 0; readyState: number = 0;
fileGetter = new HttpGetter();
constructor( constructor(
private transactionService: TransactionService, private transactionService: TransactionService,
private loggingService: LoggingService private loggingService: LoggingService,
private registryService: RegistryService
) {} ) {}
blockSync(address: string = null, offset: number = 0, limit: number = 100): any { blockSync(address: string = null, offset: number = 0, limit: number = 100): void {
this.transactionService.resetTransactionsList(); this.transactionService.resetTransactionsList();
const settings = new Settings(this.scan); const settings: Settings = new Settings(this.scan);
const provider = environment.web3Provider; const readyStateElements: { network: number } = { network: 2 };
const readyStateElements = { network: 2 }; settings.w3.provider = environment.web3Provider;
settings.w3.provider = provider; settings.w3.engine = this.registryService.getWeb3();
settings.w3.engine = new Web3(provider); settings.registry = this.registryService.getRegistry();
settings.registry = new CICRegistry(settings.w3.engine, environment.registryAddress, this.fileGetter,
['../../assets/js/block-sync/data']);
settings.registry.declaratorHelper.addTrust(environment.trustedDeclaratorAddress);
settings.txHelper = new TransactionHelper(settings.w3.engine, settings.registry); settings.txHelper = new TransactionHelper(settings.w3.engine, settings.registry);
settings.txHelper.ontransfer = async (transaction: any): Promise<void> => { settings.txHelper.ontransfer = async (transaction: any): Promise<void> => {
@ -47,10 +43,17 @@ export class BlockSyncService {
settings.registry.load(); settings.registry.load();
} }
readyStateProcessor(settings: Settings, bit: number, address: string, offset: number, limit: number): void { readyStateProcessor(
settings: Settings,
bit: number,
address: string,
offset: number,
limit: number
): void {
// tslint:disable-next-line:no-bitwise
this.readyState |= bit; this.readyState |= bit;
if (this.readyStateTarget === this.readyState && this.readyStateTarget) { if (this.readyStateTarget === this.readyState && this.readyStateTarget) {
const wHeadSync = new Worker('./../assets/js/block-sync/head.js'); const wHeadSync: Worker = new Worker('./../assets/js/block-sync/head.js');
wHeadSync.onmessage = (m) => { wHeadSync.onmessage = (m) => {
settings.txHelper.processReceipt(m.data); settings.txHelper.processReceipt(m.data);
}; };
@ -58,18 +61,24 @@ export class BlockSyncService {
w3_provider: settings.w3.provider, w3_provider: settings.w3.provider,
}); });
if (address === null) { if (address === null) {
this.transactionService.getAllTransactions(offset, limit).pipe(first()).subscribe(res => { this.transactionService
.getAllTransactions(offset, limit)
.pipe(first())
.subscribe((res) => {
this.fetcher(settings, res); this.fetcher(settings, res);
}); });
} else { } else {
this.transactionService.getAddressTransactions(address, offset, limit).pipe(first()).subscribe(res => { this.transactionService
.getAddressTransactions(address, offset, limit)
.pipe(first())
.subscribe((res) => {
this.fetcher(settings, res); this.fetcher(settings, res);
}); });
} }
} }
} }
newTransferEvent(tx): any { newTransferEvent(tx: any): any {
return new CustomEvent('cic_transfer', { return new CustomEvent('cic_transfer', {
detail: { detail: {
tx, tx,
@ -77,7 +86,7 @@ export class BlockSyncService {
}); });
} }
newConversionEvent(tx): any { newConversionEvent(tx: any): any {
return new CustomEvent('cic_convert', { return new CustomEvent('cic_convert', {
detail: { detail: {
tx, tx,
@ -85,8 +94,15 @@ export class BlockSyncService {
}); });
} }
async scan(settings, lo, hi, bloomBlockBytes, bloomBlocktxBytes, bloomRounds): Promise<void> { async scan(
const w = new Worker('./../assets/js/block-sync/ondemand.js'); settings: Settings,
lo: number,
hi: number,
bloomBlockBytes: Uint8Array,
bloomBlocktxBytes: Uint8Array,
bloomRounds: any
): Promise<void> {
const w: Worker = new Worker('./../assets/js/block-sync/ondemand.js');
w.onmessage = (m) => { w.onmessage = (m) => {
settings.txHelper.processReceipt(m.data); settings.txHelper.processReceipt(m.data);
}; };
@ -94,23 +110,27 @@ export class BlockSyncService {
w3_provider: settings.w3.provider, w3_provider: settings.w3.provider,
lo, lo,
hi, hi,
filters: [ filters: [bloomBlockBytes, bloomBlocktxBytes],
bloomBlockBytes,
bloomBlocktxBytes,
],
filter_rounds: bloomRounds, filter_rounds: bloomRounds,
}); });
} }
fetcher(settings: Settings, transactionsInfo: any): void { fetcher(settings: Settings, transactionsInfo: any): void {
const blockFilterBinstr = window.atob(transactionsInfo.block_filter); const blockFilterBinstr: string = window.atob(transactionsInfo.block_filter);
const bOne = new Uint8Array(blockFilterBinstr.length); const bOne: Uint8Array = new Uint8Array(blockFilterBinstr.length);
bOne.map((e, i, v) => v[i] = blockFilterBinstr.charCodeAt(i)); bOne.map((e, i, v) => (v[i] = blockFilterBinstr.charCodeAt(i)));
const blocktxFilterBinstr = window.atob(transactionsInfo.blocktx_filter); const blocktxFilterBinstr: string = window.atob(transactionsInfo.blocktx_filter);
const bTwo = new Uint8Array(blocktxFilterBinstr.length); const bTwo: Uint8Array = new Uint8Array(blocktxFilterBinstr.length);
bTwo.map((e, i, v) => v[i] = blocktxFilterBinstr.charCodeAt(i)); bTwo.map((e, i, v) => (v[i] = blocktxFilterBinstr.charCodeAt(i)));
settings.scanFilter(settings, transactionsInfo.low, transactionsInfo.high, bOne, bTwo, transactionsInfo.filter_rounds); settings.scanFilter(
settings,
transactionsInfo.low,
transactionsInfo.high,
bOne,
bTwo,
transactionsInfo.filter_rounds
);
} }
} }

View File

@ -1,27 +1,25 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {MatDialog} from '@angular/material/dialog'; import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { ErrorDialogComponent } from '@app/shared/error-dialog/error-dialog.component'; import { ErrorDialogComponent } from '@app/shared/error-dialog/error-dialog.component';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class ErrorDialogService { export class ErrorDialogService {
public isDialogOpen: boolean = false; public isDialogOpen: boolean = false;
constructor( constructor(public dialog: MatDialog) {}
public dialog: MatDialog,
) { }
openDialog(data): any { openDialog(data): any {
if (this.isDialogOpen) { if (this.isDialogOpen) {
return false; return false;
} }
this.isDialogOpen = true; this.isDialogOpen = true;
const dialogRef = this.dialog.open(ErrorDialogComponent, { const dialogRef: MatDialogRef<any> = this.dialog.open(ErrorDialogComponent, {
width: '300px', width: '300px',
data data,
}); });
dialogRef.afterClosed().subscribe(() => this.isDialogOpen = false); dialogRef.afterClosed().subscribe(() => (this.isDialogOpen = false));
} }
} }

View File

@ -1,22 +1,30 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {BehaviorSubject} from 'rxjs'; import { Observable } from 'rxjs';
import { environment } from '@src/environments/environment'; import { environment } from '@src/environments/environment';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class LocationService { export class LocationService {
locations: any = ''; constructor(private httpClient: HttpClient) {}
private locationsList = new BehaviorSubject<any>(this.locations);
locationsSubject = this.locationsList.asObservable();
constructor( getAreaNames(): Observable<any> {
private httpClient: HttpClient, return this.httpClient.get(`${environment.cicMetaUrl}/areanames`);
) { } }
getLocations(): void { getAreaNameByLocation(location: string): Observable<any> {
this.httpClient.get(`${environment.cicCacheUrl}/locations`).pipe(first()).subscribe(res => this.locationsList.next(res)); return this.httpClient.get(`${environment.cicMetaUrl}/areanames/${location.toLowerCase()}`);
}
getAreaTypes(): Observable<any> {
return this.httpClient.get(`${environment.cicMetaUrl}/areatypes`).pipe(first());
}
getAreaTypeByArea(area: string): Observable<any> {
return this.httpClient
.get(`${environment.cicMetaUrl}/areatypes/${area.toLowerCase()}`)
.pipe(first());
} }
} }

View File

@ -2,7 +2,7 @@ import {Injectable, isDevMode} from '@angular/core';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class LoggingService { export class LoggingService {
env: string; env: string;
@ -15,31 +15,31 @@ export class LoggingService {
} }
} }
sendTraceLevelMessage(message, source, error): void { sendTraceLevelMessage(message: any, source: any, error: any): void {
this.logger.trace(message, source, error); this.logger.trace(message, source, error);
} }
sendDebugLevelMessage(message, source, error): void { sendDebugLevelMessage(message: any, source: any, error: any): void {
this.logger.debug(message, source, error); this.logger.debug(message, source, error);
} }
sendInfoLevelMessage(message): void { sendInfoLevelMessage(message: any): void {
this.logger.info(message); this.logger.info(message);
} }
sendLogLevelMessage(message, source, error): void { sendLogLevelMessage(message: any, source: any, error: any): void {
this.logger.log(message, source, error); this.logger.log(message, source, error);
} }
sendWarnLevelMessage(message, error): void { sendWarnLevelMessage(message: any, error: any): void {
this.logger.warn(message, error); this.logger.warn(message, error);
} }
sendErrorLevelMessage(message, source, error): void { sendErrorLevelMessage(message: any, source: any, error: any): void {
this.logger.error(message, source, error); this.logger.error(message, source, error);
} }
sendFatalLevelMessage(message, source, error): void { sendFatalLevelMessage(message: any, source: any, error: any): void {
this.logger.fatal(message, source, error); this.logger.fatal(message, source, error);
} }
} }

View File

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

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import Web3 from 'web3';
import { environment } from '@src/environments/environment';
import { CICRegistry, FileGetter } from 'cic-client';
import { HttpGetter } from '@app/_helpers';
@Injectable({
providedIn: 'root',
})
export class RegistryService {
web3: Web3 = new Web3(environment.web3Provider);
fileGetter: FileGetter = new HttpGetter();
registry: CICRegistry = new CICRegistry(
this.web3,
environment.registryAddress,
'Registry',
this.fileGetter,
['../../assets/js/block-sync/data']
);
constructor() {
this.registry.declaratorHelper.addTrust(environment.trustedDeclaratorAddress);
this.registry.load();
}
getRegistry(): any {
return this.registry;
}
getWeb3(): any {
return this.web3;
}
}

View File

@ -1,32 +1,33 @@
import { Injectable } from '@angular/core'; import { EventEmitter, Injectable } from '@angular/core';
import { environment } from '@src/environments/environment'; import { environment } from '@src/environments/environment';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import {HttpGetter} from '@app/_helpers';
import { CICRegistry } from 'cic-client'; import { CICRegistry } from 'cic-client';
import Web3 from 'web3'; import { TokenRegistry } from '@app/_eth';
import {Registry, TokenRegistry} from '@app/_eth';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { RegistryService } from '@app/_services/registry.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class TokenService { export class TokenService {
web3 = new Web3(environment.web3Provider); registry: CICRegistry;
fileGetter = new HttpGetter(); tokenRegistry: TokenRegistry;
registry = new Registry(environment.registryAddress); LoadEvent: EventEmitter<number> = new EventEmitter<number>();
cicRegistry = new CICRegistry(this.web3, environment.registryAddress, this.fileGetter, ['../../assets/js/block-sync/data']);
tokens: any = '';
private tokensList = new BehaviorSubject<any>(this.tokens);
tokensSubject = this.tokensList.asObservable();
constructor( constructor(private httpClient: HttpClient, private registryService: RegistryService) {
private httpClient: HttpClient, this.registry = registryService.getRegistry();
) { } this.registry.load();
this.registry.onload = async (address: string): Promise<void> => {
this.tokenRegistry = new TokenRegistry(
await this.registry.getContractAddressByName('TokenRegistry')
);
this.LoadEvent.next(Date.now());
};
}
async getTokens(): Promise<any> { async getTokens(): Promise<Array<Promise<string>>> {
const tokenRegistryQuery = new TokenRegistry(await this.registry.addressOf('TokenRegistry')); const count: number = await this.tokenRegistry.totalTokens();
const count = await tokenRegistryQuery.totalTokens(); return Array.from({ length: count }, async (v, i) => await this.tokenRegistry.entry(i));
return Array.from({length: count}, async (v, i) => await tokenRegistryQuery.entry(i));
} }
getTokenBySymbol(symbol: string): Observable<any> { getTokenBySymbol(symbol: string): Observable<any> {
@ -34,8 +35,7 @@ export class TokenService {
} }
async getTokenBalance(address: string): Promise<number> { async getTokenBalance(address: string): Promise<number> {
const tokenRegistryQuery = new TokenRegistry(await this.registry.addressOf('TokenRegistry')); const sarafuToken = await this.registry.addToken(await this.tokenRegistry.entry(0));
const sarafuToken = await this.cicRegistry.addToken(await tokenRegistryQuery.entry(0));
return await sarafuToken.methods.balanceOf(address).call(); return await sarafuToken.methods.balanceOf(address).call();
} }
} }

View File

@ -11,7 +11,7 @@ describe('TransactionService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [HttpClientTestingModule] imports: [HttpClientTestingModule],
}); });
httpClient = TestBed.inject(HttpClient); httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController); httpTestingController = TestBed.inject(HttpTestingController);

View File

@ -13,28 +13,34 @@ import * as secp256k1 from 'secp256k1';
import { AuthService } from '@app/_services/auth.service'; import { AuthService } from '@app/_services/auth.service';
import { defaultAccount } from '@app/_models'; import { defaultAccount } from '@app/_models';
import { LoggingService } from '@app/_services/logging.service'; import { LoggingService } from '@app/_services/logging.service';
import {Registry} from '@app/_eth';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
const Web3 = require('web3'); import { CICRegistry } from 'cic-client';
import { RegistryService } from '@app/_services/registry.service';
import Web3 from 'web3';
const vCard = require('vcard-parser'); const vCard = require('vcard-parser');
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class TransactionService { export class TransactionService {
transactions: any[] = []; transactions: any[] = [];
private transactionList = new BehaviorSubject<any[]>(this.transactions); private transactionList = new BehaviorSubject<any[]>(this.transactions);
transactionsSubject = this.transactionList.asObservable(); transactionsSubject = this.transactionList.asObservable();
userInfo: any; userInfo: any;
web3 = new Web3(environment.web3Provider); web3: Web3;
registry = new Registry(environment.registryAddress); registry: CICRegistry;
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
private authService: AuthService, private authService: AuthService,
private userService: UserService, private userService: UserService,
private loggingService: LoggingService private loggingService: LoggingService,
) { } private registryService: RegistryService
) {
this.web3 = this.registryService.getWeb3();
this.registry = registryService.getRegistry();
this.registry.load();
}
getAllTransactions(offset: number, limit: number): Observable<any> { getAllTransactions(offset: number, limit: number): Observable<any> {
return this.httpClient.get(`${environment.cicCacheUrl}/tx/${offset}/${limit}`); return this.httpClient.get(`${environment.cicCacheUrl}/tx/${offset}/${limit}`);
@ -45,36 +51,58 @@ export class TransactionService {
} }
async setTransaction(transaction, cacheSize: number): Promise<void> { async setTransaction(transaction, cacheSize: number): Promise<void> {
if (this.transactions.find(cachedTx => cachedTx.tx.txHash === transaction.tx.txHash)) { return; } if (this.transactions.find((cachedTx) => cachedTx.tx.txHash === transaction.tx.txHash)) {
return;
}
transaction.value = Number(transaction.value); transaction.value = Number(transaction.value);
transaction.type = 'transaction'; transaction.type = 'transaction';
try { try {
this.userService.getAccountDetailsFromMeta(await User.toKey(transaction.from)).pipe(first()).subscribe((res) => { this.userService
.getAccountDetailsFromMeta(await User.toKey(transaction.from))
.pipe(first())
.subscribe(
(res) => {
transaction.sender = this.getAccountInfo(res.body); transaction.sender = this.getAccountInfo(res.body);
}, error => { },
(error) => {
transaction.sender = defaultAccount; transaction.sender = defaultAccount;
}); }
this.userService.getAccountDetailsFromMeta(await User.toKey(transaction.to)).pipe(first()).subscribe((res) => { );
this.userService
.getAccountDetailsFromMeta(await User.toKey(transaction.to))
.pipe(first())
.subscribe(
(res) => {
transaction.recipient = this.getAccountInfo(res.body); transaction.recipient = this.getAccountInfo(res.body);
}, error => { },
(error) => {
transaction.recipient = defaultAccount; transaction.recipient = defaultAccount;
}); }
);
} finally { } finally {
this.addTransaction(transaction, cacheSize); this.addTransaction(transaction, cacheSize);
} }
} }
async setConversion(conversion, cacheSize): Promise<void> { async setConversion(conversion, cacheSize): Promise<void> {
if (this.transactions.find(cachedTx => cachedTx.tx.txHash === conversion.tx.txHash)) { return; } if (this.transactions.find((cachedTx) => cachedTx.tx.txHash === conversion.tx.txHash)) {
return;
}
conversion.type = 'conversion'; conversion.type = 'conversion';
conversion.fromValue = Number(conversion.fromValue); conversion.fromValue = Number(conversion.fromValue);
conversion.toValue = Number(conversion.toValue); conversion.toValue = Number(conversion.toValue);
try { try {
this.userService.getAccountDetailsFromMeta(await User.toKey(conversion.trader)).pipe(first()).subscribe((res) => { this.userService
.getAccountDetailsFromMeta(await User.toKey(conversion.trader))
.pipe(first())
.subscribe(
(res) => {
conversion.sender = conversion.recipient = this.getAccountInfo(res.body); conversion.sender = conversion.recipient = this.getAccountInfo(res.body);
}, error => { },
(error) => {
conversion.sender = conversion.recipient = defaultAccount; conversion.sender = conversion.recipient = defaultAccount;
}); }
);
} finally { } finally {
this.addTransaction(conversion, cacheSize); this.addTransaction(conversion, cacheSize);
} }
@ -94,23 +122,33 @@ export class TransactionService {
} }
getAccountInfo(account: string): any { getAccountInfo(account: string): any {
let accountInfo = Envelope.fromJSON(JSON.stringify(account)).unwrap().m.data; const accountInfo = Envelope.fromJSON(JSON.stringify(account)).unwrap().m.data;
accountInfo.vcard = vCard.parse(atob(accountInfo.vcard)); accountInfo.vcard = vCard.parse(atob(accountInfo.vcard));
return accountInfo; return accountInfo;
} }
async transferRequest(tokenAddress: string, senderAddress: string, recipientAddress: string, value: number): Promise<any> { async transferRequest(
const transferAuthAddress = await this.registry.addressOf('TransferAuthorization'); tokenAddress: string,
senderAddress: string,
recipientAddress: string,
value: number
): Promise<any> {
const transferAuthAddress = await this.registry.getContractAddressByName(
'TransferAuthorization'
);
const hashFunction = new Keccak(256); const hashFunction = new Keccak(256);
hashFunction.update('createRequest(address,address,address,uint256)'); hashFunction.update('createRequest(address,address,address,uint256)');
const hash = hashFunction.digest(); const hash = hashFunction.digest();
const methodSignature = hash.toString('hex').substring(0, 8); const methodSignature = hash.toString('hex').substring(0, 8);
const abiCoder = new utils.AbiCoder(); const abiCoder = new utils.AbiCoder();
const abi = await abiCoder.encode(['address', 'address', 'address', 'uint256'], [senderAddress, recipientAddress, tokenAddress, value]); const abi = await abiCoder.encode(
['address', 'address', 'address', 'uint256'],
[senderAddress, recipientAddress, tokenAddress, value]
);
const data = fromHex(methodSignature + strip0x(abi)); const data = fromHex(methodSignature + strip0x(abi));
const tx = new Tx(environment.bloxbergChainId); const tx = new Tx(environment.bloxbergChainId);
tx.nonce = await this.web3.eth.getTransactionCount(senderAddress); tx.nonce = await this.web3.eth.getTransactionCount(senderAddress);
tx.gasPrice = await this.web3.eth.getGasPrice(); tx.gasPrice = Number(await this.web3.eth.getGasPrice());
tx.gasLimit = 8000000; tx.gasLimit = 8000000;
tx.to = fromHex(strip0x(transferAuthAddress)); tx.to = fromHex(strip0x(transferAuthAddress));
tx.value = toValue(value); tx.value = toValue(value);

View File

@ -11,7 +11,7 @@ describe('UserService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [HttpClientTestingModule] imports: [HttpClientTestingModule],
}); });
httpClient = TestBed.inject(HttpClient); httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController); httpTestingController = TestBed.inject(HttpTestingController);
@ -34,7 +34,7 @@ describe('UserService', () => {
failedPinAttempts: 1, failedPinAttempts: 1,
status: 'approved', status: 'approved',
bio: 'Bodaboda', bio: 'Bodaboda',
gender: 'male' gender: 'male',
}); });
}); });
@ -48,7 +48,7 @@ describe('UserService', () => {
user: 'Tom', user: 'Tom',
role: 'enroller', role: 'enroller',
action: 'Disburse RSV 100', action: 'Disburse RSV 100',
approval: false approval: false,
}); });
}); });
@ -63,7 +63,7 @@ describe('UserService', () => {
user: 'Tom', user: 'Tom',
role: 'enroller', role: 'enroller',
action: 'Disburse RSV 100', action: 'Disburse RSV 100',
approval: true approval: true,
}); });
}); });
@ -74,7 +74,7 @@ describe('UserService', () => {
user: 'Christine', user: 'Christine',
role: 'admin', role: 'admin',
action: 'Change user phone number', action: 'Change user phone number',
approval: false approval: false,
}); });
}); });
}); });

View File

@ -1,64 +1,90 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs'; import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { environment } from '@src/environments/environment'; import { environment } from '@src/environments/environment';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import {ArgPair, Envelope, Syncable, User} from 'cic-client-meta'; import { ArgPair, Envelope, Phone, Syncable, User } from 'cic-client-meta';
import {MetaResponse} from '@app/_models'; import { AccountDetails } from '@app/_models';
import { LoggingService } from '@app/_services/logging.service'; import { LoggingService } from '@app/_services/logging.service';
import { TokenService } from '@app/_services/token.service'; import { TokenService } from '@app/_services/token.service';
import {AccountIndex, Registry} from '@app/_eth'; import { AccountIndex } from '@app/_eth';
import {MutableKeyStore, MutablePgpKeyStore, PGPSigner, Signer} from '@app/_pgp'; import { MutableKeyStore, PGPSigner, Signer } from '@app/_pgp';
import { RegistryService } from '@app/_services/registry.service';
import { CICRegistry } from 'cic-client';
import { AuthService } from '@app/_services/auth.service';
import { personValidation, vcardValidation } from '@app/_helpers';
import { add0x } from '@src/assets/js/ethtx/dist/hex';
const vCard = require('vcard-parser'); const vCard = require('vcard-parser');
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class UserService { export class UserService {
headers: HttpHeaders = new HttpHeaders({ 'x-cic-automerge': 'client' }); headers: HttpHeaders = new HttpHeaders({ 'x-cic-automerge': 'client' });
keystore: MutableKeyStore = new MutablePgpKeyStore(); keystore: MutableKeyStore;
signer: Signer = new PGPSigner(this.keystore); signer: Signer;
registry = new Registry(environment.registryAddress); registry: CICRegistry;
accountsMeta = []; accounts: Array<AccountDetails> = [];
accounts: any = []; private accountsList: BehaviorSubject<Array<AccountDetails>> = new BehaviorSubject<
private accountsList = new BehaviorSubject<any>(this.accounts); Array<AccountDetails>
accountsSubject = this.accountsList.asObservable(); >(this.accounts);
accountsSubject: Observable<Array<AccountDetails>> = this.accountsList.asObservable();
actions: any = ''; actions: Array<any> = [];
private actionsList = new BehaviorSubject<any>(this.actions); private actionsList: BehaviorSubject<any> = new BehaviorSubject<any>(this.actions);
actionsSubject = this.actionsList.asObservable(); actionsSubject: Observable<Array<any>> = this.actionsList.asObservable();
staff: any = '';
private staffList = new BehaviorSubject<any>(this.staff);
staffSubject = this.staffList.asObservable();
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
private loggingService: LoggingService, private loggingService: LoggingService,
private tokenService: TokenService private tokenService: TokenService,
private registryService: RegistryService,
private authService: AuthService
) { ) {
this.authService.init().then(() => {
this.keystore = authService.mutableKeyStore;
this.signer = new PGPSigner(this.keystore);
});
this.registry = registryService.getRegistry();
this.registry.load();
} }
resetPin(phone: string): Observable<any> { resetPin(phone: string): Observable<any> {
const params = new HttpParams().set('phoneNumber', phone); const params: HttpParams = new HttpParams().set('phoneNumber', phone);
return this.httpClient.get(`${environment.cicUssdUrl}/pin`, { params }); return this.httpClient.get(`${environment.cicUssdUrl}/pin`, { params });
} }
getAccountStatus(phone: string): any { getAccountStatus(phone: string): Observable<any> {
const params = new HttpParams().set('phoneNumber', phone); const params: HttpParams = new HttpParams().set('phoneNumber', phone);
return this.httpClient.get(`${environment.cicUssdUrl}/pin`, { params }); return this.httpClient.get(`${environment.cicUssdUrl}/pin`, { params });
} }
getLockedAccounts(offset: number, limit: number): any { getLockedAccounts(offset: number, limit: number): Observable<any> {
return this.httpClient.get(`${environment.cicUssdUrl}/accounts/locked/${offset}/${limit}`); return this.httpClient.get(`${environment.cicUssdUrl}/accounts/locked/${offset}/${limit}`);
} }
async changeAccountInfo(address: string, name: string, phoneNumber: string, age: string, type: string, bio: string, gender: string, async changeAccountInfo(
businessCategory: string, userLocation: string, location: string, locationType: string, metaAccount: MetaResponse address: string,
name: string,
phoneNumber: string,
age: string,
type: string,
bio: string,
gender: string,
businessCategory: string,
userLocation: string,
location: string,
locationType: string
): Promise<any> { ): Promise<any> {
let reqBody = metaAccount; const accountInfo: any = {
let accountInfo = reqBody.m.data; vcard: {
fn: [{}],
n: [{}],
tel: [{}],
},
location: {},
};
accountInfo.vcard.fn[0].value = name; accountInfo.vcard.fn[0].value = name;
accountInfo.vcard.n[0].value = name.split(' '); accountInfo.vcard.n[0].value = name.split(' ');
accountInfo.vcard.tel[0].value = phoneNumber; accountInfo.vcard.tel[0].value = phoneNumber;
@ -70,46 +96,58 @@ export class UserService {
accountInfo.location.area = location; accountInfo.location.area = location;
accountInfo.location.area_name = userLocation; accountInfo.location.area_name = userLocation;
accountInfo.location.area_type = locationType; accountInfo.location.area_type = locationType;
accountInfo.vcard = vCard.generate(accountInfo.vcard); await vcardValidation(accountInfo.vcard);
reqBody.m.data = accountInfo; accountInfo.vcard = btoa(vCard.generate(accountInfo.vcard));
const accountKey = await User.toKey(address); const accountKey: string = await User.toKey(address);
this.httpClient.get(`${environment.cicMetaUrl}/${accountKey}`, { headers: this.headers }).pipe(first()).subscribe(async res => { this.getAccountDetailsFromMeta(accountKey)
.pipe(first())
.subscribe(
async (res) => {
const syncableAccount: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap(); const syncableAccount: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
let update = []; const update: Array<ArgPair> = [];
for (const prop in reqBody) { for (const prop of Object.keys(accountInfo)) {
update.push(new ArgPair(prop, reqBody[prop])); update.push(new ArgPair(prop, accountInfo[prop]));
} }
syncableAccount.update(update, 'client-branch'); syncableAccount.update(update, 'client-branch');
await personValidation(syncableAccount.m.data);
await this.updateMeta(syncableAccount, accountKey, this.headers); await this.updateMeta(syncableAccount, accountKey, this.headers);
}, async error => { },
this.loggingService.sendErrorLevelMessage('Can\'t find account info in meta service', this, {error}); async (error) => {
this.loggingService.sendErrorLevelMessage(
'Cannot find account info in meta service',
this,
{ error }
);
const syncableAccount: Syncable = new Syncable(accountKey, accountInfo); const syncableAccount: Syncable = new Syncable(accountKey, accountInfo);
await this.updateMeta(syncableAccount, accountKey, this.headers); await this.updateMeta(syncableAccount, accountKey, this.headers);
}); }
);
return accountKey; return accountKey;
} }
async updateMeta(syncableAccount: Syncable, accountKey: string, headers: HttpHeaders): Promise<any> { async updateMeta(
const envelope = await this.wrap(syncableAccount , this.signer); syncableAccount: Syncable,
const reqBody = envelope.toJSON(); accountKey: string,
this.httpClient.put(`${environment.cicMetaUrl}/${accountKey}`, reqBody , { headers }).pipe(first()).subscribe(res => { headers: HttpHeaders
): Promise<any> {
const envelope: Envelope = await this.wrap(syncableAccount, this.signer);
const reqBody: string = envelope.toJSON();
this.httpClient
.put(`${environment.cicMetaUrl}/${accountKey}`, reqBody, { headers })
.pipe(first())
.subscribe((res) => {
this.loggingService.sendInfoLevelMessage(`Response: ${res}`); this.loggingService.sendInfoLevelMessage(`Response: ${res}`);
}); });
} }
getAccounts(): void {
this.httpClient.get(`${environment.cicCacheUrl}/accounts`).pipe(first()).subscribe(res => this.accountsList.next(res));
}
getAccountById(id: number): Observable<any> {
return this.httpClient.get(`${environment.cicCacheUrl}/accounts/${id}`);
}
getActions(): void { getActions(): void {
this.httpClient.get(`${environment.cicCacheUrl}/actions`).pipe(first()).subscribe(res => this.actionsList.next(res)); this.httpClient
.get(`${environment.cicCacheUrl}/actions`)
.pipe(first())
.subscribe((res) => this.actionsList.next(res));
} }
getActionById(id: string): any { getActionById(id: string): Observable<any> {
return this.httpClient.get(`${environment.cicCacheUrl}/actions/${id}`); return this.httpClient.get(`${environment.cicCacheUrl}/actions/${id}`);
} }
@ -121,30 +159,19 @@ export class UserService {
return this.httpClient.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: false }); return this.httpClient.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: false });
} }
getHistoryByUser(id: string): Observable<any> {
return this.httpClient.get(`${environment.cicCacheUrl}/history/${id}`);
}
getAccountDetailsFromMeta(userKey: string): Observable<any> { getAccountDetailsFromMeta(userKey: string): Observable<any> {
return this.httpClient.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers }); return this.httpClient.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers });
} }
getUser(userKey: string): any {
return this.httpClient.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers })
.pipe(first()).subscribe(async res => {
return Envelope.fromJSON(JSON.stringify(res)).unwrap();
});
}
wrap(syncable: Syncable, signer: Signer): Promise<Envelope> { wrap(syncable: Syncable, signer: Signer): Promise<Envelope> {
return new Promise<Envelope>(async (whohoo, doh) => { return new Promise<Envelope>(async (resolve, reject) => {
syncable.setSigner(signer); syncable.setSigner(signer);
syncable.onwrap = async (env) => { syncable.onwrap = async (env) => {
if (env === undefined) { if (env === undefined) {
doh(); reject();
return; return;
} }
whohoo(env); resolve(env);
}; };
await syncable.sign(); await syncable.sign();
}); });
@ -152,28 +179,89 @@ export class UserService {
async loadAccounts(limit: number = 100, offset: number = 0): Promise<void> { async loadAccounts(limit: number = 100, offset: number = 0): Promise<void> {
this.resetAccountsList(); this.resetAccountsList();
const accountIndexAddress = await this.registry.addressOf('AccountRegistry'); const accountIndexAddress: string = await this.registry.getContractAddressByName(
'AccountRegistry'
);
const accountIndexQuery = new AccountIndex(accountIndexAddress); const accountIndexQuery = new AccountIndex(accountIndexAddress);
const accountAddresses = await accountIndexQuery.last(await accountIndexQuery.totalAccounts()); const accountAddresses: Array<string> = await accountIndexQuery.last(
await accountIndexQuery.totalAccounts()
);
this.loggingService.sendInfoLevelMessage(accountAddresses); this.loggingService.sendInfoLevelMessage(accountAddresses);
for (const accountAddress of accountAddresses.slice(offset, offset + limit)) { for (const accountAddress of accountAddresses.slice(offset, offset + limit)) {
this.getAccountDetailsFromMeta(await User.toKey(accountAddress)).pipe(first()).subscribe(async res => { await this.getAccountByAddress(accountAddress, limit);
const account = Envelope.fromJSON(JSON.stringify(res)).unwrap(); }
this.accountsMeta.push(account); }
async getAccountByAddress(
accountAddress: string,
limit: number = 100
): Promise<Observable<AccountDetails>> {
const accountSubject: Subject<any> = new Subject<any>();
this.getAccountDetailsFromMeta(await User.toKey(add0x(accountAddress)))
.pipe(first())
.subscribe(async (res) => {
const account: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
const accountInfo = account.m.data; const accountInfo = account.m.data;
accountInfo.balance = await this.tokenService.getTokenBalance(accountInfo.identities.evm['bloxberg:8996'][0]); await personValidation(accountInfo);
accountInfo.balance = await this.tokenService.getTokenBalance(
accountInfo.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0]
);
accountInfo.vcard = vCard.parse(atob(accountInfo.vcard)); accountInfo.vcard = vCard.parse(atob(accountInfo.vcard));
await vcardValidation(accountInfo.vcard);
this.accounts.unshift(accountInfo); this.accounts.unshift(accountInfo);
if (this.accounts.length > limit) { if (this.accounts.length > limit) {
this.accounts.length = limit; this.accounts.length = limit;
} }
this.accountsList.next(this.accounts); this.accountsList.next(this.accounts);
accountSubject.next(accountInfo);
}); });
return accountSubject.asObservable();
} }
async getAccountByPhone(
phoneNumber: string,
limit: number = 100
): Promise<Observable<AccountDetails>> {
const accountSubject: Subject<any> = new Subject<any>();
this.getAccountDetailsFromMeta(await Phone.toKey(phoneNumber))
.pipe(first())
.subscribe(async (res) => {
const response: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
const address: string = response.m.data;
const account: Observable<AccountDetails> = await this.getAccountByAddress(address, limit);
account.subscribe((result) => {
accountSubject.next(result);
});
});
return accountSubject.asObservable();
} }
resetAccountsList(): void { resetAccountsList(): void {
this.accounts = []; this.accounts = [];
this.accountsList.next(this.accounts); this.accountsList.next(this.accounts);
} }
searchAccountByName(name: string): any {
return;
}
getCategories(): Observable<any> {
return this.httpClient.get(`${environment.cicMetaUrl}/categories`);
}
getCategoryByProduct(product: string): Observable<any> {
return this.httpClient.get(`${environment.cicMetaUrl}/categories/${product.toLowerCase()}`);
}
getAccountTypes(): Observable<any> {
return this.httpClient.get(`${environment.cicMetaUrl}/accounttypes`);
}
getTransactionTypes(): Observable<any> {
return this.httpClient.get(`${environment.cicMetaUrl}/transactiontypes`);
}
getGenders(): Observable<any> {
return this.httpClient.get(`${environment.cicMetaUrl}/genders`);
}
} }

View File

@ -3,15 +3,22 @@ import {Routes, RouterModule, PreloadAllModules} from '@angular/router';
import { AuthGuard } from '@app/_guards'; import { AuthGuard } from '@app/_guards';
const routes: Routes = [ const routes: Routes = [
{ path: 'auth', loadChildren: () => import('@app/auth/auth.module').then(m => m.AuthModule) }, { path: 'auth', loadChildren: () => import('@app/auth/auth.module').then((m) => m.AuthModule) },
{ path: '', loadChildren: () => import('@pages/pages.module').then(m => m.PagesModule), canActivate: [AuthGuard] }, {
{ path: '**', redirectTo: '', pathMatch: 'full' } path: '',
loadChildren: () => import('@pages/pages.module').then((m) => m.PagesModule),
canActivate: [AuthGuard],
},
{ path: '**', redirectTo: '', pathMatch: 'full' },
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes, { imports: [
preloadingStrategy: PreloadAllModules RouterModule.forRoot(routes, {
})], preloadingStrategy: PreloadAllModules,
exports: [RouterModule] useHash: true,
}),
],
exports: [RouterModule],
}) })
export class AppRoutingModule {} export class AppRoutingModule {}

View File

@ -1 +1,2 @@
<app-network-status></app-network-status>
<router-outlet (activate)="onResize(mediaQuery)"></router-outlet> <router-outlet (activate)="onResize(mediaQuery)"></router-outlet>

View File

@ -2,23 +2,19 @@ import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from '@app/app.component'; import { AppComponent } from '@app/app.component';
import { TransactionService } from '@app/_services'; import { TransactionService } from '@app/_services';
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent, TransactionServiceStub} from '@src/testing'; import {
FooterStubComponent,
SidebarStubComponent,
TopbarStubComponent,
TransactionServiceStub,
} from '@src/testing';
describe('AppComponent', () => { describe('AppComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [RouterTestingModule],
RouterTestingModule declarations: [AppComponent, FooterStubComponent, SidebarStubComponent, TopbarStubComponent],
], providers: [{ provide: TransactionService, useClass: TransactionServiceStub }],
declarations: [
AppComponent,
FooterStubComponent,
SidebarStubComponent,
TopbarStubComponent
],
providers: [
{ provide: TransactionService, useClass: TransactionServiceStub }
]
}).compileComponents(); }).compileComponents();
}); });

View File

@ -1,42 +1,68 @@
import {ChangeDetectionStrategy, Component, HostListener} from '@angular/core'; import { ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
import {AuthService, ErrorDialogService, LoggingService, TransactionService} from '@app/_services'; import {
AuthService,
ErrorDialogService,
LoggingService,
TransactionService,
} from '@app/_services';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { SwUpdate } from '@angular/service-worker';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AppComponent { export class AppComponent implements OnInit {
title = 'CICADA'; title = 'CICADA';
readyStateTarget: number = 3; readyStateTarget: number = 3;
readyState: number = 0; readyState: number = 0;
mediaQuery = window.matchMedia('(max-width: 768px)'); mediaQuery: MediaQueryList = window.matchMedia('(max-width: 768px)');
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private transactionService: TransactionService, private transactionService: TransactionService,
private loggingService: LoggingService, private loggingService: LoggingService,
private errorDialogService: ErrorDialogService private errorDialogService: ErrorDialogService,
private swUpdate: SwUpdate
) { ) {
(async () => { (async () => {
await this.authService.mutableKeyStore.loadKeyring(); try {
this.authService.getPublicKeys() await this.authService.init();
.pipe(catchError(async (error) => { // this.authService.getPublicKeys()
this.loggingService.sendErrorLevelMessage('Unable to load trusted public keys.', this, {error}); // .pipe(catchError(async (error) => {
this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'}); // this.loggingService.sendErrorLevelMessage('Unable to load trusted public keys.', this, {error});
})).subscribe(this.authService.mutableKeyStore.importPublicKey); // this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'});
// })).subscribe(this.authService.mutableKeyStore.importPublicKey);
const publicKeys = await this.authService.getPublicKeys();
await this.authService.mutableKeyStore.importPublicKey(publicKeys);
} catch (error) {
this.errorDialogService.openDialog({
message: 'Trusted keys endpoint cannot be reached. Please try again later.',
});
// TODO do something to halt user progress...show a sad cicada page 🦗?
}
})(); })();
this.mediaQuery.addListener(this.onResize); this.mediaQuery.addEventListener('change', this.onResize);
this.onResize(this.mediaQuery); this.onResize(this.mediaQuery);
} }
ngOnInit(): void {
if (!this.swUpdate.isEnabled) {
this.swUpdate.available.subscribe(() => {
if (confirm('New Version available. Load New Version?')) {
window.location.reload();
}
});
}
}
// Load resize // Load resize
onResize(e): void { onResize(e): void {
const sidebar = document.getElementById('sidebar'); const sidebar: HTMLElement = document.getElementById('sidebar');
const content = document.getElementById('content'); const content: HTMLElement = document.getElementById('content');
const sidebarCollapse = document.getElementById('sidebarCollapse'); const sidebarCollapse: HTMLElement = document.getElementById('sidebarCollapse');
if (sidebarCollapse?.classList.contains('active')) { if (sidebarCollapse?.classList.contains('active')) {
sidebarCollapse?.classList.remove('active'); sidebarCollapse?.classList.remove('active');
} }
@ -59,13 +85,13 @@ export class AppComponent {
@HostListener('window:cic_transfer', ['$event']) @HostListener('window:cic_transfer', ['$event'])
async cicTransfer(event: CustomEvent): Promise<void> { async cicTransfer(event: CustomEvent): Promise<void> {
const transaction = event.detail.tx; const transaction: any = event.detail.tx;
await this.transactionService.setTransaction(transaction, 100); await this.transactionService.setTransaction(transaction, 100);
} }
@HostListener('window:cic_convert', ['$event']) @HostListener('window:cic_convert', ['$event'])
async cicConvert(event: CustomEvent): Promise<void> { async cicConvert(event: CustomEvent): Promise<void> {
const conversion = event.detail.tx; const conversion: any = event.detail.tx;
await this.transactionService.setConversion(conversion, 100); await this.transactionService.setConversion(conversion, 100);
} }
} }

View File

@ -5,11 +5,7 @@ import {AppRoutingModule} from '@app/app-routing.module';
import { AppComponent } from '@app/app.component'; import { AppComponent } from '@app/app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { import { GlobalErrorHandler, MockBackendProvider } from '@app/_helpers';
GlobalErrorHandler,
MockBackendProvider,
} from '@app/_helpers';
import {DataTablesModule} from 'angular-datatables';
import { SharedModule } from '@app/shared/shared.module'; import { SharedModule } from '@app/shared/shared.module';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { AuthGuard } from '@app/_guards'; import { AuthGuard } from '@app/_guards';
@ -17,25 +13,24 @@ import {LoggerModule} from 'ngx-logger';
import { environment } from '@src/environments/environment'; import { environment } from '@src/environments/environment';
import { ErrorInterceptor, HttpConfigInterceptor, LoggingInterceptor } from '@app/_interceptors'; import { ErrorInterceptor, HttpConfigInterceptor, LoggingInterceptor } from '@app/_interceptors';
import { MutablePgpKeyStore } from '@app/_pgp'; import { MutablePgpKeyStore } from '@app/_pgp';
import { ServiceWorkerModule } from '@angular/service-worker';
@NgModule({ @NgModule({
declarations: [ declarations: [AppComponent],
AppComponent
],
imports: [ imports: [
BrowserModule, BrowserModule,
AppRoutingModule, AppRoutingModule,
BrowserAnimationsModule, BrowserAnimationsModule,
HttpClientModule, HttpClientModule,
DataTablesModule,
SharedModule, SharedModule,
MatTableModule, MatTableModule,
LoggerModule.forRoot({ LoggerModule.forRoot({
level: environment.logLevel, level: environment.logLevel,
serverLogLevel: environment.serverLogLevel, serverLogLevel: environment.serverLogLevel,
serverLoggingUrl: `${environment.loggingUrl}/api/logs/`, serverLoggingUrl: `${environment.loggingUrl}/api/logs/`,
disableConsoleLogging: false disableConsoleLogging: false,
}) }),
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
], ],
providers: [ providers: [
AuthGuard, AuthGuard,
@ -47,6 +42,6 @@ import {MutablePgpKeyStore} from '@app/_pgp';
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent],
}) })
export class AppModule {} export class AppModule {}

View File

@ -1,7 +1,9 @@
import { PasswordToggleDirective } from '@app/auth/_directives/password-toggle.directive'; import { PasswordToggleDirective } from '@app/auth/_directives/password-toggle.directive';
import { ElementRef, Renderer2 } from '@angular/core'; import { ElementRef, Renderer2 } from '@angular/core';
// tslint:disable-next-line:prefer-const
let elementRef: ElementRef; let elementRef: ElementRef;
// tslint:disable-next-line:prefer-const
let renderer: Renderer2; let renderer: Renderer2;
describe('PasswordToggleDirective', () => { describe('PasswordToggleDirective', () => {

View File

@ -1,7 +1,7 @@
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
@Directive({ @Directive({
selector: '[appPasswordToggle]' selector: '[appPasswordToggle]',
}) })
export class PasswordToggleDirective { export class PasswordToggleDirective {
@Input() @Input()
@ -10,18 +10,15 @@ export class PasswordToggleDirective {
@Input() @Input()
iconId: string; iconId: string;
constructor( constructor(private elementRef: ElementRef, private renderer: Renderer2) {
private elementRef: ElementRef,
private renderer: Renderer2,
) {
this.renderer.listen(this.elementRef.nativeElement, 'click', () => { this.renderer.listen(this.elementRef.nativeElement, 'click', () => {
this.togglePasswordVisibility(); this.togglePasswordVisibility();
}); });
} }
togglePasswordVisibility(): void { togglePasswordVisibility(): void {
const password = document.getElementById(this.id); const password: HTMLElement = document.getElementById(this.id);
const icon = document.getElementById(this.iconId); const icon: HTMLElement = document.getElementById(this.iconId);
// @ts-ignore // @ts-ignore
if (password.type === 'password') { if (password.type === 'password') {
// @ts-ignore // @ts-ignore

View File

@ -10,6 +10,6 @@ const routes: Routes = [
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class AuthRoutingModule {} export class AuthRoutingModule {}

View File

@ -8,9 +8,8 @@ describe('AuthComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ AuthComponent ] declarations: [AuthComponent],
}) }).compileComponents();
.compileComponents();
}); });
beforeEach(() => { beforeEach(() => {

View File

@ -8,16 +8,16 @@ import {Router} from '@angular/router';
selector: 'app-auth', selector: 'app-auth',
templateUrl: './auth.component.html', templateUrl: './auth.component.html',
styleUrls: ['./auth.component.scss'], styleUrls: ['./auth.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AuthComponent implements OnInit { export class AuthComponent implements OnInit {
keyForm: FormGroup; keyForm: FormGroup;
matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
keyFormSubmitted: boolean = false; keyFormSubmitted: boolean = false;
keyFormLoading: boolean = false; keyFormLoading: boolean = false;
passwordForm: FormGroup; passwordForm: FormGroup;
passwordFormSubmitted: boolean = false; passwordFormSubmitted: boolean = false;
passwordFormLoading: boolean = false; passwordFormLoading: boolean = false;
matcher = new CustomErrorStateMatcher();
constructor( constructor(
private authService: AuthService, private authService: AuthService,
@ -50,7 +50,9 @@ export class AuthComponent implements OnInit {
async onSubmit(): Promise<void> { async onSubmit(): Promise<void> {
this.keyFormSubmitted = true; this.keyFormSubmitted = true;
if (this.keyForm.invalid) { return; } if (this.keyForm.invalid) {
return;
}
this.keyFormLoading = true; this.keyFormLoading = true;
const keySetup = await this.authService.setKey(this.keyFormStub.key.value); const keySetup = await this.authService.setKey(this.keyFormStub.key.value);
@ -111,7 +113,7 @@ export class AuthComponent implements OnInit {
} }
toggleDisplay(element: any, active: boolean): void { toggleDisplay(element: any, active: boolean): void {
const style = window.getComputedStyle(element).display; const style: string = window.getComputedStyle(element).display;
if (active) { if (active) {
element.style.display = 'block'; element.style.display = 'block';
} else { } else {

View File

@ -11,7 +11,6 @@ import {MatInputModule} from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatRippleModule } from '@angular/material/core'; import { MatRippleModule } from '@angular/material/core';
@NgModule({ @NgModule({
declarations: [AuthComponent, PasswordToggleDirective], declarations: [AuthComponent, PasswordToggleDirective],
imports: [ imports: [
@ -23,6 +22,6 @@ import {MatRippleModule} from '@angular/material/core';
MatInputModule, MatInputModule,
MatButtonModule, MatButtonModule,
MatRippleModule, MatRippleModule,
] ],
}) })
export class AuthModule {} export class AuthModule {}

View File

@ -14,7 +14,7 @@
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li> <li class="breadcrumb-item"><a routerLink="/home">Home</a></li>
<li class="breadcrumb-item"><a routerLink="/accounts">Accounts</a></li> <li class="breadcrumb-item"><a routerLink="/accounts">Accounts</a></li>
<li *ngIf="account !== undefined" class="breadcrumb-item active" aria-current="page">{{account?.vcard?.fn[0].value}}</li> <li *ngIf="account" class="breadcrumb-item active" aria-current="page">{{account?.vcard?.fn[0].value}}</li>
</ol> </ol>
</nav> </nav>
<div *ngIf="!account" class="text-center"> <div *ngIf="!account" class="text-center">
@ -35,7 +35,10 @@
</h3> </h3>
<span class="ml-auto"><strong>Balance:</strong> {{account?.balance | tokenRatio}} SRF</span> <span class="ml-auto"><strong>Balance:</strong> {{account?.balance | tokenRatio}} SRF</span>
<span class="ml-2"><strong>Created:</strong> {{account?.date_registered | date}}</span> <span class="ml-2"><strong>Created:</strong> {{account?.date_registered | date}}</span>
<span class="ml-2"><strong>Address:</strong><a href="{{bloxbergLink}}" target="_blank"> {{account?.identities.evm['bloxberg:8996']}} </a></span> <span class="ml-2"><strong>Address:</strong>
<a href="{{bloxbergLink}}" target="_blank"> {{accountAddress}} </a>
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress()" alt="Copy">
</span>
</div> </div>
</div> </div>
<div *ngIf="account" class="card mt-3 mb-3"> <div *ngIf="account" class="card mt-3 mb-3">
@ -75,11 +78,9 @@
<mat-label> ACCOUNT TYPE: </mat-label> <mat-label> ACCOUNT TYPE: </mat-label>
<mat-select id="accountType" [(value)]="account.type" formControlName="type" <mat-select id="accountType" [(value)]="account.type" formControlName="type"
[errorStateMatcher]="matcher"> [errorStateMatcher]="matcher">
<mat-option value="user"> USER </mat-option> <mat-option *ngFor="let accountType of accountTypes" [value]="accountType">
<mat-option value="cashier"> CASHIER </mat-option> {{accountType | uppercase}}
<mat-option value="vendor"> VENDOR </mat-option> </mat-option>
<mat-option value="tokenAgent"> TOKENAGENT </mat-option>
<mat-option value="group"> GROUPACCOUNT </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="submitted && accountInfoFormStub.type.errors">Type is required.</mat-error> <mat-error *ngIf="submitted && accountInfoFormStub.type.errors">Type is required.</mat-error>
</mat-form-field> </mat-form-field>
@ -99,9 +100,9 @@
<mat-label> GENDER: </mat-label> <mat-label> GENDER: </mat-label>
<mat-select id="gender" [(value)]="account.gender" formControlName="gender" <mat-select id="gender" [(value)]="account.gender" formControlName="gender"
[errorStateMatcher]="matcher"> [errorStateMatcher]="matcher">
<mat-option value="male"> MALE </mat-option> <mat-option *ngFor="let gender of genders" [value]="gender">
<mat-option value="female"> FEMALE </mat-option> {{gender | uppercase}}
<mat-option value="other"> OTHER </mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="submitted && accountInfoFormStub.gender.errors">Gender is required.</mat-error> <mat-error *ngIf="submitted && accountInfoFormStub.gender.errors">Gender is required.</mat-error>
</mat-form-field> </mat-form-field>
@ -112,16 +113,9 @@
<mat-label> BUSINESS CATEGORY: </mat-label> <mat-label> BUSINESS CATEGORY: </mat-label>
<mat-select id="businessCategory" [(value)]="account.category" formControlName="businessCategory" <mat-select id="businessCategory" [(value)]="account.category" formControlName="businessCategory"
[errorStateMatcher]="matcher"> [errorStateMatcher]="matcher">
<mat-option value="food/water">Food/Water</mat-option> <mat-option *ngFor="let category of categories" [value]="category">
<mat-option value="fuel/energy">Fuel/Energy</mat-option> {{category | titlecase}}
<mat-option value="education">Education</mat-option> </mat-option>
<mat-option value="health">Health</mat-option>
<mat-option value="shop">Shop</mat-option>
<mat-option value="environment">Environment</mat-option>
<mat-option value="transport">Transport</mat-option>
<mat-option value="farming/labour">Farming/Labour</mat-option>
<mat-option value="savings">Savings Group</mat-option>
<mat-option value="other">Savings Group</mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="submitted && accountInfoFormStub.businessCategory.errors"> <mat-error *ngIf="submitted && accountInfoFormStub.businessCategory.errors">
Category is required. Category is required.
@ -146,16 +140,9 @@
<mat-label> LOCATION: </mat-label> <mat-label> LOCATION: </mat-label>
<mat-select id="location" [(value)]="account.location.area" formControlName="location" <mat-select id="location" [(value)]="account.location.area" formControlName="location"
[errorStateMatcher]="matcher"> [errorStateMatcher]="matcher">
<div *ngFor="let county of locations; trackBy: trackByName"> <mat-option *ngFor="let area of areaNames" [value]="area">
<div *ngFor="let district of county.districts; trackBy: trackByName"> {{area | uppercase}}
<mat-optgroup *ngFor="let location of district.locations; trackBy: trackByName" [label]="county.name + ' / ' +
district.name + ' / ' + location.name">
<mat-option *ngFor="let village of location.villages; trackBy: trackByName" [value]="village">
{{village}}
</mat-option> </mat-option>
</mat-optgroup>
</div>
</div>
</mat-select> </mat-select>
<mat-error *ngIf="submitted && accountInfoFormStub.location.errors">Location is required.</mat-error> <mat-error *ngIf="submitted && accountInfoFormStub.location.errors">Location is required.</mat-error>
</mat-form-field> </mat-form-field>
@ -166,23 +153,22 @@
<mat-label> LOCATION TYPE: </mat-label> <mat-label> LOCATION TYPE: </mat-label>
<mat-select id="locationType" [(value)]="account.location.area_type" formControlName="locationType" <mat-select id="locationType" [(value)]="account.location.area_type" formControlName="locationType"
[errorStateMatcher]="matcher"> [errorStateMatcher]="matcher">
<mat-option value="Urban"> URBAN </mat-option> <mat-option *ngFor="let type of areaTypes" [value]="type">
<mat-option value="Periurban"> PERIURBAN </mat-option> {{type | uppercase}}
<mat-option value="Rural"> RURAL </mat-option> </mat-option>
<mat-option value="Other"> OTHER </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="submitted && accountInfoFormStub.locationType.errors">Location Type is required.</mat-error> <mat-error *ngIf="submitted && accountInfoFormStub.locationType.errors">Location Type is required.</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-md-6 col-lg-4"> <div class="col-md-6 col-lg-4">
<button mat-raised-button color="primary" type="button" class="btn btn btn-outline-primary mb-3"> <button mat-raised-button color="primary" type="button" class="btn btn-outline-primary mb-3">
Add User KYC Add User KYC
</button> </button>
</div> </div>
<div class="col-md-6 col-lg-4"> <div class="col-md-6 col-lg-4">
<button mat-raised-button color="primary" type="button" class="btn btn btn-outline-success mb-3" <button mat-raised-button color="primary" type="button" class="btn btn-outline-success mb-3"
(click)="resetPin()"> (click)="resetPin()">
Reset Pin Reset Pin
</button> </button>
@ -240,7 +226,7 @@
</div> </div>
</div> </div>
<mat-tab-group dynamicHeight mat-align-tabs="start"> <mat-tab-group *ngIf="account" dynamicHeight mat-align-tabs="start">
<mat-tab label="Transactions"> <mat-tab label="Transactions">
<app-transaction-details [transaction]="transaction"></app-transaction-details> <app-transaction-details [transaction]="transaction"></app-transaction-details>
<div class="card mt-1"> <div class="card mt-1">
@ -250,11 +236,9 @@
<mat-label> TRANSACTION TYPE </mat-label> <mat-label> TRANSACTION TYPE </mat-label>
<mat-select id="transferSelect" [(value)]="transactionsType" (selectionChange)="filterTransactions()"> <mat-select id="transferSelect" [(value)]="transactionsType" (selectionChange)="filterTransactions()">
<mat-option value="all">ALL TRANSFERS</mat-option> <mat-option value="all">ALL TRANSFERS</mat-option>
<mat-option value="transaction">PAYMENTS</mat-option> <mat-option *ngFor="let transactionType of transactionsTypes" [value]="transactionType">
<mat-option value="conversion">CONVERSION</mat-option> {{transactionType | uppercase}}
<mat-option value="disbursements">DISBURSEMENTS</mat-option> </mat-option>
<mat-option value="rewards">REWARDS</mat-option>
<mat-option value="reclamation">RECLAMATION</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv(transactions, 'transactions')"> EXPORT </button> <button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv(transactions, 'transactions')"> EXPORT </button>
@ -324,11 +308,9 @@
<mat-label> ACCOUNT TYPE </mat-label> <mat-label> ACCOUNT TYPE </mat-label>
<mat-select id="typeSelect" [(value)]="accountsType" (selectionChange)="filterAccounts()"> <mat-select id="typeSelect" [(value)]="accountsType" (selectionChange)="filterAccounts()">
<mat-option value="all">ALL</mat-option> <mat-option value="all">ALL</mat-option>
<mat-option value="user">USER</mat-option> <mat-option *ngFor="let accountType of accountTypes" [value]="accountType">
<mat-option value="cashier">CASHIER</mat-option> {{accountType | uppercase}}
<mat-option value="vendor">VENDOR</mat-option> </mat-option>
<mat-option value="tokenAgent">TOKENAGENT</mat-option>
<mat-option value="group">GROUPACCOUNT</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv(accounts, 'accounts')"> EXPORT </button> <button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv(accounts, 'accounts')"> EXPORT </button>

View File

@ -7,7 +7,13 @@ import {ActivatedRoute} from '@angular/router';
import { AccountsModule } from '@pages/accounts/accounts.module'; import { AccountsModule } from '@pages/accounts/accounts.module';
import { UserService } from '@app/_services'; import { UserService } from '@app/_services';
import { AppModule } from '@app/app.module'; import { AppModule } from '@app/app.module';
import {ActivatedRouteStub, FooterStubComponent, SidebarStubComponent, TopbarStubComponent, UserServiceStub} from '@src/testing'; import {
ActivatedRouteStub,
FooterStubComponent,
SidebarStubComponent,
TopbarStubComponent,
UserServiceStub,
} from '@src/testing';
describe('AccountDetailsComponent', () => { describe('AccountDetailsComponent', () => {
let component: AccountDetailsComponent; let component: AccountDetailsComponent;
@ -24,19 +30,14 @@ describe('AccountDetailsComponent', () => {
AccountDetailsComponent, AccountDetailsComponent,
FooterStubComponent, FooterStubComponent,
SidebarStubComponent, SidebarStubComponent,
TopbarStubComponent TopbarStubComponent,
],
imports: [
AccountsModule,
AppModule,
HttpClientTestingModule,
], ],
imports: [AccountsModule, AppModule, HttpClientTestingModule],
providers: [ providers: [
{ provide: ActivatedRoute, useValue: route }, { provide: ActivatedRoute, useValue: route },
{ provide: UserService, useClass: UserServiceStub } { provide: UserService, useClass: UserServiceStub },
] ],
}) }).compileComponents();
.compileComponents();
httpClient = TestBed.inject(HttpClient); httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController); httpTestingController = TestBed.inject(HttpTestingController);
}); });
@ -50,12 +51,4 @@ describe('AccountDetailsComponent', () => {
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('#addTransfer() should toggle #isDisbursing', () => {
expect(component.isDisbursing).toBe(false, 'off at first');
component.addTransfer();
expect(component.isDisbursing).toBe(true, 'on after click');
component.addTransfer();
expect(component.isDisbursing).toBe(false, 'off after second click');
});
}); });

View File

@ -1,49 +1,67 @@
import {ChangeDetectionStrategy, Component, OnInit, ViewChild} from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnInit,
ViewChild,
} from '@angular/core';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import {BlockSyncService, LocationService, LoggingService, TokenService, TransactionService, UserService} from '@app/_services'; import {
BlockSyncService,
LocationService,
LoggingService,
TokenService,
TransactionService,
UserService,
} from '@app/_services';
import { ActivatedRoute, Params, Router } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {CustomErrorStateMatcher, exportCsv} from '@app/_helpers'; import { copyToClipboard, CustomErrorStateMatcher, exportCsv } from '@app/_helpers';
import {Envelope, User} from 'cic-client-meta'; import { MatSnackBar } from '@angular/material/snack-bar';
const vCard = require('vcard-parser'); import { add0x, strip0x } from '@src/assets/js/ethtx/dist/hex';
import { environment } from '@src/environments/environment';
import { AccountDetails, AreaName, AreaType, Category, Transaction } from '@app/_models';
@Component({ @Component({
selector: 'app-account-details', selector: 'app-account-details',
templateUrl: './account-details.component.html', templateUrl: './account-details.component.html',
styleUrls: ['./account-details.component.scss'], styleUrls: ['./account-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AccountDetailsComponent implements OnInit { export class AccountDetailsComponent implements OnInit {
transactionsDataSource: MatTableDataSource<any>; transactionsDataSource: MatTableDataSource<any>;
transactionsDisplayedColumns = ['sender', 'recipient', 'value', 'created', 'type']; transactionsDisplayedColumns: Array<string> = ['sender', 'recipient', 'value', 'created', 'type'];
transactionsDefaultPageSize = 10; transactionsDefaultPageSize: number = 10;
transactionsPageSizeOptions = [10, 20, 50, 100]; transactionsPageSizeOptions: Array<number> = [10, 20, 50, 100];
@ViewChild('TransactionTablePaginator', { static: true }) transactionTablePaginator: MatPaginator; @ViewChild('TransactionTablePaginator', { static: true }) transactionTablePaginator: MatPaginator;
@ViewChild('TransactionTableSort', { static: true }) transactionTableSort: MatSort; @ViewChild('TransactionTableSort', { static: true }) transactionTableSort: MatSort;
userDataSource: MatTableDataSource<any>; userDataSource: MatTableDataSource<any>;
userDisplayedColumns = ['name', 'phone', 'created', 'balance', 'location']; userDisplayedColumns: Array<string> = ['name', 'phone', 'created', 'balance', 'location'];
usersDefaultPageSize = 10; usersDefaultPageSize: number = 10;
usersPageSizeOptions = [10, 20, 50, 100]; usersPageSizeOptions: Array<number> = [10, 20, 50, 100];
@ViewChild('UserTablePaginator', { static: true }) userTablePaginator: MatPaginator; @ViewChild('UserTablePaginator', { static: true }) userTablePaginator: MatPaginator;
@ViewChild('UserTableSort', { static: true }) userTableSort: MatSort; @ViewChild('UserTableSort', { static: true }) userTableSort: MatSort;
accountInfoForm: FormGroup; accountInfoForm: FormGroup;
account: any; account: AccountDetails;
accountAddress: string; accountAddress: string;
accountBalance: number;
accountStatus: any; accountStatus: any;
metaAccount: any; accounts: Array<AccountDetails> = [];
accounts: any[] = []; accountsType: string = 'all';
accountsType = 'all'; categories: Array<Category>;
locations: any; areaNames: Array<AreaName>;
areaTypes: Array<AreaType>;
transaction: any; transaction: any;
transactions: any[]; transactions: Array<Transaction>;
transactionsType = 'all'; transactionsType: string = 'all';
matcher = new CustomErrorStateMatcher(); accountTypes: Array<string>;
transactionsTypes: Array<string>;
genders: Array<string>;
matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
submitted: boolean = false; submitted: boolean = false;
bloxbergLink: string; bloxbergLink: string;
@ -56,7 +74,9 @@ export class AccountDetailsComponent implements OnInit {
private router: Router, private router: Router,
private tokenService: TokenService, private tokenService: TokenService,
private loggingService: LoggingService, private loggingService: LoggingService,
private blockSyncService: BlockSyncService private blockSyncService: BlockSyncService,
private cdr: ChangeDetectorRef,
private snackBar: MatSnackBar
) { ) {
this.accountInfoForm = this.formBuilder.group({ this.accountInfoForm = this.formBuilder.group({
name: ['', Validators.required], name: ['', Validators.required],
@ -71,15 +91,17 @@ export class AccountDetailsComponent implements OnInit {
locationType: ['', Validators.required], locationType: ['', Validators.required],
}); });
this.route.paramMap.subscribe(async (params: Params) => { this.route.paramMap.subscribe(async (params: Params) => {
this.accountAddress = params.get('id'); this.accountAddress = add0x(params.get('id'));
this.bloxbergLink = 'https://blockexplorer.bloxberg.org/address/' + this.accountAddress + '/transactions'; this.bloxbergLink =
this.userService.getAccountDetailsFromMeta(await User.toKey(this.accountAddress)).pipe(first()).subscribe(async res => { 'https://blockexplorer.bloxberg.org/address/' + this.accountAddress + '/transactions';
this.metaAccount = Envelope.fromJSON(JSON.stringify(res.body)).unwrap(); (await this.userService.getAccountByAddress(this.accountAddress, 100)).subscribe(
this.account = this.metaAccount.m.data; async (res) => {
if (res !== undefined) {
this.account = res;
this.cdr.detectChanges();
this.loggingService.sendInfoLevelMessage(this.account); this.loggingService.sendInfoLevelMessage(this.account);
this.accountBalance = await this.tokenService.getTokenBalance(this.accountAddress); // this.userService.getAccountStatus(this.account.vcard?.tel[0].value).pipe(first())
this.account.vcard = vCard.parse(atob(this.account.vcard)); // .subscribe(response => this.accountStatus = response);
this.userService.getAccountStatus(this.account.vcard?.tel[0].value).pipe(first()).subscribe(response => this.accountStatus = response);
this.accountInfoForm.patchValue({ this.accountInfoForm.patchValue({
name: this.account.vcard?.fn[0].value, name: this.account.vcard?.fn[0].value,
phoneNumber: this.account.vcard?.tel[0].value, phoneNumber: this.account.vcard?.tel[0].value,
@ -92,25 +114,48 @@ export class AccountDetailsComponent implements OnInit {
location: this.account.location.area, location: this.account.location.area,
locationType: this.account.location.area_type, locationType: this.account.location.area_type,
}); });
}); } else {
alert('Account not found!');
}
}
);
this.blockSyncService.blockSync(this.accountAddress); this.blockSyncService.blockSync(this.accountAddress);
}); });
this.userService.getAccounts(); this.userService
this.locationService.getLocations(); .getCategories()
this.locationService.locationsSubject.subscribe(locations => { .pipe(first())
this.locations = locations; .subscribe((res) => (this.categories = res));
}); this.locationService
.getAreaNames()
.pipe(first())
.subscribe((res) => (this.areaNames = res));
this.locationService
.getAreaTypes()
.pipe(first())
.subscribe((res) => (this.areaTypes = res));
this.userService
.getAccountTypes()
.pipe(first())
.subscribe((res) => (this.accountTypes = res));
this.userService
.getTransactionTypes()
.pipe(first())
.subscribe((res) => (this.transactionsTypes = res));
this.userService
.getGenders()
.pipe(first())
.subscribe((res) => (this.genders = res));
} }
ngOnInit(): void { ngOnInit(): void {
this.userService.accountsSubject.subscribe(accounts => { this.userService.accountsSubject.subscribe((accounts) => {
this.userDataSource = new MatTableDataSource<any>(accounts); this.userDataSource = new MatTableDataSource<any>(accounts);
this.userDataSource.paginator = this.userTablePaginator; this.userDataSource.paginator = this.userTablePaginator;
this.userDataSource.sort = this.userTableSort; this.userDataSource.sort = this.userTableSort;
this.accounts = accounts; this.accounts = accounts;
}); });
this.transactionService.transactionsSubject.subscribe(transactions => { this.transactionService.transactionsSubject.subscribe((transactions) => {
this.transactionsDataSource = new MatTableDataSource<any>(transactions); this.transactionsDataSource = new MatTableDataSource<any>(transactions);
this.transactionsDataSource.paginator = this.transactionTablePaginator; this.transactionsDataSource.paginator = this.transactionTablePaginator;
this.transactionsDataSource.sort = this.transactionTableSort; this.transactionsDataSource.sort = this.transactionTableSort;
@ -131,16 +176,22 @@ export class AccountDetailsComponent implements OnInit {
} }
viewAccount(account): void { viewAccount(account): void {
this.router.navigateByUrl(`/accounts/${account.id}`); this.router.navigateByUrl(
`/accounts/${strip0x(account.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0])}`
);
} }
get accountInfoFormStub(): any { return this.accountInfoForm.controls; } get accountInfoFormStub(): any {
return this.accountInfoForm.controls;
}
async saveInfo(): Promise<void> { async saveInfo(): Promise<void> {
this.submitted = true; this.submitted = true;
if (this.accountInfoForm.invalid || !confirm('Change user\'s profile information?')) { return; } if (this.accountInfoForm.invalid || !confirm(`Change user's profile information?`)) {
return;
}
const accountKey = await this.userService.changeAccountInfo( const accountKey = await this.userService.changeAccountInfo(
this.account.address, this.accountAddress,
this.accountInfoFormStub.name.value, this.accountInfoFormStub.name.value,
this.accountInfoFormStub.phoneNumber.value, this.accountInfoFormStub.phoneNumber.value,
this.accountInfoFormStub.age.value, this.accountInfoFormStub.age.value,
@ -150,47 +201,58 @@ export class AccountDetailsComponent implements OnInit {
this.accountInfoFormStub.businessCategory.value, this.accountInfoFormStub.businessCategory.value,
this.accountInfoFormStub.userLocation.value, this.accountInfoFormStub.userLocation.value,
this.accountInfoFormStub.location.value, this.accountInfoFormStub.location.value,
this.accountInfoFormStub.locationType.value, this.accountInfoFormStub.locationType.value
this.metaAccount
); );
this.loggingService.sendInfoLevelMessage(`Response: ${accountKey}`);
this.submitted = false; this.submitted = false;
} }
filterAccounts(): void { filterAccounts(): void {
if (this.accountsType === 'all') { if (this.accountsType === 'all') {
this.userService.accountsSubject.subscribe(accounts => { this.userService.accountsSubject.subscribe((accounts) => {
this.userDataSource.data = accounts; this.userDataSource.data = accounts;
this.accounts = accounts; this.accounts = accounts;
}); });
} else { } else {
this.userDataSource.data = this.accounts.filter(account => account.type === this.accountsType); this.userDataSource.data = this.accounts.filter(
(account) => account.type === this.accountsType
);
} }
} }
filterTransactions(): void { filterTransactions(): void {
if (this.transactionsType === 'all') { if (this.transactionsType === 'all') {
this.transactionService.transactionsSubject.subscribe(transactions => { this.transactionService.transactionsSubject.subscribe((transactions) => {
this.transactionsDataSource.data = transactions; this.transactionsDataSource.data = transactions;
this.transactions = transactions; this.transactions = transactions;
}); });
} else { } else {
this.transactionsDataSource.data = this.transactions.filter(transaction => transaction.type === this.transactionsType); this.transactionsDataSource.data = this.transactions.filter(
(transaction) => transaction.type === this.transactionsType
);
} }
} }
resetPin(): void { resetPin(): void {
if (!confirm('Reset user\'s pin?')) { return; } if (!confirm(`Reset user's pin?`)) {
this.userService.resetPin(this.account.phone).pipe(first()).subscribe(res => { return;
}
this.userService
.resetPin(this.account.vcard.tel[0].value)
.pipe(first())
.subscribe((res) => {
this.loggingService.sendInfoLevelMessage(`Response: ${res}`); this.loggingService.sendInfoLevelMessage(`Response: ${res}`);
}); });
} }
public trackByName(index, item): string {
return item.name;
}
downloadCsv(data: any, filename: string): void { downloadCsv(data: any, filename: string): void {
exportCsv(data, filename); exportCsv(data, filename);
} }
copyAddress(): void {
if (copyToClipboard(this.accountAddress)) {
this.snackBar.open(this.accountAddress + ' copied successfully!', 'Close', {
duration: 3000,
});
}
}
} }

View File

@ -0,0 +1,59 @@
<!-- Begin page -->
<div class="wrapper">
<app-sidebar></app-sidebar>
<!-- ============================================================== -->
<!-- Start Page Content here -->
<!-- ============================================================== -->
<div id="content">
<app-topbar></app-topbar>
<!-- Start Content-->
<div class="container-fluid" appMenuSelection>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li>
<li class="breadcrumb-item"><a routerLink="/accounts">Accounts</a></li>
<li class="breadcrumb-item active" aria-current="page">Search</li>
</ol>
</nav>
<div class="card">
<mat-card-title class="card-header">
Accounts
</mat-card-title>
<div class="card-body">
<mat-tab-group>
<mat-tab label="Phone Number">
<form [formGroup]="phoneSearchForm" (ngSubmit)="onPhoneSearch()">
<mat-form-field appearance="outline">
<mat-label> Search </mat-label>
<input matInput type="text" placeholder="Search by phone number" formControlName="phoneNumber" [errorStateMatcher]="matcher">
<mat-error *ngIf="phoneSearchSubmitted && phoneSearchFormStub.phoneNumber.errors">Phone Number is required.</mat-error>
<mat-icon matSuffix>phone</mat-icon>
<mat-hint>Phone Number</mat-hint>
</mat-form-field>
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary ml-3"> SEARCH </button>
</form>
</mat-tab>
<mat-tab label="Account Address">
<form [formGroup]="addressSearchForm" (ngSubmit)="onAddressSearch()">
<mat-form-field appearance="outline">
<mat-label> Search </mat-label>
<input matInput type="text" placeholder="Search by account address" formControlName="address" [errorStateMatcher]="matcher">
<mat-error *ngIf="addressSearchSubmitted && addressSearchFormStub.address.errors">Account Address is required.</mat-error>
<mat-icon matSuffix>view_in_ar</mat-icon>
<mat-hint>Account Address</mat-hint>
</mat-form-field>
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary ml-3"> SEARCH </button>
</form>
</mat-tab>
</mat-tab-group>
</div>
</div>
</div>
<app-footer appMenuSelection></app-footer>
</div>
<!-- ============================================================== -->
<!-- End Page content -->
<!-- ============================================================== -->
</div>

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AccountSearchComponent } from './account-search.component';
describe('AccountSearchComponent', () => {
let component: AccountSearchComponent;
let fixture: ComponentFixture<AccountSearchComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AccountSearchComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AccountSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,104 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CustomErrorStateMatcher } from '@app/_helpers';
import { UserService } from '@app/_services';
import { Router } from '@angular/router';
import { strip0x } from '@src/assets/js/ethtx/dist/hex';
import { environment } from '@src/environments/environment';
@Component({
selector: 'app-account-search',
templateUrl: './account-search.component.html',
styleUrls: ['./account-search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccountSearchComponent implements OnInit {
nameSearchForm: FormGroup;
nameSearchSubmitted: boolean = false;
nameSearchLoading: boolean = false;
phoneSearchForm: FormGroup;
phoneSearchSubmitted: boolean = false;
phoneSearchLoading: boolean = false;
addressSearchForm: FormGroup;
addressSearchSubmitted: boolean = false;
addressSearchLoading: boolean = false;
matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
constructor(
private formBuilder: FormBuilder,
private userService: UserService,
private router: Router
) {}
ngOnInit(): void {
this.nameSearchForm = this.formBuilder.group({
name: ['', Validators.required],
});
this.phoneSearchForm = this.formBuilder.group({
phoneNumber: ['', Validators.required],
});
this.addressSearchForm = this.formBuilder.group({
address: ['', Validators.required],
});
}
get nameSearchFormStub(): any {
return this.nameSearchForm.controls;
}
get phoneSearchFormStub(): any {
return this.phoneSearchForm.controls;
}
get addressSearchFormStub(): any {
return this.addressSearchForm.controls;
}
onNameSearch(): void {
this.nameSearchSubmitted = true;
if (this.nameSearchForm.invalid) {
return;
}
this.nameSearchLoading = true;
this.userService.searchAccountByName(this.nameSearchFormStub.name.value);
this.nameSearchLoading = false;
}
async onPhoneSearch(): Promise<void> {
this.phoneSearchSubmitted = true;
if (this.phoneSearchForm.invalid) {
return;
}
this.phoneSearchLoading = true;
(
await this.userService.getAccountByPhone(this.phoneSearchFormStub.phoneNumber.value, 100)
).subscribe(async (res) => {
if (res !== undefined) {
await this.router.navigateByUrl(
`/accounts/${strip0x(res.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0])}`
);
} else {
alert('Account not found!');
}
});
this.phoneSearchLoading = false;
}
async onAddressSearch(): Promise<void> {
this.addressSearchSubmitted = true;
if (this.addressSearchForm.invalid) {
return;
}
this.addressSearchLoading = true;
(
await this.userService.getAccountByAddress(this.addressSearchFormStub.address.value, 100)
).subscribe(async (res) => {
if (res !== undefined) {
await this.router.navigateByUrl(
`/accounts/${strip0x(res.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0])}`
);
} else {
alert('Account not found!');
}
});
this.addressSearchLoading = false;
}
}

View File

@ -3,19 +3,19 @@ import { Routes, RouterModule } from '@angular/router';
import { AccountsComponent } from '@pages/accounts/accounts.component'; import { AccountsComponent } from '@pages/accounts/accounts.component';
import { CreateAccountComponent } from '@pages/accounts/create-account/create-account.component'; import { CreateAccountComponent } from '@pages/accounts/create-account/create-account.component';
import {ExportAccountsComponent} from '@pages/accounts/export-accounts/export-accounts.component';
import { AccountDetailsComponent } from '@pages/accounts/account-details/account-details.component'; import { AccountDetailsComponent } from '@pages/accounts/account-details/account-details.component';
import { AccountSearchComponent } from '@pages/accounts/account-search/account-search.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: AccountsComponent }, { path: '', component: AccountsComponent },
{ path: 'search', component: AccountSearchComponent },
// { path: 'create', component: CreateAccountComponent }, // { path: 'create', component: CreateAccountComponent },
{ path: 'export', component: ExportAccountsComponent },
{ path: ':id', component: AccountDetailsComponent }, { path: ':id', component: AccountDetailsComponent },
{ path: '**', redirectTo: '', pathMatch: 'full' } { path: '**', redirectTo: '', pathMatch: 'full' },
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class AccountsRoutingModule {} export class AccountsRoutingModule {}

View File

@ -26,14 +26,13 @@
<mat-label> ACCOUNT TYPE </mat-label> <mat-label> ACCOUNT TYPE </mat-label>
<mat-select id="typeSelect" [(value)]="accountsType" (selectionChange)="filterAccounts()"> <mat-select id="typeSelect" [(value)]="accountsType" (selectionChange)="filterAccounts()">
<mat-option value="all">ALL</mat-option> <mat-option value="all">ALL</mat-option>
<mat-option value="user">USER</mat-option> <mat-option *ngFor="let accountType of accountTypes" [value]="accountType">
<mat-option value="cashier">CASHIER</mat-option> {{accountType | uppercase}}
<mat-option value="vendor">VENDOR</mat-option> </mat-option>
<mat-option value="tokenAgent">TOKENAGENT</mat-option>
<mat-option value="group">GROUPACCOUNT</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv()"> EXPORT </button> <button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" routerLink="/accounts/search"> SEARCH </button>
<button mat-raised-button color="primary" type="button" class="btn btn-outline-primary mr-2" (click)="downloadCsv()"> EXPORT </button>
</div> </div>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">

View File

@ -1,7 +1,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AccountsComponent } from './accounts.component'; import { AccountsComponent } from './accounts.component';
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent, UserServiceStub} from '@src/testing'; import {
FooterStubComponent,
SidebarStubComponent,
TopbarStubComponent,
UserServiceStub,
} from '@src/testing';
import { AccountsModule } from '@pages/accounts/accounts.module'; import { AccountsModule } from '@pages/accounts/accounts.module';
import { AppModule } from '@app/app.module'; import { AppModule } from '@app/app.module';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
@ -20,18 +25,11 @@ describe('AccountsComponent', () => {
AccountsComponent, AccountsComponent,
FooterStubComponent, FooterStubComponent,
SidebarStubComponent, SidebarStubComponent,
TopbarStubComponent TopbarStubComponent,
], ],
imports: [ imports: [AccountsModule, AppModule, HttpClientTestingModule],
AccountsModule, providers: [{ provide: UserService, useClass: UserServiceStub }],
AppModule, }).compileComponents();
HttpClientTestingModule,
],
providers: [
{ provide: UserService, useClass: UserServiceStub }
]
})
.compileComponents();
httpClient = TestBed.inject(HttpClient); httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController); httpTestingController = TestBed.inject(HttpTestingController);
}); });

View File

@ -5,20 +5,25 @@ import {MatSort} from '@angular/material/sort';
import { LoggingService, UserService } from '@app/_services'; import { LoggingService, UserService } from '@app/_services';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { exportCsv } from '@app/_helpers'; import { exportCsv } from '@app/_helpers';
import { strip0x } from '@src/assets/js/ethtx/dist/hex';
import { first } from 'rxjs/operators';
import { environment } from '@src/environments/environment';
import { AccountDetails } from '@app/_models';
@Component({ @Component({
selector: 'app-accounts', selector: 'app-accounts',
templateUrl: './accounts.component.html', templateUrl: './accounts.component.html',
styleUrls: ['./accounts.component.scss'], styleUrls: ['./accounts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AccountsComponent implements OnInit { export class AccountsComponent implements OnInit {
dataSource: MatTableDataSource<any>; dataSource: MatTableDataSource<any>;
accounts: any[] = []; accounts: Array<AccountDetails> = [];
displayedColumns = ['name', 'phone', 'created', 'balance', 'location']; displayedColumns: Array<string> = ['name', 'phone', 'created', 'balance', 'location'];
defaultPageSize = 10; defaultPageSize: number = 10;
pageSizeOptions = [10, 20, 50, 100]; pageSizeOptions: Array<number> = [10, 20, 50, 100];
accountsType = 'all'; accountsType: string = 'all';
accountTypes: Array<string>;
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ -30,15 +35,20 @@ export class AccountsComponent implements OnInit {
) { ) {
(async () => { (async () => {
try { try {
// TODO it feels like this should be in the onInit handler
await this.userService.loadAccounts(100); await this.userService.loadAccounts(100);
} catch (error) { } catch (error) {
this.loggingService.sendErrorLevelMessage('Failed to load accounts', this, { error }); this.loggingService.sendErrorLevelMessage('Failed to load accounts', this, { error });
} }
})(); })();
this.userService
.getAccountTypes()
.pipe(first())
.subscribe((res) => (this.accountTypes = res));
} }
ngOnInit(): void { ngOnInit(): void {
this.userService.accountsSubject.subscribe(accounts => { this.userService.accountsSubject.subscribe((accounts) => {
this.dataSource = new MatTableDataSource<any>(accounts); this.dataSource = new MatTableDataSource<any>(accounts);
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
@ -51,17 +61,19 @@ export class AccountsComponent implements OnInit {
} }
async viewAccount(account): Promise<void> { async viewAccount(account): Promise<void> {
await this.router.navigateByUrl(`/accounts/${account.identities.evm['bloxberg:8996']}`); await this.router.navigateByUrl(
`/accounts/${strip0x(account.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0])}`
);
} }
filterAccounts(): void { filterAccounts(): void {
if (this.accountsType === 'all') { if (this.accountsType === 'all') {
this.userService.accountsSubject.subscribe(accounts => { this.userService.accountsSubject.subscribe((accounts) => {
this.dataSource.data = accounts; this.dataSource.data = accounts;
this.accounts = accounts; this.accounts = accounts;
}); });
} else { } else {
this.dataSource.data = this.accounts.filter(account => account.type === this.accountsType); this.dataSource.data = this.accounts.filter((account) => account.type === this.accountsType);
} }
} }

View File

@ -5,10 +5,7 @@ import { AccountsRoutingModule } from '@pages/accounts/accounts-routing.module';
import { AccountsComponent } from '@pages/accounts/accounts.component'; import { AccountsComponent } from '@pages/accounts/accounts.component';
import { SharedModule } from '@app/shared/shared.module'; import { SharedModule } from '@app/shared/shared.module';
import { AccountDetailsComponent } from '@pages/accounts/account-details/account-details.component'; import { AccountDetailsComponent } from '@pages/accounts/account-details/account-details.component';
import {DataTablesModule} from 'angular-datatables';
import { CreateAccountComponent } from '@pages/accounts/create-account/create-account.component'; import { CreateAccountComponent } from '@pages/accounts/create-account/create-account.component';
import { DisbursementComponent } from '@pages/accounts/disbursement/disbursement.component';
import { ExportAccountsComponent } from '@pages/accounts/export-accounts/export-accounts.component';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@ -24,15 +21,20 @@ import {MatTabsModule} from '@angular/material/tabs';
import { MatRippleModule } from '@angular/material/core'; import { MatRippleModule } from '@angular/material/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { AccountSearchComponent } from './account-search/account-search.component';
import { MatSnackBarModule } from '@angular/material/snack-bar';
@NgModule({ @NgModule({
declarations: [AccountsComponent, AccountDetailsComponent, CreateAccountComponent, DisbursementComponent, ExportAccountsComponent], declarations: [
AccountsComponent,
AccountDetailsComponent,
CreateAccountComponent,
AccountSearchComponent,
],
imports: [ imports: [
CommonModule, CommonModule,
AccountsRoutingModule, AccountsRoutingModule,
SharedModule, SharedModule,
DataTablesModule,
MatTableModule, MatTableModule,
MatSortModule, MatSortModule,
MatCheckboxModule, MatCheckboxModule,
@ -47,7 +49,8 @@ import {ReactiveFormsModule} from '@angular/forms';
MatTabsModule, MatTabsModule,
MatRippleModule, MatRippleModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
ReactiveFormsModule ReactiveFormsModule,
] MatSnackBarModule,
],
}) })
export class AccountsModule {} export class AccountsModule {}

View File

@ -27,11 +27,9 @@
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Account Type: </mat-label> <mat-label>Account Type: </mat-label>
<mat-select id="accountType" formControlName="accountType" [errorStateMatcher]="matcher"> <mat-select id="accountType" formControlName="accountType" [errorStateMatcher]="matcher">
<mat-option value="user">USER</mat-option> <mat-option *ngFor="let accountType of accountTypes" [value]="accountType">
<mat-option value="cashier">CASHIER</mat-option> {{accountType | uppercase}}
<mat-option value="vendor">VENDOR</mat-option> </mat-option>
<mat-option value="tokenAgent">TOKENAGENT</mat-option>
<mat-option value="group">GROUPACCOUNT</mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="submitted && createFormStub.accountType.errors">Account type is required.</mat-error> <mat-error *ngIf="submitted && createFormStub.accountType.errors">Account type is required.</mat-error>
</mat-form-field><br> </mat-form-field><br>
@ -81,15 +79,9 @@
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Location: </mat-label> <mat-label>Location: </mat-label>
<mat-select id="location" formControlName="location" [errorStateMatcher]="matcher"> <mat-select id="location" formControlName="location" [errorStateMatcher]="matcher">
<div *ngFor="let county of locations; trackBy: trackByName"> <mat-option *ngFor="let area of areaNames" [value]="area">
<div *ngFor="let district of county.districts; trackBy: trackByName"> {{area | uppercase}}
<mat-optgroup *ngFor="let location of district.locations; trackBy: trackByName" [label]="county.name + ' / ' + district.name + ' / ' + location.name">
<mat-option *ngFor="let village of location.villages; trackBy: trackByName" [value]="village">
{{village}}
</mat-option> </mat-option>
</mat-optgroup>
</div>
</div>
</mat-select> </mat-select>
<mat-error *ngIf="submitted && createFormStub.location.errors">Location is required.</mat-error> <mat-error *ngIf="submitted && createFormStub.location.errors">Location is required.</mat-error>
</mat-form-field><br> </mat-form-field><br>
@ -99,9 +91,9 @@
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Gender: </mat-label> <mat-label>Gender: </mat-label>
<mat-select id="gender" formControlName="gender" [errorStateMatcher]="matcher"> <mat-select id="gender" formControlName="gender" [errorStateMatcher]="matcher">
<mat-option value="female">FEMALE</mat-option> <mat-option *ngFor="let gender of genders" [value]="gender">
<mat-option value="male">MALE</mat-option> {{gender | uppercase}}
<mat-option value="other">OTHER</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="submitted && createFormStub.gender.errors">Gender is required.</mat-error> <mat-error *ngIf="submitted && createFormStub.gender.errors">Gender is required.</mat-error>
</mat-form-field><br> </mat-form-field><br>
@ -119,16 +111,9 @@
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Business Category: </mat-label> <mat-label>Business Category: </mat-label>
<mat-select id="businessCategory" formControlName="businessCategory" [errorStateMatcher]="matcher"> <mat-select id="businessCategory" formControlName="businessCategory" [errorStateMatcher]="matcher">
<mat-option value="food/water">Food/Water</mat-option> <mat-option *ngFor="let category of categories" [value]="category">
<mat-option value="fuel/energy">Fuel/Energy</mat-option> {{category | titlecase}}
<mat-option value="education">Education</mat-option> </mat-option>
<mat-option value="health">Health</mat-option>
<mat-option value="shop">Shop</mat-option>
<mat-option value="environment">Environment</mat-option>
<mat-option value="transport">Transport</mat-option>
<mat-option value="farming/labour">Farming/Labour</mat-option>
<mat-option value="savings">Savings Group</mat-option>
<mat-option value="other">Other</mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="submitted && createFormStub.businessCategory.errors">Business Category is required.</mat-error> <mat-error *ngIf="submitted && createFormStub.businessCategory.errors">Business Category is required.</mat-error>
</mat-form-field> </mat-form-field>

View File

@ -5,7 +5,6 @@ import {AccountsModule} from '@pages/accounts/accounts.module';
import { AppModule } from '@app/app.module'; import { AppModule } from '@app/app.module';
import { FooterStubComponent, SidebarStubComponent, TopbarStubComponent } from '@src/testing'; import { FooterStubComponent, SidebarStubComponent, TopbarStubComponent } from '@src/testing';
describe('CreateAccountComponent', () => { describe('CreateAccountComponent', () => {
let component: CreateAccountComponent; let component: CreateAccountComponent;
let fixture: ComponentFixture<CreateAccountComponent>; let fixture: ComponentFixture<CreateAccountComponent>;
@ -16,14 +15,10 @@ describe('CreateAccountComponent', () => {
CreateAccountComponent, CreateAccountComponent,
FooterStubComponent, FooterStubComponent,
SidebarStubComponent, SidebarStubComponent,
TopbarStubComponent TopbarStubComponent,
], ],
imports: [ imports: [AccountsModule, AppModule],
AccountsModule, }).compileComponents();
AppModule
]
})
.compileComponents();
}); });
beforeEach(() => { beforeEach(() => {

View File

@ -1,23 +1,29 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {LocationService} from '@app/_services'; import { LocationService, UserService } from '@app/_services';
import { CustomErrorStateMatcher } from '@app/_helpers'; import { CustomErrorStateMatcher } from '@app/_helpers';
import { first } from 'rxjs/operators';
import { AreaName, Category } from '@app/_models';
@Component({ @Component({
selector: 'app-create-account', selector: 'app-create-account',
templateUrl: './create-account.component.html', templateUrl: './create-account.component.html',
styleUrls: ['./create-account.component.scss'], styleUrls: ['./create-account.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class CreateAccountComponent implements OnInit { export class CreateAccountComponent implements OnInit {
createForm: FormGroup; createForm: FormGroup;
matcher = new CustomErrorStateMatcher(); matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
submitted: boolean = false; submitted: boolean = false;
locations: any; categories: Array<Category>;
areaNames: Array<AreaName>;
accountTypes: Array<string>;
genders: Array<string>;
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private locationService: LocationService private locationService: LocationService,
private userService: UserService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@ -31,23 +37,35 @@ export class CreateAccountComponent implements OnInit {
location: ['', Validators.required], location: ['', Validators.required],
gender: ['', Validators.required], gender: ['', Validators.required],
referrer: ['', Validators.required], referrer: ['', Validators.required],
businessCategory: ['', Validators.required] businessCategory: ['', Validators.required],
});
this.locationService.getLocations();
this.locationService.locationsSubject.subscribe(locations => {
this.locations = locations;
}); });
this.userService
.getCategories()
.pipe(first())
.subscribe((res) => (this.categories = res));
this.locationService
.getAreaNames()
.pipe(first())
.subscribe((res) => (this.areaNames = res));
this.userService
.getAccountTypes()
.pipe(first())
.subscribe((res) => (this.accountTypes = res));
this.userService
.getGenders()
.pipe(first())
.subscribe((res) => (this.genders = res));
} }
get createFormStub(): any { return this.createForm.controls; } get createFormStub(): any {
return this.createForm.controls;
}
onSubmit(): void { onSubmit(): void {
this.submitted = true; this.submitted = true;
if (this.createForm.invalid || !confirm('Create account?')) { return; } if (this.createForm.invalid || !confirm('Create account?')) {
return;
}
this.submitted = false; this.submitted = false;
} }
public trackByName(index, item): string {
return item.name;
}
} }

View File

@ -1,36 +0,0 @@
<div class="card">
<mat-card-title class="card-header">
<div class="row">
NEW TRANSFER
<button mat-raised-button color="warn" type="button" class="btn btn-outline-danger ml-auto mr-2" (click)="cancel()"> CANCEL </button>
</div>
</mat-card-title>
<div class="card-body">
<form [formGroup]="disbursementForm" (ngSubmit)="createTransfer()">
<div class="row form-inline">
<mat-form-field appearance="outline">
<mat-label> TRANSACTION TYPE </mat-label>
<mat-select id="transactionType" formControlName="transactionType" [errorStateMatcher]="matcher">
<mat-option value="disbursement">DISBURSEMENT</mat-option>
<mat-option value="transfer">TRANSFER</mat-option>
<mat-option value="deposit">DEPOSIT</mat-option>
<mat-option value="reclamation">RECLAMATION</mat-option>
</mat-select>
<mat-error *ngIf="submitted && disbursementFormStub.transactionType.errors">Transaction type is required.</mat-error>
</mat-form-field>
<mat-form-field *ngIf="disbursementFormStub.transactionType.value === 'transfer'" appearance="outline" class="ml-3">
<mat-label>Enter Recipient: </mat-label>
<input matInput type="text" id="recipient" placeholder="Recipient" formControlName="recipient">
</mat-form-field>
<mat-form-field appearance="outline" class="ml-3">
<mat-label>Enter Amount: </mat-label>
<input matInput type="text" id="amount" placeholder="Amount" formControlName="amount" [errorStateMatcher]="matcher">
<mat-error *ngIf="submitted && disbursementFormStub.amount.errors">Amount is required.</mat-error>
</mat-form-field>
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary ml-3" style="margin-bottom: 1.2rem;">
CREATE TRANSFER
</button>
</div>
</form>
</div>
</div>

View File

@ -1,37 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DisbursementComponent } from '@pages/accounts/disbursement/disbursement.component';
import {AccountsModule} from '@pages/accounts/accounts.module';
import {AppModule} from '@app/app.module';
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent} from '@src/testing';
describe('DisbursementComponent', () => {
let component: DisbursementComponent;
let fixture: ComponentFixture<DisbursementComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
DisbursementComponent,
FooterStubComponent,
SidebarStubComponent,
TopbarStubComponent
],
imports: [
AccountsModule,
AppModule
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DisbursementComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,51 +0,0 @@
import {Component, OnInit, EventEmitter, Output, Input, ChangeDetectionStrategy} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {CustomErrorStateMatcher} from '@app/_helpers';
import {TransactionService} from '@app/_services';
@Component({
selector: 'app-disbursement',
templateUrl: './disbursement.component.html',
styleUrls: ['./disbursement.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DisbursementComponent implements OnInit {
@Input() account;
@Output() cancelDisbursmentEvent = new EventEmitter();
disbursementForm: FormGroup;
matcher = new CustomErrorStateMatcher();
submitted: boolean = false;
constructor(
private formBuilder: FormBuilder,
private transactionService: TransactionService
) { }
ngOnInit(): void {
this.disbursementForm = this.formBuilder.group({
transactionType: ['', Validators.required],
recipient: '',
amount: ['', Validators.required]
});
}
get disbursementFormStub(): any { return this.disbursementForm.controls; }
async createTransfer(): Promise<void> {
this.submitted = true;
if (this.disbursementForm.invalid || !confirm('Make transfer?')) { return; }
if (this.disbursementFormStub.transactionType.value === 'transfer') {
await this.transactionService.transferRequest(
this.account.token,
this.account.address,
this.disbursementFormStub.recipient.value,
this.disbursementFormStub.amount.value
);
}
this.submitted = false;
}
cancel(): void {
this.cancelDisbursmentEvent.emit();
}
}

View File

@ -1,53 +0,0 @@
<!-- Begin page -->
<div class="wrapper">
<app-sidebar></app-sidebar>
<!-- ============================================================== -->
<!-- Start Page Content here -->
<!-- ============================================================== -->
<div id="content">
<app-topbar></app-topbar>
<!-- Start Content-->
<div class="container-fluid" appMenuSelection>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li>
<li class="breadcrumb-item"><a routerLink="/accounts">Accounts</a></li>
<li class="breadcrumb-item active" aria-current="page">Export Accounts</li>
</ol>
</nav>
<div class="card">
<mat-card-title class="card-header">
EXPORT ACCOUNTS
</mat-card-title>
<div class="card-body">
<form [formGroup]="exportForm" (ngSubmit)="export()">
<div class="form-inline mb-2">
<mat-form-field appearance="outline">
<mat-label>Export : </mat-label>
<mat-select id="accountType" formControlName="accountType" [errorStateMatcher]="matcher">
<mat-option value="vendors">VENDORS</mat-option>
<mat-option value="partners">PARTNERS</mat-option>
<mat-option value="selected">SELECTED</mat-option>
</mat-select>
<mat-error *ngIf="submitted && exportFormStub.accountType.errors">Account Type is required.</mat-error>
</mat-form-field>
</div>
<div class="form-inline mb-3">
<div class="form-group form-check">
<label class="form-check-label mr-2" for="transfers">Include transfers?</label>
<mat-checkbox id="transfers" formControlName="transfers"></mat-checkbox>
</div>
</div>
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary"> EXPORT </button>
</form>
</div>
</div>
</div>
<app-footer appMenuSelection></app-footer>
</div>
<!-- ============================================================== -->
<!-- End Page content -->
<!-- ============================================================== -->
</div>

View File

@ -1,37 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExportAccountsComponent } from '@pages/accounts/export-accounts/export-accounts.component';
import {AccountsModule} from '@pages/accounts/accounts.module';
import {AppModule} from '@app/app.module';
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent} from '@src/testing';
describe('ExportAccountsComponent', () => {
let component: ExportAccountsComponent;
let fixture: ComponentFixture<ExportAccountsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
ExportAccountsComponent,
FooterStubComponent,
SidebarStubComponent,
TopbarStubComponent
],
imports: [
AccountsModule,
AppModule
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ExportAccountsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,34 +0,0 @@
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {CustomErrorStateMatcher} from '@app/_helpers';
@Component({
selector: 'app-export-accounts',
templateUrl: './export-accounts.component.html',
styleUrls: ['./export-accounts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExportAccountsComponent implements OnInit {
exportForm: FormGroup;
matcher = new CustomErrorStateMatcher();
submitted: boolean = false;
constructor(
private formBuilder: FormBuilder
) { }
ngOnInit(): void {
this.exportForm = this.formBuilder.group({
accountType: ['', Validators.required],
transfers: ['']
});
}
get exportFormStub(): any { return this.exportForm.controls; }
export(): void {
this.submitted = true;
if (this.exportForm.invalid || !confirm('Export accounts?')) { return; }
this.submitted = false;
}
}

View File

@ -7,6 +7,6 @@ const routes: Routes = [{ path: '', component: AdminComponent }];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class AdminRoutingModule {} export class AdminRoutingModule {}

View File

@ -4,7 +4,12 @@ import { AdminComponent } from '@pages/admin/admin.component';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { AdminModule } from '@pages/admin/admin.module'; import { AdminModule } from '@pages/admin/admin.module';
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent, UserServiceStub} from '@src/testing'; import {
FooterStubComponent,
SidebarStubComponent,
TopbarStubComponent,
UserServiceStub,
} from '@src/testing';
import { AppModule } from '@app/app.module'; import { AppModule } from '@app/app.module';
import { UserService } from '@app/_services'; import { UserService } from '@app/_services';
@ -21,18 +26,11 @@ describe('AdminComponent', () => {
AdminComponent, AdminComponent,
FooterStubComponent, FooterStubComponent,
SidebarStubComponent, SidebarStubComponent,
TopbarStubComponent TopbarStubComponent,
], ],
imports: [ imports: [AdminModule, AppModule, HttpClientTestingModule],
AdminModule, providers: [{ provide: UserService, useClass: UserServiceStub }],
AppModule, }).compileComponents();
HttpClientTestingModule,
],
providers: [
{ provide: UserService, useClass: UserServiceStub }
]
})
.compileComponents();
httpClient = TestBed.inject(HttpClient); httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController); httpTestingController = TestBed.inject(HttpTestingController);
userService = new UserServiceStub(); userService = new UserServiceStub();
@ -55,7 +53,7 @@ describe('AdminComponent', () => {
user: 'Tom', user: 'Tom',
role: 'enroller', role: 'enroller',
action: 'Disburse RSV 100', action: 'Disburse RSV 100',
approval: false approval: false,
}); });
}); });
}); });

View File

@ -6,6 +6,7 @@ import {LoggingService, UserService} from '@app/_services';
import { animate, state, style, transition, trigger } from '@angular/animations'; import { animate, state, style, transition, trigger } from '@angular/animations';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { exportCsv } from '@app/_helpers'; import { exportCsv } from '@app/_helpers';
import { Action } from '../../_models';
@Component({ @Component({
selector: 'app-admin', selector: 'app-admin',
@ -17,34 +18,29 @@ import {exportCsv} from '@app/_helpers';
state('collapsed', style({ height: '0px', minHeight: 0, visibility: 'hidden' })), state('collapsed', style({ height: '0px', minHeight: 0, visibility: 'hidden' })),
state('expanded', style({ height: '*', visibility: 'visible' })), state('expanded', style({ height: '*', visibility: 'visible' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]) ]),
] ],
}) })
export class AdminComponent implements OnInit { export class AdminComponent implements OnInit {
dataSource: MatTableDataSource<any>; dataSource: MatTableDataSource<any>;
displayedColumns = ['expand', 'user', 'role', 'action', 'status', 'approve']; displayedColumns: Array<string> = ['expand', 'user', 'role', 'action', 'status', 'approve'];
action: any; action: Action;
actions: any; actions: Array<Action>;
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
constructor( constructor(private userService: UserService, private loggingService: LoggingService) {
private userService: UserService,
private loggingService: LoggingService
) {
this.userService.getActions(); this.userService.getActions();
this.userService.actionsSubject.subscribe(actions => { this.userService.actionsSubject.subscribe((actions) => {
this.dataSource = new MatTableDataSource<any>(actions); this.dataSource = new MatTableDataSource<any>(actions);
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.actions = actions; this.actions = actions;
console.log(this.actions);
}); });
} }
ngOnInit(): void { ngOnInit(): void {}
}
doFilter(value: string): void { doFilter(value: string): void {
this.dataSource.filter = value.trim().toLocaleLowerCase(); this.dataSource.filter = value.trim().toLocaleLowerCase();
@ -55,14 +51,24 @@ export class AdminComponent implements OnInit {
} }
approveAction(action: any): void { approveAction(action: any): void {
if (!confirm('Approve action?')) { return; } if (!confirm('Approve action?')) {
this.userService.approveAction(action.id).pipe(first()).subscribe(res => this.loggingService.sendInfoLevelMessage(res)); return;
}
this.userService
.approveAction(action.id)
.pipe(first())
.subscribe((res) => this.loggingService.sendInfoLevelMessage(res));
this.userService.getActions(); this.userService.getActions();
} }
disapproveAction(action: any): void { disapproveAction(action: any): void {
if (!confirm('Disapprove action?')) { return; } if (!confirm('Disapprove action?')) {
this.userService.revokeAction(action.id).pipe(first()).subscribe(res => this.loggingService.sendInfoLevelMessage(res)); return;
}
this.userService
.revokeAction(action.id)
.pipe(first())
.subscribe((res) => this.loggingService.sendInfoLevelMessage(res));
this.userService.getActions(); this.userService.getActions();
} }

View File

@ -14,7 +14,6 @@ import {MatPaginatorModule} from '@angular/material/paginator';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatRippleModule } from '@angular/material/core'; import { MatRippleModule } from '@angular/material/core';
@NgModule({ @NgModule({
declarations: [AdminComponent], declarations: [AdminComponent],
imports: [ imports: [
@ -29,7 +28,7 @@ import {MatRippleModule} from '@angular/material/core';
MatSortModule, MatSortModule,
MatPaginatorModule, MatPaginatorModule,
MatButtonModule, MatButtonModule,
MatRippleModule MatRippleModule,
] ],
}) })
export class AdminModule {} export class AdminModule {}

View File

@ -5,16 +5,32 @@ import { PagesComponent } from './pages.component';
const routes: Routes = [ const routes: Routes = [
{ path: 'home', component: PagesComponent }, { path: 'home', component: PagesComponent },
{ path: 'tx', loadChildren: () => import('@pages/transactions/transactions.module').then(m => m.TransactionsModule) }, {
{ path: 'settings', loadChildren: () => import('@pages/settings/settings.module').then(m => m.SettingsModule) }, path: 'tx',
{ path: 'accounts', loadChildren: () => import('@pages/accounts/accounts.module').then(m => m.AccountsModule) }, loadChildren: () =>
{ path: 'tokens', loadChildren: () => import('@pages/tokens/tokens.module').then(m => m.TokensModule) }, import('@pages/transactions/transactions.module').then((m) => m.TransactionsModule),
{ path: 'admin', loadChildren: () => import('@pages/admin/admin.module').then(m => m.AdminModule) }, },
{ path: '**', redirectTo: 'home', pathMatch: 'full'} {
path: 'settings',
loadChildren: () => import('@pages/settings/settings.module').then((m) => m.SettingsModule),
},
{
path: 'accounts',
loadChildren: () => import('@pages/accounts/accounts.module').then((m) => m.AccountsModule),
},
{
path: 'tokens',
loadChildren: () => import('@pages/tokens/tokens.module').then((m) => m.TokensModule),
},
{
path: 'admin',
loadChildren: () => import('@pages/admin/admin.module').then((m) => m.AdminModule),
},
{ path: '**', redirectTo: 'home', pathMatch: 'full' },
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class PagesRoutingModule {} export class PagesRoutingModule {}

View File

@ -15,97 +15,13 @@
<li class="breadcrumb-item active" aria-current="page">Home</li> <li class="breadcrumb-item active" aria-current="page">Home</li>
</ol> </ol>
</nav> </nav>
<div class="card"> <div class="embed-responsive embed-responsive-16by9">
<mat-card-title class="card-header"> <iframe class="embed-responsive-item" [src]="url | safe" allow="fullscreen" loading="lazy"
CICADA DASHBOARD title="Community inclusion currencies dashboard" referrerpolicy="no-referrer">
</mat-card-title> <p>
<div class="col-12"> <a href="{{url}}"> Your browser does not support iframes. </a>
<div class="card-body"> </p>
<mat-form-field appearance="outline"> </iframe>
<mat-label>Filter by location : </mat-label>
<mat-select class="ml-2" id="filterUser">
<div *ngFor="let county of locations; trackBy: trackByName">
<div *ngFor="let district of county.districts; trackBy: trackByName">
<mat-optgroup *ngFor="let location of district.locations; trackBy: trackByName" [label]="county.name + ' / ' + district.name + ' / ' + location.name">
<mat-option *ngFor="let village of location.villages; trackBy: trackByName" [value]="village">
{{village}}
</mat-option>
</mat-optgroup>
</div>
</div>
</mat-select>
</mat-form-field>
</div>
<div class="row">
<div class="card col-md-8">
<div class="card-body">
<div style="display: block">
<canvas baseChart
[datasets]="lineChartData"
[labels]="lineChartLabels"
[options]="lineChartOptions"
[colors]="lineChartColors"
[legend]="lineChartLegend"
[chartType]="lineChartType"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
</div>
</div>
<hr>
<div class="card-body row">
<div class="col-md-3 text-center">
<h4>MASTER WALLET BALANCE</h4>
<p>{{10000000 | number}} RCU</p>
</div>
<div class="col-md-3 text-center">
<h4>TOTAL DISTRIBUTED</h4>
<p>{{disbursements * 1000 | number}} RCU</p>
</div>
<div class="col-md-3 text-center">
<h4>TOTAL SPENT</h4>
<p>{{transactions * 100000 | number}} RCU</p>
</div>
<div class="col-md-3 text-center">
<h4>TOTAL USERS</h4>
<p>{{users * 10 | number}} users</p>
</div>
</div>
<hr>
<div class="card-body">
<div style="display: block">
<canvas baseChart
[datasets]="barChartData"
[labels]="barChartLabels"
[options]="barChartOptions"
[legend]="barChartLegend"
[chartType]="barChartType">
</canvas>
</div>
</div>
</div>
<div class="card col-md-4">
<div class="card-body text-center">
<h4>TRANSFER USAGES</h4>
<div style="display: block">
<canvas baseChart
[data]="transferUsagesChartData"
[labels]="transferUsagesChartLabels"
[options]="transferUsagesChartOptions"
[colors]="transferUsagesChartColors"
[legend]="transferUsagesChartLegend"
[chartType]="transferUsagesChartType">
</canvas>
</div>
</div>
<hr>
<div class="card-body">
<h4 class="text-center">PARTNER LIVE FEED</h4>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<app-footer appMenuSelection></app-footer> <app-footer appMenuSelection></app-footer>

View File

@ -8,9 +8,8 @@ describe('PagesComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ PagesComponent ] declarations: [PagesComponent],
}) }).compileComponents();
.compileComponents();
}); });
beforeEach(() => { beforeEach(() => {

View File

@ -1,190 +1,13 @@
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import {Color, Label} from 'ng2-charts';
import {ChartDataSets, ChartOptions, ChartType} from 'chart.js';
import {LocationService, LoggingService} from '@app/_services';
import {ArraySum} from '@app/_helpers';
@Component({ @Component({
selector: 'app-pages', selector: 'app-pages',
templateUrl: './pages.component.html', templateUrl: './pages.component.html',
styleUrls: ['./pages.component.scss'], styleUrls: ['./pages.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class PagesComponent implements OnInit { export class PagesComponent {
disbursements: number = 0; url: string = 'https://dashboard.sarafu.network/';
users: any;
locations: any;
transactions: number = 0;
public lineChartData: ChartDataSets[] = [
{ data: [65, 59, 80, 81, 56, 55, 40], label: 'User Registration'},
{ data: [28, 48, 40, 19, 86, 27, 90], label: 'Transaction Volumes'},
{ data: [180, 480, 770, 90, 1000, 270, 400], label: 'Token Disbursements', yAxisID: 'y-axis-1'}
];
public lineChartLabels: Label[] = ['January', 'February', 'March', 'April', 'May', 'June', 'July']; constructor() {}
public lineChartOptions: (ChartOptions & { annotation: any }) = {
responsive: true,
scales: {
// We use this empty structure as a placeholder for dynamic theming.
xAxes: [{}],
yAxes: [
{
id: 'y-axis-0',
position: 'left',
},
{
id: 'y-axis-1',
position: 'right',
gridLines: {
color: 'rgba(255,0,0,0.3)',
},
ticks: {
fontColor: 'red',
}
}
]
},
annotation: {
annotations: [
{
type: 'line',
mode: 'vertical',
scaleID: 'x-axis-0',
value: 'March',
borderColor: 'orange',
borderWidth: 2,
label: {
enabled: true,
fontColor: 'orange',
content: 'LineAnno'
}
},
],
},
};
public lineChartColors: Color[] = [
{ // grey
backgroundColor: 'rgba(148,159,177,0.2)',
borderColor: 'rgba(148,159,177,1)',
pointBackgroundColor: 'rgba(148,159,177,1)',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: 'rgba(148,159,177,0.8)'
},
{ // dark grey
backgroundColor: 'rgba(77,83,96,0.2)',
borderColor: 'rgba(77,83,96,1)',
pointBackgroundColor: 'rgba(77,83,96,1)',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: 'rgba(77,83,96,1)'
},
{ // red
backgroundColor: 'rgba(255,0,0,0.3)',
borderColor: 'red',
pointBackgroundColor: 'rgba(148,159,177,1)',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: 'rgba(148,159,177,0.8)'
}
];
public lineChartLegend = true;
public lineChartType = 'line';
public barChartOptions: ChartOptions = {
responsive: true,
// We use these empty structures as placeholders for dynamic theming.
scales: { xAxes: [{}], yAxes: [{}] },
plugins: {
datalabels: {
anchor: 'end',
align: 'end',
}
}
};
public barChartLabels: Label[] = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
public barChartType: ChartType = 'horizontalBar';
public barChartLegend = true;
public barChartData: ChartDataSets[] = [
{ data: [65, 59, 80, 81, 56, 55, 40], label: 'New Users'},
{ data: [28, 48, 40, 19, 86, 27, 90], label: 'Recurrent Users'}
];
public transferUsagesChartLabels: Label[] = ['Food/Water', 'Fuel/Energy', 'Education', 'Health', 'Shop', 'Environment', 'Transport',
'Farming/Labour', 'Savings Group', 'Savings Group'];
public transferUsagesChartData: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
public transferUsagesChartType: ChartType = 'pie';
public transferUsagesChartOptions: ChartOptions = {
responsive: true,
legend: {
position: 'top',
},
plugins: {
datalabels: {
formatter: (value, ctx) => {
const label = ctx.chart.data.labels[ctx.dataIndex];
return label;
},
},
}
};
public transferUsagesChartLegend = true;
public transferUsagesChartColors = [
{
backgroundColor: [
'rgba(0,0,255,0.3)',
'rgba(255,0,0,0.3)',
'rgba(0,255,0,0.3)',
'rgba(255,0,255,0.3)',
'rgba(255,255,0,0.3)',
'rgba(0,255,255,0.3)',
'rgba(255,255,255,0.3)',
'rgba(255,100,0,0.3)',
'rgba(0,255,100,0.3)',
'rgba(100,0,255,0.3)'],
},
];
constructor(
private locationService: LocationService,
private loggingService: LoggingService
) {
this.locationService.getLocations();
this.locationService.locationsSubject.subscribe(locations => {
this.locations = locations;
});
}
ngOnInit(): void {
this.newDataPoint([50, 80], 'August');
this.transferUsagesChartData = [18, 12, 10, 8, 6, 5, 5, 3, 2, 1];
this.disbursements = ArraySum.arraySum(this.lineChartData.find(data => data.label === 'Token Disbursements').data);
this.users = ArraySum.arraySum(this.barChartData.find(data => data.label === 'New Users').data);
this.transactions = ArraySum.arraySum(this.lineChartData.find(data => data.label === 'Transaction Volumes').data);
}
newDataPoint(dataArr: any[], label: string): void {
this.barChartData.forEach((dataset, index) => {
this.barChartData[index] = Object.assign({}, this.barChartData[index], {
data: [...this.barChartData[index].data, dataArr[index]]
});
});
this.barChartLabels = [...this.barChartLabels, label];
}
public chartClicked({ event, active}: { event: MouseEvent, active: {}[] }): void {
this.loggingService.sendInfoLevelMessage(`Event: ${event}, ${active}`);
}
public chartHovered({ event, active }: { event: MouseEvent, active: {}[] }): void {
this.loggingService.sendInfoLevelMessage(`Event: ${event}, ${active}`);
}
public trackByName(index, item): string {
return item.name;
}
} }

View File

@ -4,26 +4,23 @@ import { CommonModule } from '@angular/common';
import { PagesRoutingModule } from '@pages/pages-routing.module'; import { PagesRoutingModule } from '@pages/pages-routing.module';
import { PagesComponent } from '@pages/pages.component'; import { PagesComponent } from '@pages/pages.component';
import { SharedModule } from '@app/shared/shared.module'; import { SharedModule } from '@app/shared/shared.module';
import {ChartsModule} from 'ng2-charts';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
@NgModule({ @NgModule({
declarations: [PagesComponent], declarations: [PagesComponent],
imports: [ imports: [
CommonModule, CommonModule,
PagesRoutingModule, PagesRoutingModule,
SharedModule, SharedModule,
ChartsModule,
MatButtonModule, MatButtonModule,
MatFormFieldModule, MatFormFieldModule,
MatSelectModule, MatSelectModule,
MatInputModule, MatInputModule,
MatCardModule MatCardModule,
] ],
}) })
export class PagesModule {} export class PagesModule {}

View File

@ -1,51 +0,0 @@
<!-- Begin page -->
<div class="wrapper">
<app-sidebar></app-sidebar>
<!-- ============================================================== -->
<!-- Start Page Content here -->
<!-- ============================================================== -->
<div id="content">
<app-topbar></app-topbar>
<!-- Start Content-->
<div class="container-fluid" appMenuSelection>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a routerLink="/home">Home</a></li>
<li class="breadcrumb-item"><a routerLink="/settings">Settings</a></li>
<li class="breadcrumb-item active" aria-current="page">Invite User</li>
</ol>
</nav>
<div class="col-md-6">
<div class="card">
<mat-card-title class="card-header text-center">
INVITE NEW USERS
</mat-card-title>
<div class="card-body">
<form [formGroup]="inviteForm" (ngSubmit)="invite()">
<mat-form-field appearance="outline">
<mat-label>Email Address: </mat-label>
<input matInput type="email" id="email" placeholder="Email" formControlName="email"
[errorStateMatcher]="matcher">
<mat-error *ngIf="submitted && inviteFormStub.email.errors">Email is required.</mat-error>
</mat-form-field><br>
<mat-radio-group aria-label="Select a role" formControlName="role">
<mat-radio-button value="Superadmin">Superadmin</mat-radio-button><br>
<mat-radio-button value="Admin">Admin</mat-radio-button><br>
<mat-radio-button value="Subadmin">Subadmin</mat-radio-button><br>
<mat-radio-button value="View">View</mat-radio-button><br>
</mat-radio-group>
<mat-error *ngIf="submitted && inviteFormStub.role.errors">Role is required.</mat-error>
<button mat-raised-button color="primary" type="submit" class="btn btn-outline-primary">INVITE</button>
</form>
</div>
</div>
</div>
</div>
<app-footer appMenuSelection></app-footer>
</div>
<!-- ============================================================== -->
<!-- End Page content -->
<!-- ============================================================== -->
</div>

Some files were not shown because too many files have changed in this diff Show More