Merge branch 'master' into spencer/pwa

# Conflicts:
#	package-lock.json
#	src/app/shared/shared.module.ts
#	src/styles.scss
This commit is contained in:
Spencer Ofwiti 2021-05-11 18:28:54 +03:00
commit d4d2837d2e
93 changed files with 1710 additions and 22524 deletions

View File

@ -27,7 +27,7 @@ ng_build:
IMAGE_TAG_BASE: $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA IMAGE_TAG_BASE: $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA
LATEST_TAG: $CI_REGISTRY_IMAGE:latest LATEST_TAG: $CI_REGISTRY_IMAGE:latest
script: script:
- export IMAGE_TAG="$IMAGE_TAG_BASE-$(date +%F.%H%M%S)" - export IMAGE_TAG="$IMAGE_TAG_BASE-$(date +%s)"
- mkdir -p /kaniko/.docker - mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > "/kaniko/.docker/config.json" - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > "/kaniko/.docker/config.json"
- /kaniko/executor --context . $KANIKO_CACHE_ARGS --destination $IMAGE_TAG --destination $CI_REGISTRY_IMAGE:latest - /kaniko/executor --context . $KANIKO_CACHE_ARGS --destination $IMAGE_TAG --destination $CI_REGISTRY_IMAGE:latest

View File

@ -22,6 +22,9 @@ Run `ng generate module module-name --route module-name --module app.module` to
## Build ## Build
set you environment variables - set these via environment variables as found in set-env.ts
// TODO create a .env file so people don't have to set these one-by-one
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 `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.
## Running unit tests ## Running unit tests

21220
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,8 +32,9 @@
"block-syncer": "^0.2.4", "block-syncer": "^0.2.4",
"bootstrap": "^4.5.3", "bootstrap": "^4.5.3",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"cic-client": "^0.1.1", "cic-client": "0.1.4",
"cic-client-meta": "0.0.7-alpha.3", "cic-client-meta": "0.0.7-alpha.6",
"cic-schemas-data-validator": "^1.0.0-alpha.3",
"datatables.net": "^1.10.22", "datatables.net": "^1.10.22",
"datatables.net-dt": "^1.10.22", "datatables.net-dt": "^1.10.22",
"ethers": "^5.0.31", "ethers": "^5.0.31",

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/AccountRegistry.json');
const abi = accountIndex.default; const web3: Web3 = new Web3(environment.web3Provider);
export class AccountIndex { export class AccountIndex {
contractAddress: string; contractAddress: string;
@ -30,14 +28,14 @@ export class AccountIndex {
} }
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 - 1;
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.accounts(i).call();
accounts.push(account); accounts.push(account);
} }
return accounts; return accounts;
@ -46,8 +44,7 @@ export class AccountIndex {
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

@ -12,10 +12,9 @@ export class AuthGuard implements CanActivate {
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 {
if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) { 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,5 +1,7 @@
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,53 @@
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 Doesn\'t 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;
}
let csv: string = Object.keys(arrayData[0]).join(delimiter) + '\n';
arrayData.forEach(obj => { arrayData.forEach(obj => {
let row = []; 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';

View File

@ -3,9 +3,19 @@ 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 loggingService: LoggingService,
@ -14,17 +24,17 @@ export class GlobalErrorHandler extends ErrorHandler {
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,7 +45,7 @@ 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: ${(error as HttpErrorResponse).status}`,
@ -50,12 +60,12 @@ export class GlobalErrorHandler extends ErrorHandler {
} }
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;
} }

View File

@ -1,13 +1,13 @@
function HttpGetter(): void {} function HttpGetter(): void {}
HttpGetter.prototype.get = filename => new Promise((whohoo, doh) => { HttpGetter.prototype.get = filename => new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.addEventListener('load', (e) => { xhr.addEventListener('load', (e) => {
if (xhr.status === 200) { if (xhr.status === 200) {
whohoo(xhr.responseText); resolve(xhr.responseText);
return; return;
} }
doh('failed with status ' + xhr.status + ': ' + xhr.statusText); reject('failed with status ' + xhr.status + ': ' + xhr.statusText);
}); });
xhr.open('GET', filename); xhr.open('GET', filename);
xhr.send(); xhr.send();

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';

View File

@ -2,16 +2,9 @@ import {HTTP_INTERCEPTORS, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest,
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {Observable, of, throwError} from 'rxjs'; import {Observable, of, throwError} from 'rxjs';
import {delay, dematerialize, materialize, mergeMap} from 'rxjs/operators'; import {delay, dematerialize, materialize, mergeMap} from 'rxjs/operators';
import {Action, AreaName, AreaType, Category, Token} from '@app/_models';
const accounts = [ const actions: Array<Action> = [
{id: 1, name: 'John Doe', phone: '+25412345678', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9865', type: 'user', age: 43, created: '08/16/2020', balance: '12987', failedPinAttempts: 1, status: 'active', bio: 'Bodaboda', category: 'transport', gender: 'male', location: 'Bofu', locationType: 'Rural', token: 'RSV', referrer: '+25412341234'},
{id: 2, name: 'Jane Buck', phone: '+25412341234', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9866', type: 'vendor', age: 25, created: '04/02/2020', balance: '56281', failedPinAttempts: 0, status: 'active', bio: 'Groceries', category: 'food/water', gender: 'female', location: 'Lindi', locationType: 'Urban', token: 'ERN', referrer: ''},
{id: 3, name: 'Mc Donald', phone: '+25498765432', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9867', type: 'group', age: 31, created: '11/16/2020', balance: '450', failedPinAttempts: 2, status: 'blocked', bio: 'Food', category: 'food/water', gender: 'male', location: 'Miyani', locationType: 'Rural', token: 'RSV', referrer: '+25498769876'},
{id: 4, name: 'Hera Cles', phone: '+25498769876', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9868', type: 'user', age: 38, created: '05/28/2020', balance: '5621', failedPinAttempts: 3, status: 'active', bio: 'Shop', category: 'shop', gender: 'female', location: 'Kayaba', locationType: 'Urban', token: 'BRT', referrer: '+25412341234'},
{id: 5, name: 'Silver Fia', phone: '+25462518374', address: '0xc86ff893ac40d3950b4d5f94a9b837258b0a9869', type: 'tokenAgent', age: 19, created: '10/10/2020', balance: '817', failedPinAttempts: 0, status: 'blocked', bio: 'Electronics', category: 'shop', gender: 'male', location: 'Mkanyeni', locationType: 'Rural', token: 'RSV', referrer: '+25412345678'},
];
const actions = [
{ id: 1, user: 'Tom', role: 'enroller', action: 'Disburse RSV 100', approval: false }, { id: 1, user: 'Tom', role: 'enroller', action: 'Disburse RSV 100', approval: false },
{ id: 2, user: 'Christine', role: 'admin', action: 'Change user phone number', approval: true }, { id: 2, user: 'Christine', role: 'admin', action: 'Change user phone number', approval: true },
{ id: 3, user: 'Will', role: 'superadmin', action: 'Reclaim RSV 1000', approval: true }, { id: 3, user: 'Will', role: 'superadmin', action: 'Reclaim RSV 1000', approval: true },
@ -20,61 +13,228 @@ const actions = [
{ id: 6, user: 'Patience', role: 'enroller', action: 'Change user information', approval: false } { id: 6, user: 'Patience', role: 'enroller', action: 'Change user information', approval: false }
]; ];
const histories = [ const tokens: Array<Token> = [
{ id: 1, userId: 3, userName: 'Mc Donald', action: 'Receive RSV 100', staff: 'Tom', timestamp: Date.now() }, {
{ id: 2, userId: 5, userName: 'Silver Fia', action: 'Change phone number from +25412345678 to +25498765432', staff: 'Christine', timestamp: Date.now()}, name: 'Giftable Reserve', symbol: 'GRZ', address: '0xa686005CE37Dce7738436256982C3903f2E4ea8E', supply: '1000000001000000000000000000',
{ id: 3, userId: 4, userName: 'Hera Cles', action: 'Completed user profile', staff: 'Vivian', timestamp: Date.now() }, decimals: '18', reserves: {}
];
const locations: any = [
{ name: 'Kwale',
districts: [
{ name: 'Kinango',
locations: [
{ name: 'Bofu', villages: ['Bofu', 'Chidzuvini', 'Mkanyeni']},
{ name: 'Mnyenzeni', villages: ['Miloeni', 'Miyani', 'Mnyenzeni', 'Vikolani', 'Vitangani']}
]
}
]
}, },
{ name: 'Nairobi', {
districts: [ name: 'Demo Token', symbol: 'DEMO', address: '0xc80D6aFF8194114c52AEcD84c9f15fd5c8abb187', supply: '99999999999999998976',
{ name: 'Dagorreti', decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '99999999999999998976'}},
locations: [ reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'
{ name: 'Kawangware', villages: ['Congo']},
]
}, },
{ name: 'Ngong', {
locations: [ name: 'Foo Token', symbol: 'FOO', address: '0x9ceD86089f7aBB5A97B40eb0E7521e7aa308d354', supply: '1000000000000000001014',
{ name: 'Kibera', villages: ['Kibera', 'Lindi']}, decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '1000000000000000001014'}},
] reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'
}, },
{ name: 'South B', {
locations: [ name: 'testb', symbol: 'tstb', address: '0xC63cFA91A3BFf41cE31Ff436f67D3ACBC977DB95', supply: '99000', decimals: '18',
{ name: 'Mukuru', villages: ['Kayaba']}, reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '99000'}}, reserveRatio: '1000000',
{ name: 'South B', villages: ['South B']}, owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'
]
}, },
] {
name: 'testa', symbol: 'tsta', address: '0x8fA4101ef19D0a078239d035659e92b278bD083C', supply: '9981', decimals: '18',
reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '9981'}}, reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'
},
{
name: 'testc', symbol: 'tstc', address: '0x4A6fA6bc3BfE4C9661bC692D9798425350C9e3D4', supply: '100990', decimals: '18',
reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '100990'}}, reserveRatio: '1000000',
owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'
} }
]; ];
const staffMembers = [ const categories: Array<Category> = [
{ id: 1, name: 'admin@acme.org', accountType: 'Admin', created: '17/11/2020', status: 'activated'}, {
{ id: 2, name: 'will@grassecon.org', accountType: 'SuperAdmin', created: '17/11/2020', status: 'activated'}, name: 'system',
{ id: 3, name: 'spence@grassecon.org', accountType: 'Enroller', created: '17/11/2020', status: 'activated'}, products: ['system', 'office main', 'office main phone']
{ id: 4, name: 'admin@redcross.org', accountType: 'View', created: '17/11/2020', status: 'activated'} },
{
name: 'education',
products: ['book', 'coach', 'teacher', 'sch', 'school', 'pry', 'education', 'student', 'mwalimu', 'maalim', 'consultant', 'consult',
'college', 'university', 'lecturer', 'primary', 'secondary', 'daycare', 'babycare', 'baby care', 'elim', 'eimu', 'nursery',
'red cross', 'volunteer', 'instructor', 'journalist', 'lesson', 'academy', 'headmistress', 'headteacher', 'cyber', 'researcher',
'professor', 'demo', 'expert', 'tution', 'tuition', 'children', 'headmaster', 'educator', 'Marital counsellor', 'counsellor',
'trainer', 'vijana', 'youth', 'intern', 'redcross', 'KRCS', 'danish', 'science', 'data', 'facilitator', 'vitabu', 'kitabu']
},
{
name: 'faith',
products: ['pastor', 'imam', 'madrasa', 'religous', 'religious', 'ustadh', 'ustadhi', 'Marital counsellor', 'counsellor', 'church',
'kanisa', 'mksiti', 'donor']
},
{
name: 'government',
products: ['elder', 'chief', 'police', 'government', 'country', 'county', 'soldier', 'village admin', 'ward', 'leader', 'kra',
'mailman', 'immagration', 'immigration']
},
{
name: 'environment',
products: ['conservation', 'toilet', 'choo', 'garbage', 'fagio', 'waste', 'tree', 'taka', 'scrap', 'cleaning', 'gardener', 'rubbish',
'usafi', 'mazingira', 'miti', 'trash', 'cleaner', 'plastic', 'collection', 'seedling', 'seedlings', 'recycling']
},
{
name: 'farming',
products: ['farm', 'farmer', 'farming', 'mkulima', 'kulima', 'ukulima', 'wakulima', 'jembe', 'shamba']
},
{
name: 'labour',
products: ['artist', 'agent', 'guard', 'askari', 'accountant', 'baker', 'beadwork', 'beauty', 'business', 'barber', 'casual',
'electrian', 'caretaker', 'car wash', 'capenter', 'construction', 'chef', 'catering', 'cobler', 'cobbler', 'carwash', 'dhobi',
'landlord', 'design', 'carpenter', 'fundi', 'hawking', 'hawker', 'househelp', 'hsehelp', 'house help', 'help', 'housegirl', 'kushona',
'juakali', 'jualikali', 'juacali', 'jua kali', 'shepherd', 'makuti', 'kujenga', 'kinyozi', 'kazi', 'knitting', 'kufua', 'fua',
'hustler', 'biashara', 'labour', 'labor', 'laundry', 'repair', 'hair', 'posho', 'mill', 'mtambo', 'uvuvi', 'engineer', 'manager',
'tailor', 'nguo', 'mason', 'mtumba', 'garage', 'mechanic', 'mjenzi', 'mfugaji', 'painter', 'receptionist', 'printing', 'programming',
'plumb', 'charging', 'salon', 'mpishi', 'msusi', 'mgema', 'footballer', 'photocopy', 'peddler', 'staff', 'sales', 'service', 'saloon',
'seremala', 'security', 'insurance', 'secretary', 'shoe', 'shepard', 'shephard', 'tout', 'tv', 'mvuvi', 'mawe', 'majani', 'maembe',
'freelance', 'mjengo', 'electronics', 'photographer', 'programmer', 'electrician', 'washing', 'bricks', 'welder', 'welding',
'working', 'worker', 'watchman', 'waiter', 'waitress', 'viatu', 'yoga', 'guitarist', 'house', 'artisan', 'musician', 'trade',
'makonge', 'ujenzi', 'vendor', 'watchlady', 'marketing', 'beautician', 'photo', 'metal work', 'supplier', 'law firm', 'brewer']
},
{
name: 'food',
products: ['avocado', 'bhajia', 'bajia', 'mbonga', 'bofu', 'beans', 'biscuits', 'biringanya', 'banana', 'bananas', 'crisps', 'chakula',
'coconut', 'chapati', 'cereal', 'chipo', 'chapo', 'chai', 'chips', 'cassava', 'cake', 'cereals', 'cook', 'corn', 'coffee', 'chicken',
'dagaa', 'donut', 'dough', 'groundnuts', 'hotel', 'holel', 'hoteli', 'butcher', 'butchery', 'fruit', 'food', 'fruits', 'fish',
'githeri', 'grocery', 'grocer', 'pojo', 'papa', 'goats', 'mabenda', 'mbenda', 'poultry', 'soda', 'peanuts', 'potatoes', 'samosa',
'soko', 'samaki', 'tomato', 'tomatoes', 'mchele', 'matunda', 'mango', 'melon', 'mellon', 'nyanya', 'nyama', 'omena', 'umena', 'ndizi',
'njugu', 'kamba kamba', 'khaimati', 'kaimati', 'kunde', 'kuku', 'kahawa', 'keki', 'muguka', 'miraa', 'milk', 'choma', 'maziwa',
'mboga', 'mbog', 'busaa', 'chumvi', 'cabbages', 'mabuyu', 'machungwa', 'mbuzi', 'mnazi', 'mchicha', 'ngombe', 'ngano', 'nazi',
'oranges', 'peanuts', 'mkate', 'bread', 'mikate', 'vitungu', 'sausages', 'maize', 'mbata', 'mchuzi', 'mchuuzi', 'mandazi', 'mbaazi',
'mahindi', 'maandazi', 'mogoka', 'meat', 'mhogo', 'mihogo', 'muhogo', 'maharagwe', 'miwa', 'mahamri', 'mitumba', 'simsim', 'porridge',
'pilau', 'vegetable', 'egg', 'mayai', 'mifugo', 'unga', 'good', 'sima', 'sweet', 'sweats', 'sambusa', 'snacks', 'sugar', 'suger',
'ugoro', 'sukari', 'soup', 'spinach', 'smokie', 'smokies', 'sukuma', 'tea', 'uji', 'ugali', 'uchuzi', 'uchuuzi', 'viazi', 'yoghurt',
'yogurt', 'wine', 'marondo', 'maandzi', 'matoke', 'omeno', 'onions', 'nzugu', 'korosho', 'barafu', 'juice']
},
{
name: 'water',
products: ['maji', 'water']
},
{
name: 'health',
products: ['agrovet', 'dispensary', 'barakoa', 'chemist', 'Chemicals', 'chv', 'doctor', 'daktari', 'dawa', 'hospital', 'herbalist',
'mganga', 'sabuni', 'soap', 'nurse', 'heath', 'community health worker', 'clinic', 'clinical', 'mask', 'medicine', 'lab technician',
'pharmacy', 'cosmetics', 'veterinary', 'vet', 'sickly', 'emergency response', 'emergency']
},
{
name: 'savings',
products: ['chama', 'group', 'savings', 'loan', 'silc', 'vsla', 'credit', 'finance']
},
{
name: 'shop',
products: ['bag', 'bead', 'belt', 'bedding', 'jik', 'bed', 'cement', 'botique', 'boutique', 'lines', 'kibanda', 'kiosk', 'spareparts',
'candy', 'cloth', 'electricals', 'mutumba', 'cafe', 'leso', 'lesso', 'duka', 'spare parts', 'socks', 'malimali', 'mitungi',
'mali mali', 'hardware', 'detergent', 'detergents', 'dera', 'retail', 'kamba', 'pombe', 'pampers', 'pool', 'phone', 'simu', 'mangwe',
'mikeka', 'movie', 'shop', 'acces', 'mchanga', 'uto', 'airtime', 'matress', 'mattress', 'mattresses', 'mpsea', 'mpesa', 'shirt',
'wholesaler', 'perfume', 'playstation', 'tissue', 'vikapu', 'uniform', 'flowers', 'vitenge', 'utencils', 'utensils', 'station',
'jewel', 'pool table', 'club', 'pub', 'bar', 'furniture', 'm-pesa', 'vyombo']
},
{
name: 'transport',
products: ['kebeba', 'beba', 'bebabeba', 'bike', 'bicycle', 'matatu', 'boda', 'bodaboda', 'cart', 'carrier', 'tour', 'travel', 'driver',
'dereva', 'tout', 'conductor', 'kubeba', 'tuktuk', 'taxi', 'piki', 'pikipiki', 'manamba', 'trasportion', 'mkokoteni', 'mover',
'motorist', 'motorbike', 'transport', 'transpoter', 'gari', 'magari', 'makanga', 'car']
},
{
name: 'fuel/energy',
products: ['timber', 'timberyard', 'biogas', 'charcol', 'charcoal', 'kuni', 'mbao', 'fuel', 'makaa', 'mafuta', 'moto', 'solar', 'stima',
'fire', 'firewood', 'wood', 'oil', 'taa', 'gas', 'paraffin', 'parrafin', 'parafin', 'petrol', 'petro', 'kerosine', 'kerosene',
'diesel']
},
{
name: 'other',
products: ['other', 'none', 'unknown', 'none']
}
]; ];
const tokens = [ const areaNames: Array<AreaName> = [
{name: 'Giftable Reserve', symbol: 'GRZ', address: '0xa686005CE37Dce7738436256982C3903f2E4ea8E', supply: '1000000001000000000000000000', decimals: '18', reserves: {}}, {
{name: 'Demo Token', symbol: 'DEMO', address: '0xc80D6aFF8194114c52AEcD84c9f15fd5c8abb187', supply: '99999999999999998976', decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '99999999999999998976'}}, reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'}, name: 'Mukuru Nairobi',
{name: 'Foo Token', symbol: 'FOO', address: '0x9ceD86089f7aBB5A97B40eb0E7521e7aa308d354', supply: '1000000000000000001014', decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '1000000000000000001014'}}, reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'}, locations: ['kayaba', 'kayba', 'kambi', 'mukuru', 'masai', 'hazina', 'south', 'tetra', 'tetrapak', 'ruben', 'rueben', 'kingston',
{name: 'testb', symbol: 'tstb', address: '0xC63cFA91A3BFf41cE31Ff436f67D3ACBC977DB95', supply: '99000', decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '99000'}}, reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'}, 'korokocho', 'kingstone', 'kamongo', 'lungalunga', 'sinai', 'sigei', 'lungu', 'lunga lunga', 'owino road', 'seigei']
{name: 'testa', symbol: 'tsta', address: '0x8fA4101ef19D0a078239d035659e92b278bD083C', supply: '9981', decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '9981'}}, reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'}, },
{name: 'testc', symbol: 'tstc', address: '0x4A6fA6bc3BfE4C9661bC692D9798425350C9e3D4', supply: '100990', decimals: '18', reserves: {'0xa686005CE37Dce7738436256982C3903f2E4ea8E': {weight: '1000000', balance: '100990'}}, reserveRatio: '1000000', owner: '0x3Da99AAD2D9CA01D131eFc3B17444b832B31Ff4a'} {
name: 'Kinango Kwale',
locations: ['amani', 'bofu', 'chibuga', 'chikomani', 'chilongoni', 'chigojoni', 'chinguluni', 'chigato', 'chigale', 'chikole',
'chilongoni', 'chilumani', 'chigojoni', 'chikomani', 'chizini', 'chikomeni', 'chidzuvini', 'chidzivuni', 'chikuyu', 'chizingo',
'doti', 'dzugwe', 'dzivani', 'dzovuni', 'hanje', 'kasemeni', 'katundani', 'kibandaogo', 'kibandaongo', 'kwale', 'kinango',
'kidzuvini', 'kalalani', 'kafuduni', 'kaloleni', 'kilibole', 'lutsangani', 'peku', 'gona', 'guro', 'gandini', 'mkanyeni', 'myenzeni',
'miyenzeni', 'miatsiani', 'mienzeni', 'mnyenzeni', 'minyenzeni', 'miyani', 'mioleni', 'makuluni', 'mariakani', 'makobeni', 'madewani',
'mwangaraba', 'mwashanga', 'miloeni', 'mabesheni', 'mazeras', 'mazera', 'mlola', 'muugano', 'mulunguni', 'mabesheni', 'miatsani',
'miatsiani', 'mwache', 'mwangani', 'mwehavikonje', 'miguneni', 'nzora', 'nzovuni', 'vikinduni', 'vikolani', 'vitangani', 'viogato',
'vyogato', 'vistangani', 'yapha', 'yava', 'yowani', 'ziwani', 'majengo', 'matuga', 'vigungani', 'vidziweni', 'vinyunduni', 'ukunda',
'kokotoni', 'mikindani']
},
{
name: 'Misc Nairobi',
locations: ['nairobi', 'west', 'lindi', 'kibera', 'kibira', 'kibra', 'makina', 'soweto', 'olympic', 'kangemi', 'ruiru', 'congo',
'kawangware', 'kwangware', 'donholm', 'dagoreti', 'dandora', 'kabete', 'sinai', 'donhom', 'donholm', 'huruma', 'kitengela',
'makadara', ',mlolongo', 'kenyatta', 'mlolongo', 'tassia', 'tasia', 'gatina', '56', 'industrial', 'kariobangi', 'kasarani', 'kayole',
'mathare', 'pipe', 'juja', 'uchumi', 'jogoo', 'umoja', 'thika', 'kikuyu', 'stadium', 'buru buru', 'ngong', 'starehe', 'mwiki',
'fuata', 'kware', 'kabiro', 'embakassi', 'embakasi', 'kmoja', 'east', 'githurai', 'landi', 'langata', 'limuru', 'mathere',
'dagoretti', 'kirembe', 'muugano', 'mwiki', 'toi market']
},
{
name: 'Misc Mombasa',
locations: ['mombasa', 'likoni', 'bangla', 'bangladesh', 'kizingo', 'old town', 'makupa', 'mvita', 'ngombeni', 'ngómbeni', 'ombeni',
'magongo', 'miritini', 'changamwe', 'jomvu', 'ohuru', 'tudor', 'diani']
},
{
name: 'Kisauni',
locations: ['bamburi', 'kisauni', 'mworoni', 'nyali', 'shanzu', 'bombolulu', 'mtopanga', 'mjambere', 'majaoni', 'manyani', 'magogoni',
'junda', 'mwakirunge', 'mshomoroni']
},
{
name: 'Kilifi',
locations: ['kilfi', 'kilifi', 'mtwapa', 'takaungu', 'makongeni', 'mnarani', 'mnarani', 'office', 'g.e', 'ge', 'raibai', 'ribe']
},
{
name: 'Kakuma',
locations: ['kakuma']
},
{
name: 'Kitui',
locations: ['kitui', 'mwingi']
},
{
name: 'Nyanza',
locations: ['busia', 'nyalgunga', 'mbita', 'siaya', 'kisumu', 'nyalenda', 'hawinga', 'rangala', 'uyoma', 'mumias', 'homabay', 'homaboy',
'migori', 'kusumu']
},
{
name: 'Misc Rural Counties',
locations: ['makueni', 'meru', 'kisii', 'bomet', 'machakos', 'bungoma', 'eldoret', 'kakamega', 'kericho', 'kajiado', 'nandi', 'nyeri',
'wote', 'kiambu', 'mwea', 'nakuru', 'narok']
},
{
name: 'other',
locations: ['other', 'none', 'unknown']
}
]; ];
const areaTypes: Array<AreaType> = [
{
name: 'urban',
area: ['urban', 'nairobi', 'mombasa']
},
{
name: 'rural',
area: ['rural', 'kakuma', 'kwale', 'kinango', 'kitui', 'nyanza']
},
{
name: 'periurban',
area: ['kilifi', 'periurban']
},
{
name: 'other',
area: ['other']
}
];
const accountTypes: Array<string> = ['user', 'cashier', 'vendor', 'tokenagent', 'group'];
const transactionTypes: Array<string> = ['transactions', 'conversions', 'disbursements', 'rewards', 'reclamation'];
const genders: Array<string> = ['male', 'female', 'other'];
@Injectable() @Injectable()
export class MockBackendInterceptor implements HttpInterceptor { export class MockBackendInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
@ -90,32 +250,34 @@ export class MockBackendInterceptor implements HttpInterceptor {
function handleRoute(): Observable<any> { function handleRoute(): Observable<any> {
switch (true) { switch (true) {
case url.endsWith('/accounts') && method === 'GET':
return getAccounts();
case url.match(/\/accounts\/\d+$/) && method === 'GET':
return getAccountById();
case url.endsWith('/actions') && method === 'GET': case url.endsWith('/actions') && method === 'GET':
return getActions(); return getActions();
case url.match(/\/actions\/\d+$/) && method === 'GET': case url.match(/\/actions\/\d+$/) && method === 'GET':
return getActionById(); return getActionById();
case url.match(/\/actions\/\d+$/) && method === 'POST': case url.match(/\/actions\/\d+$/) && method === 'POST':
return approveAction(); return approveAction();
case url.match(/\/history\/\d+$/) && method === 'GET':
return getHistoryByUser();
case url.endsWith('/locations') && method === 'GET':
return getLocations();
case url.endsWith('/staff') && method === 'GET':
return getStaff();
case url.match(/\/staff\/\d+$/) && method === 'GET':
return getStaffById();
case url.match(/\/staff\/\d+$/) && method === 'POST' && body.status !== undefined:
return changeStaffStatus();
case url.match(/\/staff\/\d+$/) && method === 'POST' && body.accountType !== undefined:
return changeStaffType();
case url.endsWith('/tokens') && method === 'GET': case url.endsWith('/tokens') && method === 'GET':
return getTokens(); return getTokens();
case url.match(/\/tokens\/\w+$/) && method === 'GET': case url.match(/\/tokens\/\w+$/) && method === 'GET':
return getTokenBySymbol(); return getTokenBySymbol();
case url.endsWith('/categories') && method === 'GET':
return getCategories();
case url.match(/\/categories\/\w+$/) && method === 'GET':
return getCategoryByProduct();
case url.endsWith('/areanames') && method === 'GET':
return getAreaNames();
case url.match(/\/areanames\/\w+$/) && method === 'GET':
return getAreaNameByLocation();
case url.endsWith('/areatypes') && method === 'GET':
return getAreaTypes();
case url.match(/\/areatypes\/\w+$/) && method === 'GET':
return getAreaTypeByArea();
case url.endsWith('/accounttypes') && method === 'GET':
return getAccountTypes();
case url.endsWith('/transactiontypes') && method === 'GET':
return getTransactionTypes();
case url.endsWith('/genders') && method === 'GET':
return getGenders();
default: default:
// pass through any requests not handled above // pass through any requests not handled above
return next.handle(request); return next.handle(request);
@ -124,76 +286,77 @@ export class MockBackendInterceptor implements HttpInterceptor {
// route functions // route functions
function getAccounts(): Observable<any> { function getActions(): Observable<HttpResponse<any>> {
return ok(accounts);
}
function getAccountById(): Observable<any> {
const queriedAccount = accounts.find(account => account.id === idFromUrl());
return ok(queriedAccount);
}
function getActions(): Observable<any> {
return ok(actions); return ok(actions);
} }
function getActionById(): Observable<any> { function getActionById(): Observable<HttpResponse<any>> {
const queriedAction = actions.find(action => action.id === idFromUrl()); const queriedAction: Action = actions.find(action => action.id === idFromUrl());
return ok(queriedAction); return ok(queriedAction);
} }
function approveAction(): Observable<any> { function approveAction(): Observable<HttpResponse<any>> {
const queriedAction = actions.find(action => action.id === idFromUrl()); const queriedAction: Action = actions.find(action => action.id === idFromUrl());
queriedAction.approval = body.approval; queriedAction.approval = body.approval;
const message = `Action approval status set to ${body.approval} successfully!`; const message: string = `Action approval status set to ${body.approval} successfully!`;
return ok(message); return ok(message);
} }
function getHistoryByUser(): Observable<any> { function getTokens(): Observable<HttpResponse<any>> {
const queriedUserHistory = histories.filter(history => history.userId === idFromUrl());
return ok(queriedUserHistory);
}
function getLocations(): Observable<any> {
return ok(locations);
}
function getStaff(): Observable<any> {
return ok(staffMembers);
}
function getStaffById(): Observable<any> {
const queriedStaff = staffMembers.find(staff => staff.id === idFromUrl());
return ok(queriedStaff);
}
function changeStaffStatus(): Observable<any> {
const queriedStaff = staffMembers.find(staff => staff.id === idFromUrl());
queriedStaff.status = body.status;
const message = `Staff account status changed to ${body.status} successfully!`;
return ok(message);
}
function changeStaffType(): Observable<any> {
const queriedStaff = staffMembers.find(staff => staff.id === idFromUrl());
queriedStaff.accountType = body.accountType;
const message = `Staff account type changed to ${body.accountType} successfully!`;
return ok(message);
}
function getTokens(): Observable<any> {
return ok(tokens); return ok(tokens);
} }
function getTokenBySymbol(): Observable<any> { function getTokenBySymbol(): Observable<HttpResponse<any>> {
const queriedToken = tokens.find(token => token.symbol === stringFromUrl()); const queriedToken: Token = tokens.find(token => token.symbol === stringFromUrl());
return ok(queriedToken); return ok(queriedToken);
} }
function getCategories(): Observable<HttpResponse<any>> {
const categoryList: Array<string> = categories.map(category => category.name);
return ok(categoryList);
}
function getCategoryByProduct(): Observable<HttpResponse<any>> {
const queriedCategory: Category = categories.find(category => category.products.includes(stringFromUrl()));
return ok(queriedCategory.name);
}
function getAreaNames(): Observable<HttpResponse<any>> {
const areaNameList: Array<string> = areaNames.map(areaName => areaName.name);
return ok(areaNameList);
}
function getAreaNameByLocation(): Observable<HttpResponse<any>> {
const queriedAreaName: AreaName = areaNames.find(areaName => areaName.locations.includes(stringFromUrl()));
return ok(queriedAreaName.name);
}
function getAreaTypes(): Observable<HttpResponse<any>> {
const areaTypeList: Array<string> = areaTypes.map(areaType => areaType.name);
return ok(areaTypeList);
}
function getAreaTypeByArea(): Observable<HttpResponse<any>> {
const queriedAreaType: AreaType = areaTypes.find(areaType => areaType.area.includes(stringFromUrl()));
return ok(queriedAreaType.name);
}
function getAccountTypes(): Observable<HttpResponse<any>> {
return ok(accountTypes);
}
function getTransactionTypes(): Observable<HttpResponse<any>> {
return ok(transactionTypes);
}
function getGenders(): Observable<HttpResponse<any>> {
return ok(genders);
}
// helper functions // helper functions
function ok(body): Observable<any> { function ok(responseBody: any): Observable<HttpResponse<any>> {
return of(new HttpResponse({ status: 200, body })); return of(new HttpResponse({ status: 200, body: responseBody }));
} }
function error(message): Observable<any> { function error(message): Observable<any> {
@ -201,12 +364,12 @@ export class MockBackendInterceptor implements HttpInterceptor {
} }
function idFromUrl(): number { function idFromUrl(): number {
const urlParts = url.split('/'); const urlParts: Array<string> = url.split('/');
return parseInt(urlParts[urlParts.length - 1], 10); return parseInt(urlParts[urlParts.length - 1], 10);
} }
function stringFromUrl(): string { function stringFromUrl(): string {
const urlParts = url.split('/'); const urlParts: Array<string> = url.split('/');
return urlParts[urlParts.length - 1]; return urlParts[urlParts.length - 1];
} }
} }

View File

@ -1,24 +1,23 @@
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(','));
}); });

View File

@ -0,0 +1,22 @@
import { validatePerson, validateVcard } from 'cic-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

@ -1,4 +1,4 @@
import {Injectable, isDevMode} from '@angular/core'; import {Injectable} from '@angular/core';
import { import {
HttpRequest, HttpRequest,
HttpHandler, HttpHandler,
@ -6,7 +6,7 @@ import {
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';
@ -22,7 +22,7 @@ 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}`;
@ -37,7 +37,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

@ -13,11 +13,11 @@ 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

@ -18,20 +18,21 @@ export class LoggingInterceptor implements HttpInterceptor {
) {} ) {}
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[];
@ -40,25 +41,25 @@ export interface AccountDetails {
}; };
} }
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: {
@ -94,3 +95,11 @@ export const defaultAccount: AccountDetails = {
}], }],
}, },
}; };
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,29 @@
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,12 @@ export class Settings {
} }
} }
export class W3 { class W3 {
engine: any; engine: any;
provider: any; provider: any;
} }
export {
Settings,
W3
};

View File

@ -1,7 +1,11 @@
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;
@ -13,3 +13,7 @@ export interface Token {
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,31 @@ 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();
@ -76,15 +76,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 _key = await openpgp.key.readArmored(key);
return !_key.err; return !_key.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;
} }

View File

@ -6,28 +6,32 @@ 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} from '@app/_helpers/global-error-handler';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AuthService { export class AuthService {
sessionToken: any; sessionToken: any;
sessionLoginCount = 0; sessionLoginCount: number = 0;
privateKey: any; mutableKeyStore: MutableKeyStore;
mutableKeyStore: MutableKeyStore = new MutablePgpKeyStore();
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')); await this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY')));
} }
} }
@ -36,7 +40,7 @@ export class AuthService {
} }
getWithToken(): void { getWithToken(): void {
const xhr = new XMLHttpRequest(); const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1)); xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1));
xhr.setRequestHeader('Authorization', 'Bearer ' + this.sessionToken); xhr.setRequestHeader('Authorization', 'Bearer ' + this.sessionToken);
@ -53,35 +57,39 @@ export class AuthService {
xhr.send(); xhr.send();
} }
sendResponse(hobaResponseEncoded): void { // TODO rename to send signed challenge and set session. Also separate these responsibilities
const xhr = new XMLHttpRequest(); sendResponse(hobaResponseEncoded: any): Promise<boolean> {
return new Promise((resolve, reject) => {
const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1)); xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1));
xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded); xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded);
xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('x-cic-automerge', 'none'); xhr.setRequestHeader('x-cic-automerge', 'none');
xhr.addEventListener('load', (e) => { xhr.addEventListener('load', (e) => {
if (xhr.status === 401) { if (xhr.status !== 200) {
throw new Error('login rejected'); const error = new HttpError(xhr.statusText, xhr.status);
return reject(error);
} }
this.sessionToken = xhr.getResponseHeader('Token'); this.sessionToken = xhr.getResponseHeader('Token');
sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken); sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken);
this.sessionLoginCount++; this.sessionLoginCount++;
this.setState('Click button to log in'); this.setState('Click button to log in');
return; return resolve(true);
}); });
xhr.send(); xhr.send();
});
} }
getChallenge(): void { getChallenge(): void {
const xhr = new XMLHttpRequest(); const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.responseType = 'arraybuffer'; xhr.responseType = 'arraybuffer';
xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1)); xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1));
xhr.onload = async (e) => { xhr.onload = async (e) => {
if (xhr.status === 401) { if (xhr.status === 401) {
const authHeader = xhr.getResponseHeader('WWW-Authenticate'); const authHeader = xhr.getResponseHeader('WWW-Authenticate');
const o = hobaParseChallengeHeader(authHeader); const o = hobaParseChallengeHeader(authHeader);
await this.loginResponse(o); this.loginResponse(o);
} }
}; };
xhr.send(); xhr.send();
@ -107,13 +115,33 @@ export class AuthService {
} }
async loginResponse(o): Promise<any> { async loginResponse(o: { challenge: string, realm: any }): Promise<any> {
return new Promise(async (resolve, reject) => {
try { try {
const r = await signChallenge(o.challenge, o.realm, environment.cicMetaUrl, this.mutableKeyStore); const r = await signChallenge(o.challenge,
this.sendResponse(r); o.realm,
environment.cicMetaUrl,
this.mutableKeyStore);
const sessionTokenResult: boolean = await this.sendResponse(r);
} catch (error) { } catch (error) {
this.errorDialogService.openDialog({message: 'Incorrect key passphrase.'}); if (error instanceof HttpError) {
if (error.status === 403) {
this.errorDialogService.openDialog({ message: 'You are not authorized to use this system' });
} }
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.'
});
}
}
// TODO define this error
this.errorDialogService.openDialog({message: 'Incorrect key passphrase.'});
resolve(false);
}
});
} }
loginView(): void { loginView(): void {
@ -128,10 +156,11 @@ export class AuthService {
if (!isValidKeyCheck) { if (!isValidKeyCheck) {
throw Error('The private key is invalid'); throw Error('The private key is invalid');
} }
const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored); // TODO leaving this out for now.
if (!isEncryptedKeyCheck) { // const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored);
throw Error('The private key doesn\'t have a password!'); // if (!isEncryptedKeyCheck) {
} // throw Error('The private key doesn\'t have a password!');
// }
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) {
@ -152,18 +181,23 @@ export class AuthService {
} }
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 await fetch(environment.publicKeysUrl)
.then(res => {
if (!res.ok) {
// TODO does angular recommend an error interface?
throw Error(`${res.statusText} - ${res.status}`);
}
return res.text();
});
} }
async getPrivateKeys(): Promise<void> { getPrivateKey(): any {
if (this.privateKey !== undefined) { return this.mutableKeyStore.getPrivateKey();
await this.mutableKeyStore.importPrivateKey(this.privateKey);
}
} }
} }

View File

@ -1,12 +1,11 @@
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'
@ -14,23 +13,20 @@ import {LoggingService} from '@app/_services/logging.service';
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> => {
@ -50,7 +46,7 @@ export class BlockSyncService {
readyStateProcessor(settings: Settings, bit: number, address: string, offset: number, limit: number): void { readyStateProcessor(settings: Settings, bit: number, address: string, offset: number, limit: number): void {
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);
}; };
@ -69,7 +65,7 @@ export class BlockSyncService {
} }
} }
newTransferEvent(tx): any { newTransferEvent(tx: any): any {
return new CustomEvent('cic_transfer', { return new CustomEvent('cic_transfer', {
detail: { detail: {
tx, tx,
@ -77,7 +73,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 +81,8 @@ export class BlockSyncService {
}); });
} }
async scan(settings, lo, hi, bloomBlockBytes, bloomBlocktxBytes, bloomRounds): Promise<void> { async scan(settings: Settings, lo: number, hi: number, bloomBlockBytes: Uint8Array, bloomBlocktxBytes: Uint8Array, bloomRounds: any): Promise<void> {
const w = new Worker('./../assets/js/block-sync/ondemand.js'); 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);
}; };
@ -103,12 +99,12 @@ export class BlockSyncService {
} }
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,5 +1,5 @@
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({
@ -17,7 +17,7 @@ export class ErrorDialogService {
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
}); });

View File

@ -1,5 +1,5 @@
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';
@ -8,15 +8,24 @@ import {HttpClient} from '@angular/common/http';
providedIn: 'root' providedIn: 'root'
}) })
export class LocationService { export class LocationService {
locations: any = '';
private locationsList = new BehaviorSubject<any>(this.locations);
locationsSubject = this.locationsList.asObservable();
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
) { } ) { }
getLocations(): void { getAreaNames(): Observable<any> {
this.httpClient.get(`${environment.cicCacheUrl}/locations`).pipe(first()).subscribe(res => this.locationsList.next(res)); return this.httpClient.get(`${environment.cicMetaUrl}/areanames`);
}
getAreaNameByLocation(location: string): Observable<any> {
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

@ -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,28 @@
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, 'CICRegistry', 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,34 @@
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 httpClient: HttpClient,
) { } private registryService: RegistryService,
) {
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 +36,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

@ -13,9 +13,10 @@ 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({
@ -26,15 +27,20 @@ export class TransactionService {
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}`);
@ -100,7 +106,7 @@ export class TransactionService {
} }
async transferRequest(tokenAddress: string, senderAddress: string, recipientAddress: string, value: number): Promise<any> { async transferRequest(tokenAddress: string, senderAddress: string, recipientAddress: string, value: number): Promise<any> {
const transferAuthAddress = await this.registry.addressOf('TransferAuthorization'); 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();
@ -110,7 +116,7 @@ export class TransactionService {
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

@ -1,14 +1,19 @@
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({
@ -16,49 +21,58 @@ const vCard = require('vcard-parser');
}) })
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<Array<AccountDetails>>(this.accounts);
private accountsList = new BehaviorSubject<any>(this.accounts); accountsSubject: Observable<Array<AccountDetails>> = this.accountsList.asObservable();
accountsSubject = 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(address: string, name: string, phoneNumber: string, age: string, type: string, bio: string, gender: string,
businessCategory: string, userLocation: string, location: string, locationType: string, metaAccount: MetaResponse 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,16 +84,17 @@ 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 in 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 => { }, async error => {
this.loggingService.sendErrorLevelMessage('Can\'t find account info in meta service', this, {error}); this.loggingService.sendErrorLevelMessage('Can\'t find account info in meta service', this, {error});
@ -90,26 +105,18 @@ export class UserService {
} }
async updateMeta(syncableAccount: Syncable, accountKey: string, headers: HttpHeaders): Promise<any> { async updateMeta(syncableAccount: Syncable, accountKey: string, headers: HttpHeaders): Promise<any> {
const envelope = await this.wrap(syncableAccount , this.signer); const envelope: Envelope = await this.wrap(syncableAccount , this.signer);
const reqBody = envelope.toJSON(); const reqBody: string = envelope.toJSON();
this.httpClient.put(`${environment.cicMetaUrl}/${accountKey}`, reqBody , { headers }).pipe(first()).subscribe(res => { 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 +128,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 +148,71 @@ 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>> {
let 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>> {
let 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

@ -13,7 +13,7 @@ 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,
@ -23,12 +23,19 @@ export class AppComponent implements OnInit {
private swUpdate: SwUpdate 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.loggingService.sendErrorLevelMessage('Unable to load trusted public keys.', this, {error});
// 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 can\'t be reached. Please try again later.'}); this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'});
})).subscribe(this.authService.mutableKeyStore.importPublicKey); // TODO do something to halt user progress...show a sad cicada page 🦗?
}
})(); })();
this.mediaQuery.addListener(this.onResize); this.mediaQuery.addListener(this.onResize);
this.onResize(this.mediaQuery); this.onResize(this.mediaQuery);
@ -46,9 +53,9 @@ export class AppComponent implements OnInit {
// 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');
} }
@ -71,13 +78,13 @@ export class AppComponent implements OnInit {
@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

@ -20,8 +20,8 @@ export class PasswordToggleDirective {
} }
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

@ -14,7 +14,7 @@ export class AuthComponent implements OnInit {
keyForm: FormGroup; keyForm: FormGroup;
submitted: boolean = false; submitted: boolean = false;
loading: boolean = false; loading: boolean = false;
matcher = new CustomErrorStateMatcher(); matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
constructor( constructor(
private authService: AuthService, private authService: AuthService,
@ -26,12 +26,11 @@ export class AuthComponent implements OnInit {
this.keyForm = this.formBuilder.group({ this.keyForm = this.formBuilder.group({
key: ['', Validators.required], key: ['', Validators.required],
}); });
if (this.authService.privateKey !== undefined) { await this.authService.init();
const setKey = await this.authService.setKey(this.authService.privateKey); // if (this.authService.privateKey !== undefined) {
if (setKey && this.authService.sessionToken !== undefined) { // const setKey = await this.authService.setKey(this.authService.privateKey);
this.authService.setState('Click button to log in'); // }
} // }
}
} }
get keyFormStub(): any { return this.keyForm.controls; } get keyFormStub(): any { return this.keyForm.controls; }
@ -47,22 +46,26 @@ export class AuthComponent implements OnInit {
} }
login(): void { login(): void {
const loginStatus = this.authService.login(); // TODO check if we have privatekey
if (loginStatus) { // Send us to home if we have a private key
// talk to meta somehow
// in the error interceptor if 401/403 handle it
// if 200 go /home
if (this.authService.getPrivateKey()) {
this.router.navigate(['/home']); this.router.navigate(['/home']);
} }
} }
switchWindows(): void { switchWindows(): void {
this.authService.sessionToken = undefined; this.authService.sessionToken = undefined;
const divOne = document.getElementById('one'); const divOne: HTMLElement = document.getElementById('one');
const divTwo = document.getElementById('two'); const divTwo: HTMLElement = document.getElementById('two');
this.toggleDisplay(divOne); this.toggleDisplay(divOne);
this.toggleDisplay(divTwo); this.toggleDisplay(divTwo);
} }
toggleDisplay(element: any): void { toggleDisplay(element: any): void {
const style = window.getComputedStyle(element).display; const style: string = window.getComputedStyle(element).display;
if (style === 'block') { if (style === 'block') {
element.style.display = 'none'; element.style.display = 'none';
} else { } else {

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

@ -50,12 +50,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,4 +1,4 @@
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';
@ -6,9 +6,11 @@ import {BlockSyncService, LocationService, LoggingService, TokenService, Transac
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',
@ -18,32 +20,35 @@ const vCard = require('vcard-parser');
}) })
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 +61,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 +78,15 @@ 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 = 'https://blockexplorer.bloxberg.org/address/' + this.accountAddress + '/transactions';
this.userService.getAccountDetailsFromMeta(await User.toKey(this.accountAddress)).pipe(first()).subscribe(async res => { (await this.userService.getAccountByAddress(this.accountAddress, 100)).subscribe(async res => {
this.metaAccount = Envelope.fromJSON(JSON.stringify(res.body)).unwrap(); if (res !== undefined) {
this.account = this.metaAccount.m.data; 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,14 +99,18 @@ 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.getCategories().pipe(first()).subscribe(res => this.categories = res);
this.locationService.getLocations(); this.locationService.getAreaNames().pipe(first()).subscribe(res => this.areaNames = res);
this.locationService.locationsSubject.subscribe(locations => { this.locationService.getAreaTypes().pipe(first()).subscribe(res => this.areaTypes = res);
this.locations = locations; 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 {
@ -131,7 +142,7 @@ 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; }
@ -140,7 +151,7 @@ export class AccountDetailsComponent implements OnInit {
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,10 +161,8 @@ 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;
} }
@ -181,16 +190,18 @@ export class AccountDetailsComponent implements OnInit {
resetPin(): void { resetPin(): void {
if (!confirm('Reset user\'s pin?')) { return; } if (!confirm('Reset user\'s pin?')) { return; }
this.userService.resetPin(this.account.phone).pipe(first()).subscribe(res => { 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,25 @@
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,84 @@
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,13 +3,13 @@ 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' }
]; ];

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

@ -5,6 +5,10 @@ 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',
@ -14,11 +18,12 @@ import {exportCsv} from '@app/_helpers';
}) })
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;
@ -27,14 +32,17 @@ export class AccountsComponent implements OnInit {
private userService: UserService, private userService: UserService,
private loggingService: LoggingService, private loggingService: LoggingService,
private router: Router private router: Router
) { )
{
(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 {
@ -51,7 +59,7 @@ 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 {

View File

@ -7,8 +7,6 @@ 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 {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,10 +22,17 @@ 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,
@ -47,7 +52,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

@ -1,7 +1,9 @@
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',
@ -11,13 +13,17 @@ import {CustomErrorStateMatcher} from '@app/_helpers';
}) })
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 {
@ -33,10 +39,10 @@ export class CreateAccountComponent implements OnInit {
referrer: ['', Validators.required], referrer: ['', Validators.required],
businessCategory: ['', Validators.required] businessCategory: ['', Validators.required]
}); });
this.locationService.getLocations(); this.userService.getCategories().pipe(first()).subscribe(res => this.categories = res);
this.locationService.locationsSubject.subscribe(locations => { this.locationService.getAreaNames().pipe(first()).subscribe(res => this.areaNames = res);
this.locations = locations; 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; }
@ -46,8 +52,4 @@ export class CreateAccountComponent implements OnInit {
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

@ -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',
@ -22,9 +23,9 @@ import {exportCsv} from '@app/_helpers';
}) })
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;
@ -39,7 +40,6 @@ export class AdminComponent implements OnInit {
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);
}); });
} }

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

@ -1,8 +1,4 @@
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',
@ -10,181 +6,8 @@ import {ArraySum} from '@app/_helpers';
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

@ -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>

View File

@ -1,37 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InviteComponent } from '@pages/settings/invite/invite.component';
import {FooterStubComponent, SidebarStubComponent, TopbarStubComponent} from '@src/testing';
import {SettingsModule} from '@pages/settings/settings.module';
import {AppModule} from '@app/app.module';
describe('InviteComponent', () => {
let component: InviteComponent;
let fixture: ComponentFixture<InviteComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
InviteComponent,
FooterStubComponent,
SidebarStubComponent,
TopbarStubComponent
],
imports: [
AppModule,
SettingsModule,
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InviteComponent);
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-invite',
templateUrl: './invite.component.html',
styleUrls: ['./invite.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class InviteComponent implements OnInit {
inviteForm: FormGroup;
submitted: boolean = false;
matcher = new CustomErrorStateMatcher();
constructor(
private formBuilder: FormBuilder
) { }
ngOnInit(): void {
this.inviteForm = this.formBuilder.group({
email: ['', Validators.required],
role: ['', Validators.required]
});
}
get inviteFormStub(): any { return this.inviteForm.controls; }
invite(): void {
this.submitted = true;
if (this.inviteForm.invalid || !confirm('Invite user?')) { return; }
this.submitted = false;
}
}

View File

@ -11,7 +11,7 @@ import {CustomErrorStateMatcher} from '@app/_helpers';
export class OrganizationComponent implements OnInit { export class OrganizationComponent implements OnInit {
organizationForm: FormGroup; organizationForm: FormGroup;
submitted: boolean = false; submitted: boolean = false;
matcher = new CustomErrorStateMatcher(); matcher: CustomErrorStateMatcher = new CustomErrorStateMatcher();
constructor( constructor(
private formBuilder: FormBuilder private formBuilder: FormBuilder

View File

@ -2,12 +2,10 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { SettingsComponent } from '@pages/settings/settings.component'; import { SettingsComponent } from '@pages/settings/settings.component';
import {InviteComponent} from '@pages/settings/invite/invite.component';
import {OrganizationComponent} from '@pages/settings/organization/organization.component'; import {OrganizationComponent} from '@pages/settings/organization/organization.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: SettingsComponent }, { path: '', component: SettingsComponent },
{ path: 'invite', component: InviteComponent },
{ path: 'organization', component: OrganizationComponent }, { path: 'organization', component: OrganizationComponent },
{ path: '**', redirectTo: '', pathMatch: 'full' } { path: '**', redirectTo: '', pathMatch: 'full' }
]; ];

View File

@ -15,8 +15,8 @@ import {exportCsv} from '@app/_helpers';
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
date: string; date: string;
dataSource: MatTableDataSource<any>; dataSource: MatTableDataSource<any>;
displayedColumns = ['name', 'email', 'userId']; displayedColumns: Array<string> = ['name', 'email', 'userId'];
trustedUsers: Staff[]; trustedUsers: Array<Staff>;
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;

View File

@ -4,7 +4,6 @@ import { CommonModule } from '@angular/common';
import { SettingsRoutingModule } from '@pages/settings/settings-routing.module'; import { SettingsRoutingModule } from '@pages/settings/settings-routing.module';
import { SettingsComponent } from '@pages/settings/settings.component'; import { SettingsComponent } from '@pages/settings/settings.component';
import {SharedModule} from '@app/shared/shared.module'; import {SharedModule} from '@app/shared/shared.module';
import { InviteComponent } from '@pages/settings/invite/invite.component';
import { OrganizationComponent } from '@pages/settings/organization/organization.component'; import { OrganizationComponent } from '@pages/settings/organization/organization.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';
@ -22,7 +21,7 @@ import {ReactiveFormsModule} from '@angular/forms';
@NgModule({ @NgModule({
declarations: [SettingsComponent, InviteComponent, OrganizationComponent], declarations: [SettingsComponent, OrganizationComponent],
imports: [ imports: [
CommonModule, CommonModule,
SettingsRoutingModule, SettingsRoutingModule,

View File

@ -2,6 +2,7 @@ import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
import {ActivatedRoute, Params} from '@angular/router'; import {ActivatedRoute, Params} from '@angular/router';
import {TokenService} from '@app/_services'; import {TokenService} from '@app/_services';
import {first} from 'rxjs/operators'; import {first} from 'rxjs/operators';
import {Token} from '../../../_models';
@Component({ @Component({
selector: 'app-token-details', selector: 'app-token-details',
@ -10,7 +11,7 @@ import {first} from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TokenDetailsComponent implements OnInit { export class TokenDetailsComponent implements OnInit {
token: any; token: Token;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,

View File

@ -5,6 +5,8 @@ import {LoggingService, TokenService} from '@app/_services';
import {MatTableDataSource} from '@angular/material/table'; import {MatTableDataSource} from '@angular/material/table';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {exportCsv} from '@app/_helpers'; import {exportCsv} from '@app/_helpers';
import {TokenRegistry} from '../../_eth';
import {Token} from '../../_models';
@Component({ @Component({
selector: 'app-tokens', selector: 'app-tokens',
@ -14,10 +16,10 @@ import {exportCsv} from '@app/_helpers';
}) })
export class TokensComponent implements OnInit { export class TokensComponent implements OnInit {
dataSource: MatTableDataSource<any>; dataSource: MatTableDataSource<any>;
columnsToDisplay = ['name', 'symbol', 'address', 'supply']; columnsToDisplay: Array<string> = ['name', 'symbol', 'address', 'supply'];
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
tokens: any; tokens: Array<Promise<string>>;
constructor( constructor(
private tokenService: TokenService, private tokenService: TokenService,
@ -26,13 +28,14 @@ export class TokensComponent implements OnInit {
) { } ) { }
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.tokenService.LoadEvent.subscribe(async () => {
this.tokens = await this.tokenService.getTokens();
});
this.tokens = await this.tokenService.getTokens(); this.tokens = await this.tokenService.getTokens();
this.loggingService.sendInfoLevelMessage(this.tokens); this.loggingService.sendInfoLevelMessage(this.tokens);
this.tokenService.tokensSubject.subscribe(tokens => { this.dataSource = new MatTableDataSource(this.tokens);
this.dataSource = new MatTableDataSource(tokens);
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
});
} }
doFilter(value: string): void { doFilter(value: string): void {

View File

@ -13,12 +13,20 @@
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item"> <li class="list-group-item">
<span>Sender: {{transaction.sender?.vcard.fn[0].value}}</span><br><br> <span>Sender: {{transaction.sender?.vcard.fn[0].value}}</span><br><br>
<span>Sender Address: <a href="{{senderBloxbergLink}}" target="_blank"> {{transaction.from}} </a></span><br><br> <span>
Sender Address:
<a href="{{senderBloxbergLink}}" target="_blank"> {{transaction.from}} </a>
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.from)" alt="Copy">
</span><br><br>
<button mat-raised-button color="primary" class="btn btn-outline-info" (click)="viewSender()">View Sender</button> <button mat-raised-button color="primary" class="btn btn-outline-info" (click)="viewSender()">View Sender</button>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<span>Recipient: {{transaction.recipient?.vcard.fn[0].value}}</span><br><br> <span>Recipient: {{transaction.recipient?.vcard.fn[0].value}}</span><br><br>
<span>Recipient Address: <a href="{{recipientBloxbergLink}}" target="_blank"> {{transaction.to}} </a></span><br><br> <span>
Recipient Address:
<a href="{{recipientBloxbergLink}}" target="_blank"> {{transaction.to}} </a>
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.to)" alt="Copy">
</span><br><br>
<button mat-raised-button color="primary" class="btn btn-outline-info" (click)="viewRecipient()">View Recipient</button> <button mat-raised-button color="primary" class="btn btn-outline-info" (click)="viewRecipient()">View Recipient</button>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
@ -28,7 +36,11 @@
<h4 class="mt-2">Token: </h4> <h4 class="mt-2">Token: </h4>
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item"> <li class="list-group-item">
<span>Address: {{transaction.token._address}}</span> <span>
Address:
{{transaction.token._address}}
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.token._address)" alt="Copy">
</span>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<span>Name: Sarafu Token</span> <span>Name: Sarafu Token</span>
@ -73,17 +85,25 @@
<span><strong>Trader: {{transaction.sender?.vcard.fn[0].value}}</strong></span> <span><strong>Trader: {{transaction.sender?.vcard.fn[0].value}}</strong></span>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<span>Trader Address: {{transaction.trader}}</span> <span>
Trader Address:
<a href="{{traderBloxbergLink}}" target="_blank"> {{transaction.trader}} </a>
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.trader)" alt="Copy">
</span>
</li> </li>
</ul> </ul>
<button mat-raised-button color="primary" class="btn btn-outline-info" routerLink="/accounts/1">View Trader</button> <button mat-raised-button color="primary" class="btn btn-outline-info" (click)="viewTrader()">View Trader</button>
<br><br> <br><br>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h4>Source Token: </h4> <h4>Source Token: </h4>
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item"> <li class="list-group-item">
<span>Address: {{transaction.sourceToken.address}}</span> <span>
Address:
{{transaction.sourceToken.address}}
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.sourceToken.address)" alt="Copy">
</span>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<span>Name: {{transaction.sourceToken.name}}</span> <span>Name: {{transaction.sourceToken.name}}</span>
@ -100,7 +120,11 @@
<h4>Destination Token: </h4> <h4>Destination Token: </h4>
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item"> <li class="list-group-item">
<span>Address: {{transaction.destinationToken.address}}</span> <span>
Address:
{{transaction.destinationToken.address}}
<img src="assets/images/checklist.svg" class="ml-2" height="20rem" (click)="copyAddress(transaction.destinationToken.address)" alt="Copy">
</span>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<span>Name: {{transaction.destinationToken.name}}</span> <span>Name: {{transaction.destinationToken.name}}</span>

View File

@ -1,6 +1,9 @@
import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {TransactionService} from '@app/_services'; import {TransactionService} from '@app/_services';
import {copyToClipboard} from '@app/_helpers';
import {MatSnackBar} from '@angular/material/snack-bar';
import {strip0x} from '@src/assets/js/ethtx/dist/hex';
@Component({ @Component({
selector: 'app-transaction-details', selector: 'app-transaction-details',
@ -12,23 +15,33 @@ export class TransactionDetailsComponent implements OnInit {
@Input() transaction; @Input() transaction;
senderBloxbergLink: string; senderBloxbergLink: string;
recipientBloxbergLink: string; recipientBloxbergLink: string;
traderBloxbergLink: string;
constructor( constructor(
private router: Router, private router: Router,
private transactionService: TransactionService private transactionService: TransactionService,
private snackBar: MatSnackBar,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
if (this.transaction?.type === 'conversion') {
this.traderBloxbergLink = 'https://blockexplorer.bloxberg.org/address/' + this.transaction?.trader + '/transactions';
} else {
this.senderBloxbergLink = 'https://blockexplorer.bloxberg.org/address/' + this.transaction?.from + '/transactions'; this.senderBloxbergLink = 'https://blockexplorer.bloxberg.org/address/' + this.transaction?.from + '/transactions';
this.recipientBloxbergLink = 'https://blockexplorer.bloxberg.org/address/' + this.transaction?.to + '/transactions'; this.recipientBloxbergLink = 'https://blockexplorer.bloxberg.org/address/' + this.transaction?.to + '/transactions';
} }
}
async viewSender(): Promise<void> { async viewSender(): Promise<void> {
await this.router.navigateByUrl(`/accounts/${this.transaction.from}`); await this.router.navigateByUrl(`/accounts/${strip0x(this.transaction.from)}`);
} }
async viewRecipient(): Promise<void> { async viewRecipient(): Promise<void> {
await this.router.navigateByUrl(`/accounts/${this.transaction.to}`); await this.router.navigateByUrl(`/accounts/${strip0x(this.transaction.to)}`);
}
async viewTrader(): Promise<void> {
await this.router.navigateByUrl(`/accounts/${strip0x(this.transaction.trader)}`);
} }
async reverseTransaction(): Promise<void> { async reverseTransaction(): Promise<void> {
@ -39,4 +52,10 @@ export class TransactionDetailsComponent implements OnInit {
this.transaction.value this.transaction.value
); );
} }
copyAddress(address: string): void {
if (copyToClipboard(address)) {
this.snackBar.open(address + ' copied successfully!', 'Close', { duration: 3000 });
}
}
} }

View File

@ -29,11 +29,9 @@
<mat-label> TRANSFER TYPE </mat-label> <mat-label> TRANSFER TYPE </mat-label>
<mat-select id="typeSelect" [(value)]="transactionsType" (selectionChange)="filterTransactions()"> <mat-select id="typeSelect" [(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()"> EXPORT </button> <button mat-raised-button color="primary" type="button" class="btn btn-outline-primary ml-auto mr-2" (click)="downloadCsv()"> EXPORT </button>

View File

@ -1,9 +1,11 @@
import {AfterViewInit, ChangeDetectionStrategy, Component, OnInit, ViewChild} from '@angular/core'; import {AfterViewInit, ChangeDetectionStrategy, Component, OnInit, ViewChild} from '@angular/core';
import {BlockSyncService, TransactionService} from '@app/_services'; import {BlockSyncService, TransactionService, UserService} from '@app/_services';
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 {exportCsv} from '@app/_helpers'; import {exportCsv} from '@app/_helpers';
import {first} from 'rxjs/operators';
import {Transaction} from '@app/_models';
@Component({ @Component({
selector: 'app-transactions', selector: 'app-transactions',
@ -13,12 +15,13 @@ import {exportCsv} from '@app/_helpers';
}) })
export class TransactionsComponent implements OnInit, AfterViewInit { export class TransactionsComponent implements OnInit, AfterViewInit {
transactionDataSource: MatTableDataSource<any>; transactionDataSource: MatTableDataSource<any>;
transactionDisplayedColumns = ['sender', 'recipient', 'value', 'created', 'type']; transactionDisplayedColumns: Array<string> = ['sender', 'recipient', 'value', 'created', 'type'];
defaultPageSize = 10; defaultPageSize: number = 10;
pageSizeOptions = [10, 20, 50, 100]; pageSizeOptions: Array<number> = [10, 20, 50, 100];
transactions: any[]; transactions: Array<Transaction>;
transaction: any; transaction: Transaction;
transactionsType = 'all'; transactionsType: string = 'all';
transactionsTypes: Array<string>;
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ -26,6 +29,7 @@ export class TransactionsComponent implements OnInit, AfterViewInit {
constructor( constructor(
private blockSyncService: BlockSyncService, private blockSyncService: BlockSyncService,
private transactionService: TransactionService, private transactionService: TransactionService,
private userService: UserService
) { ) {
this.blockSyncService.blockSync(); this.blockSyncService.blockSync();
} }
@ -37,6 +41,7 @@ export class TransactionsComponent implements OnInit, AfterViewInit {
this.transactionDataSource.sort = this.sort; this.transactionDataSource.sort = this.sort;
this.transactions = transactions; this.transactions = transactions;
}); });
this.userService.getTransactionTypes().pipe(first()).subscribe(res => this.transactionsTypes = res);
} }
viewTransaction(transaction): void { viewTransaction(transaction): void {

View File

@ -17,6 +17,7 @@ import {MatIconModule} from '@angular/material/icon';
import {MatSelectModule} from '@angular/material/select'; import {MatSelectModule} from '@angular/material/select';
import {MatCardModule} from '@angular/material/card'; import {MatCardModule} from '@angular/material/card';
import {MatRippleModule} from '@angular/material/core'; import {MatRippleModule} from '@angular/material/core';
import {MatSnackBarModule} from '@angular/material/snack-bar';
@NgModule({ @NgModule({
@ -39,7 +40,8 @@ import {MatRippleModule} from '@angular/material/core';
MatIconModule, MatIconModule,
MatSelectModule, MatSelectModule,
MatCardModule, MatCardModule,
MatRippleModule MatRippleModule,
MatSnackBarModule,
] ]
}) })
export class TransactionsModule { } export class TransactionsModule { }

View File

@ -18,15 +18,15 @@ export class MenuSelectionDirective {
} }
onMenuSelect(): void { onMenuSelect(): void {
const sidebar = document.getElementById('sidebar'); const sidebar: HTMLElement = document.getElementById('sidebar');
if (!sidebar?.classList.contains('active')) { if (!sidebar?.classList.contains('active')) {
sidebar?.classList.add('active'); sidebar?.classList.add('active');
} }
const content = document.getElementById('content'); const content: HTMLElement = document.getElementById('content');
if (!content?.classList.contains('active')) { if (!content?.classList.contains('active')) {
content?.classList.add('active'); content?.classList.add('active');
} }
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');
} }

View File

@ -16,11 +16,11 @@ export class MenuToggleDirective {
// Menu Trigger // Menu Trigger
onMenuToggle(): void { onMenuToggle(): void {
const sidebar = document.getElementById('sidebar'); const sidebar: HTMLElement = document.getElementById('sidebar');
sidebar?.classList.toggle('active'); sidebar?.classList.toggle('active');
const content = document.getElementById('content'); const content: HTMLElement = document.getElementById('content');
content?.classList.toggle('active'); content?.classList.toggle('active');
const sidebarCollapse = document.getElementById('sidebarCollapse'); const sidebarCollapse: HTMLElement = document.getElementById('sidebarCollapse');
sidebarCollapse?.classList.toggle('active'); sidebarCollapse?.classList.toggle('active');
} }
} }

View File

@ -0,0 +1,8 @@
import { SafePipe } from './safe.pipe';
describe('SafePipe', () => {
it('create an instance', () => {
const pipe = new SafePipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Pipe, PipeTransform } from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';
@Pipe({
name: 'safe'
})
export class SafePipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
transform(url: string, ...args: unknown[]): unknown {
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
}

View File

@ -8,8 +8,9 @@ import { MenuToggleDirective } from '@app/shared/_directives/menu-toggle.directi
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {MatIconModule} from '@angular/material/icon'; import {MatIconModule} from '@angular/material/icon';
import {TokenRatioPipe} from '@app/shared/_pipes/token-ratio.pipe'; import {TokenRatioPipe} from '@app/shared/_pipes/token-ratio.pipe';
import { ErrorDialogComponent } from './error-dialog/error-dialog.component'; import { ErrorDialogComponent } from '@app/shared/error-dialog/error-dialog.component';
import {MatDialogModule} from '@angular/material/dialog'; import {MatDialogModule} from '@angular/material/dialog';
import { SafePipe } from '@app/shared/_pipes/safe.pipe';
import { NetworkStatusComponent } from './network-status/network-status.component'; import { NetworkStatusComponent } from './network-status/network-status.component';
@ -23,6 +24,7 @@ import { NetworkStatusComponent } from './network-status/network-status.componen
MenuToggleDirective, MenuToggleDirective,
TokenRatioPipe, TokenRatioPipe,
ErrorDialogComponent, ErrorDialogComponent,
SafePipe,
NetworkStatusComponent NetworkStatusComponent
], ],
exports: [ exports: [
@ -31,6 +33,7 @@ import { NetworkStatusComponent } from './network-status/network-status.componen
SidebarComponent, SidebarComponent,
MenuSelectionDirective, MenuSelectionDirective,
TokenRatioPipe, TokenRatioPipe,
SafePipe,
NetworkStatusComponent NetworkStatusComponent
], ],
imports: [ imports: [

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -0,0 +1 @@
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"bytes32","name":"_key","type":"bytes32"}],"name":"addressOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_idx","type":"uint256"}],"name":"entry","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"entryCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"register","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"registry","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"_interfaceCode","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}]

View File

@ -11,6 +11,6 @@ export const environment = {
cicCacheUrl: 'https://cache.dev.grassrootseconomics.net', cicCacheUrl: 'https://cache.dev.grassrootseconomics.net',
web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net', web3Provider: 'wss://bloxberg-ws.dev.grassrootseconomics.net',
cicUssdUrl: 'https://ussd.dev.grassrootseconomics.net', cicUssdUrl: 'https://ussd.dev.grassrootseconomics.net',
registryAddress: '0xAf1B487491073C2d49136Db3FD87E293302CF839', registryAddress: '0xea6225212005e86a4490018ded4bf37f3e772161',
trustedDeclaratorAddress: '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C' trustedDeclaratorAddress: '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C'
}; };

View File

@ -1,18 +1,23 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
.declaration-order {
/* Positioning */
/* Box-model */
/* Typography */
/* Visual */
/* Misc */
}
@import "~bootstrap/dist/css/bootstrap.css"; @import "~bootstrap/dist/css/bootstrap.css";
@import "https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"; @import "https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap";
html, body { height: 100%; } html,
body { height: 100%; }
body { body {
margin: 0; margin: 0;
font-family: Roboto, 'Roboto', "Helvetica Neue", sans-serif; font-family: Roboto, 'Roboto', sans-serif;
background: #fafafa; background: #fafafa;
} }
.full-width, table {
width: 100%;
}
.bg-dark { .bg-dark {
background: #313a46; background: #313a46;
} }
@ -22,16 +27,20 @@ p {
font-size: 1.1em; font-size: 1.1em;
font-weight: 300; font-weight: 300;
line-height: 1.7em; line-height: 1.7em;
color: #000000; color: #000;
} }
a, a:hover, a:focus { a,
color: inherit; a:hover,
a:focus {
text-decoration: none; text-decoration: none;
transition: all 0.3s; color: inherit;
transition: all .3s;
} }
.wrapper { .wrapper {
perspective: 1500px;
align-items: stretch;
display: flex; display: flex;
width: 100%; width: 100%;
align-items: stretch; align-items: stretch;
@ -39,18 +48,34 @@ a, a:hover, a:focus {
perspective: 1500px; perspective: 1500px;
} }
ul ul a {
padding-left: 30px;
font-size: .9em;
background: #6d7fcc;
}
.full-width,
table { width: 100%; }
li.breadcrumb-item.active,
footer.footer { color: black; }
.clipboard {
position: absolute;
left: -9999px;
}
#sidebar { #sidebar {
min-width: 250px;
max-width: 250px;
height: 100vh;
position: -webkit-sticky;
position: sticky; position: sticky;
position: -webkit-sticky;
top: 0; top: 0;
left: 0; left: 0;
z-index: 9999; z-index: 9999;
min-width: 250px;
max-width: 250px;
height: 100vh;
background: #313a46; background: #313a46;
color: #fff; color: #fff;
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665);
transform-origin: center left; transform-origin: center left;
} }
@ -58,7 +83,6 @@ a, a:hover, a:focus {
min-width: 100px; min-width: 100px;
max-width: 100px; max-width: 100px;
text-align: center; text-align: center;
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665);
} }
#sidebar .sidebar-header { #sidebar .sidebar-header {
@ -66,55 +90,44 @@ a, a:hover, a:focus {
background: #313a46; background: #313a46;
} }
#sidebar .sidebar-header strong { #sidebar .sidebar-header strong,
display: none; #sidebar.active .sidebar-header h3 { display: none; }
}
#sidebar.active .sidebar-header h3 { #sidebar.active .sidebar-header strong { display: block; }
display: none;
}
#sidebar.active .sidebar-header strong {
display: block;
}
#sidebar ul li a { #sidebar ul li a {
color: #ffffff; display: block;
padding: 10px;
text-align: left; text-align: left;
font-size: 1.1em;
color: #fff;
} }
#sidebar ul li a.active { #sidebar ul li a.active,
background: #000000; #sidebar.active ul li a.active { background: #000; }
}
#sidebar.active ul li a { #sidebar.active ul li a {
padding: 20px 10px; padding: 20px 10px;
font-size: .85em;
text-align: center; text-align: center;
font-size: 0.85em;
}
#sidebar.active ul li a.active {
background: #000000;
} }
#sidebar.active ul li a i { #sidebar.active ul li a i {
margin-right: 0;
display: block; display: block;
font-size: 1.8em; margin-right: 0;
margin-bottom: 5px; margin-bottom: 5px;
font-size: 1.8em;
} }
#sidebar.active ul li a { #sidebar.active ul li a { padding: 10px; }
padding: 10px !important;
}
#sidebar.active .dropdown-toggle::after { #sidebar.active .dropdown-toggle::after {
top: auto; top: auto;
bottom: 10px; bottom: 10px;
right: 50%; right: 50%;
-webkit-transform: translateX(50%);
-ms-transform: translateX(50%);
transform: translateX(50%); transform: translateX(50%);
-ms-transform: translateX(50%);
-webkit-transform: translateX(50%);
} }
#sidebar ul.components { #sidebar ul.components {
@ -123,14 +136,8 @@ a, a:hover, a:focus {
} }
#sidebar ul p { #sidebar ul p {
padding: 10px;
color: #fff; color: #fff;
padding: 10px;
}
#sidebar ul li a {
padding: 10px;
font-size: 1.1em;
display: block;
} }
#sidebar ul li a:hover { #sidebar ul li a:hover {
@ -138,43 +145,32 @@ a, a:hover, a:focus {
background: #fff; background: #fff;
} }
#sidebar ul li.active > a, a[aria-expanded="true"] { #sidebar ul li.active > a,
a[aria-expanded="true"] {
color: #fff; color: #fff;
background: #313a46; background: #313a46;
} }
ul ul a { a[data-toggle="collapse"] { position: relative; }
font-size: 0.9em !important;
padding-left: 30px !important;
}
li.breadcrumb-item.active, footer.footer {
color: black;
}
a[data-toggle="collapse"] {
position: relative;
}
.dropdown-toggle::after { .dropdown-toggle::after {
display: block;
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 20%; right: 20%;
display: block;
transform: translateY(-50%); transform: translateY(-50%);
} }
#content { #content {
width: 100%;
position: relative; position: relative;
overflow: auto; overflow: auto;
width: 100%;
min-height: 100vh; min-height: 100vh;
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665);
}
#content.active {
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665);
} }
#sidebar,
#sidebar.active,
#content,
#content.active { transition: all .3s cubic-bezier(.945, .020, .270, .665); }
#sidebarCollapse { #sidebarCollapse {
width: 40px; width: 40px;
@ -183,84 +179,57 @@ a[data-toggle="collapse"] {
} }
#sidebarCollapse span { #sidebarCollapse span {
display: block;
width: 80%; width: 80%;
height: 2px; height: 2px;
margin: 0 auto; margin: 0 auto;
display: block;
background: #555; background: #555;
transition: all 0.8s cubic-bezier(0.810, -0.330, 0.345, 1.375); transition: all .8s cubic-bezier(.810, -.330, .345, 1.375);
} }
#sidebarCollapse span:first-of-type { #sidebarCollapse span:first-of-type { transform: rotate(45deg) translate(2px, 2px); }
/* rotate first one */
transform: rotate(45deg) translate(2px, 2px);
}
#sidebarCollapse span:nth-of-type(2) { #sidebarCollapse span:nth-of-type(2) { opacity: 0; }
/* second one is not visible */
opacity: 0;
}
#sidebarCollapse span:last-of-type { #sidebarCollapse span:last-of-type { transform: rotate(-45deg) translate(1px, -1px); }
/* rotate third one */
transform: rotate(-45deg) translate(1px, -1px);
}
#sidebarCollapse.active span { #sidebarCollapse.active span {
/* no rotation */
transform: none;
/* all bars are visible */
opacity: 1;
margin: 5px auto; margin: 5px auto;
transform: none;
opacity: 1;
} }
.footer { .footer {
color: #98a6ad;
padding: 10px; padding: 10px;
text-align: center; text-align: center;
color: #98a6ad;
} }
.mat-column-select { .mat-column-select { overflow: initial; }
overflow: initial;
}
button { button { height: 2.5rem; }
height: 2.5rem;
}
.badge-pill { .badge-pill { width: 5rem; }
width: 5rem;
}
.mat-column { .mat-column {
word-wrap: break-word !important; word-wrap: break-word;
white-space: unset !important; white-space: unset;
overflow-wrap: break-word; overflow-wrap: break-word;
word-break: break-word; word-break: break-word;
hyphens: auto;
-ms-hyphens: auto; -ms-hyphens: auto;
-moz-hyphens: auto; -moz-hyphens: auto;
-webkit-hyphens: auto; -webkit-hyphens: auto;
hyphens: auto;
} }
.mat-column-address { .mat-column-address {
flex: 0 0 30% !important; flex: 0 0 30%;
width: 30% !important; width: 30%;
} }
.mat-column-supply { .mat-column-supply {
flex: 0 0 25% !important; flex: 0 0 25%;
width: 25% !important; width: 25%;
}
.mat-column-select {
flex: 0 0 10% !important;
width: 10% !important;
}
.mat-column-view {
flex: 0 0 5% !important;
width: 5% !important;
} }
.center-body { .center-body {
@ -269,92 +238,70 @@ button {
} }
@media (max-width: 768px) { @media (max-width: 768px) {
#sidebar {
margin-left: 0;
}
#sidebar.active { #sidebar.active {
min-width: 100px; min-width: 100px;
max-width: 100px; max-width: 100px;
margin-left: -100px;
text-align: center; text-align: center;
margin-left: -100px !important;
} }
#sidebar .sidebar-header strong { #sidebar .sidebar-header strong,
display: none; #sidebar.active .sidebar-header h3 { display: none; }
}
#sidebar.active .sidebar-header h3 { #sidebar.active .sidebar-header strong,
display: none; #sidebar.active ul li a i { display: block; }
}
#sidebar.active .sidebar-header strong {
display: block;
}
#sidebar.active ul li a { #sidebar.active ul li a {
padding: 20px 10px; padding: 20px 10px;
font-size: 0.85em; font-size: .85em;
} }
#sidebar.active ul li a i { #sidebar.active ul li a i {
margin-right: 0; margin-right: 0;
display: block;
font-size: 1.8em;
margin-bottom: 5px; margin-bottom: 5px;
font-size: 1.8em;
} }
#sidebar.active ul ul a { #sidebar.active ul ul a { padding: 10px; }
padding: 10px !important;
}
.dropdown-toggle::after { .dropdown-toggle::after {
top: auto; top: auto;
bottom: 10px; bottom: 10px;
right: 50%; right: 50%;
-webkit-transform: translateX(50%);
-ms-transform: translateX(50%);
transform: translateX(50%); transform: translateX(50%);
-ms-transform: translateX(50%);
-webkit-transform: translateX(50%);
} }
#content, #content.active { #content,
height: 100vh; #content.active {
position: fixed; position: fixed;
margin-left: 0; height: 100vh;
} }
#content .menutoggle { #content .menutoggle { margin-left: 250px; }
margin-left: 250px;
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665);
}
#content.active .menutoggle { #sidebar,
margin-left: 0; #content,
transition: all 0.3s cubic-bezier(0.945, 0.020, 0.270, 0.665); #content.active,
} #content.active .menutoggle { margin-left: 0; }
#content .menutoggle,
#content.active .menutoggle { transition: all .3s cubic-bezier(.945, .020, .270, .665); }
#sidebarCollapse span:first-of-type, #sidebarCollapse span:first-of-type,
#sidebarCollapse span:nth-of-type(2), #sidebarCollapse span:nth-of-type(2),
#sidebarCollapse span:last-of-type { #sidebarCollapse span:last-of-type {
margin: 5px auto;
transform: none; transform: none;
opacity: 1; opacity: 1;
margin: 5px auto;
} }
#sidebarCollapse.active span { #sidebarCollapse.active span { margin: 0 auto; }
margin: 0 auto;
}
#sidebarCollapse.active span:first-of-type { #sidebarCollapse.active span:first-of-type { transform: rotate(45deg) translate(2px, 2px); }
transform: rotate(45deg) translate(2px, 2px);
}
#sidebarCollapse.active span:nth-of-type(2) { #sidebarCollapse.active span:nth-of-type(2) { opacity: 0; }
opacity: 0;
}
#sidebarCollapse.active span:last-of-type {
transform: rotate(-45deg) translate(1px, -1px);
}
#sidebarCollapse.active span:last-of-type { transform: rotate(-45deg) translate(1px, -1px); }
} }