Initial public commit

This commit is contained in:
Rene Vergara 2022-04-11 09:48:34 -05:00
commit 0e961e165f
Signed by: pitmutt
GPG Key ID: 65122AD495A7F5B2
123 changed files with 27248 additions and 0 deletions

17
.browserslistrc Normal file
View File

@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

48
.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
txs.json
.angular

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright © 2022 Vergara Technologies LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# ZGo - The Zcash Register
A Point-of-Sale application for accepting payments in [Zcash](https://z.cash/)
Visit our [ZGo Homepage](https://zgo.cash/) for more details.

110
angular.json Normal file
View File

@ -0,0 +1,110 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"cli": {
"analytics": false
},
"version": 1,
"newProjectRoot": "projects",
"projects": {
"zgo": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/zgo",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "zgo:build:production"
},
"development": {
"browserTarget": "zgo:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "zgo:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.css"
],
"scripts": []
}
}
}
}
},
"defaultProject": "zgo"
}

631
backend/app.js Normal file
View File

@ -0,0 +1,631 @@
const express = require('express');
const app = express();
const bodyparser = require('body-parser');
const cors = require('cors');
const crypto = require('crypto');
const postmodel = require('./models/post');
const usermodel = require('./models/user');
const ownermodel = require('./models/owner');
const itemmodel = require('./models/item');
const ordermodel = require('./models/order');
const pricemodel = require('./models/price');
const txmodel = require('./models/tx');
const paymentmodel = require('./models/payment');
const zecTxModel = require('./models/zectxs.js');
const countryModel = require('./models/country.js');
const mongoose = require('mongoose');
const stdrpc = require('stdrpc');
const CoinGecko = require('coingecko-api');
var URLSafeBase64 = require('urlsafe-base64');
var Buffer = require('buffer/').Buffer;
var db = require('./config/db');
mongoose.connect('mongodb://'+db.user+':'+db.password+'@'+db.server+'/'+db.database).then(() => {
console.log("connecting-- ", db.database);
}).catch(() => {
console.log("connection failed!");
});
var fullnode = require('./config/fullnode');
const rpc = stdrpc({
url: fullnode.url,
username: fullnode.username,
password: fullnode.password
});
var async = require('async');
const CoinGeckoClient = new CoinGecko();
var intervalObject = setInterval( function() {
CoinGeckoClient.simple.price({
ids: ['zcash'],
vs_currencies: ['usd', 'gbp', 'eur', 'cad', 'aud']
}).then((data) => {
pricemodel.findOneAndUpdate({currency: 'usd'}, { price: data.data.zcash.usd, timestamp: Date.now()}, {new:true, upsert:true}, function(err,docs) {
if(err) {
console.log(err);
}
});
pricemodel.findOneAndUpdate({currency: 'gbp'}, { price: data.data.zcash.gbp, timestamp: Date.now()}, {new:true, upsert:true}, function(err,docs) {
if(err) {
console.log(err);
}
});
pricemodel.findOneAndUpdate({currency: 'eur'}, { price: data.data.zcash.eur, timestamp: Date.now()}, {new:true, upsert:true}, function(err,docs) {
if(err) {
console.log(err);
}
});
pricemodel.findOneAndUpdate({currency: 'cad'}, { price: data.data.zcash.cad, timestamp: Date.now()}, {new:true, upsert:true}, function(err,docs) {
if(err) {
console.log(err);
}
});
pricemodel.findOneAndUpdate({currency: 'aud'}, { price: data.data.zcash.aud, timestamp: Date.now()}, {new:true, upsert:true}, function(err,docs) {
if(err) {
console.log(err);
}
});
}).catch((err) => {
console.log(err);
});
}, 90000);
function hexToString(hexString) {
var str = '';
for (var n=0; n < hexString.length; n +=2) {
str += String.fromCharCode(parseInt(hexString.substr(n, 2), 16));
}
return str;
}
function sendPin(pin, address) {
//var memo = URLSafeBase64.encode(Buffer.from('ZGO pin: '.concat(pin)));
var memo = Buffer.from('ZGo PIN: '.concat(pin)).toString('hex');
//console.log(typeof(memo));
var amounts = [
{
address: address,
amount: 0.00000001,
memo: memo
}
];
rpc.z_sendmany(fullnode.addr, amounts).catch((err) => {
console.log('Sendmany', err);
});
}
var blockInterval = setInterval( function() {
console.log('Node periodic Zcash scan');
rpc.z_listreceivedbyaddress(fullnode.addr, 1).then(txs => {
var re = /.*ZGO::(.*)\sReply-To:\s(z\w+)/;
var pay = /.*ZGOp::(.*)/;
async.each (txs, function(txData, callback) {
var memo = hexToString(txData.memo).replace(/\0/g, '');
if (!txData.change) {
zecTxModel.updateOne({txid: txData.txid}, { txid: txData.txid, confirmations: txData.confirmations, amount:txData.amount, memo: memo}, {new:true, upsert:true}, function(err,docs) {
if (err) {
console.log(err);
}
});
}
if (re.test(memo) && txData.confirmations < 100) {
//console.log('Processing tx:', memo);
var match = re.exec(memo);
if (match != null) {
var address = match[2];
var session = match[1];
var blocktime = txData.blocktime;
var amount = txData.amount;
var expiration = blocktime;
//console.log(' ', session, blocktime);
txmodel.updateOne({txid: txData.txid}, { txid: txData.txid, address: address, session: session, confirmations: txData.confirmations, amount:txData.amount, memo: memo}, {new:true, upsert:true}, function(err,docs) {
if (err) {
console.log(err);
}
});
if (txData.confirmations >= 2 ) {
usermodel.findOne({address: address, session: session, blocktime: blocktime}).then(function(doc){
if (doc == null){
console.log('User not found', session, blocktime);
const n = crypto.randomInt(0, 10000000);
const pin = n.toString().padStart(6, '0');
sendPin(pin, address);
var user = new usermodel({
address: address,
session: session,
blocktime: blocktime,
pin: pin,
validated: false
});
user.save(function(error) {
if (error) {
console.log(error);
}
console.log('User saved');
});
}
});
}
}
}
var exptime = 0;
if (pay.test(memo) && txData.confirmations < 100) {
var match2 = pay.exec(memo);
if (match2 != null) {
var session = match2[1];
}
if (txData.amount >= 0.001 && txData.amount < 0.005){
exptime = txData.blocktime + 3600;
} else if (txData.amount >= 0.005 && txData.amount < 0.025){
exptime = txData.blocktime + 24*3600;
} else if (txData.amount >= 0.025 && txData.amount < 0.1) {
exptime = txData.blocktime + 7*24*3600;
} else if (txData.amount >= 0.1) {
exptime = txData.blocktime + 4*7*24*3600;
}
usermodel.findOne({session: session}).then(function(doc){
if(doc != null) {
paymentmodel.findOne({address: doc.address, blocktime: txData.blocktime, amount: txData.amount}).then(function(payments){
if(payments == null){
var payment = new paymentmodel({
address: doc.address,
blocktime: txData.blocktime,
expiration: new Date(exptime * 1000),
amount: txData.amount,
session: doc.session
});
payment.save(function(error) {
if (error) {
console.log(error);
} else {
console.log('Payment saved');
}
});
}
});
}
});
}
}, function (err) {
if (err) {
console.log(err);
}
console.log('Txs synced');
});
});
}, 75000);
var payCheck = setInterval( function() {
ownermodel.find({}).then((documents) => {
if(documents.length > 0){
//console.log(documents);
async.each (documents, function(document, callback) {
paymentmodel.findOne({ address: document.address, expiration: {$gt: Date.now()}}).then(payment => {
if (payment != null){
document.paid = true;
document.save();
} else {
document.paid = false;
document.save();
}
});
}, function (err) {
if (err) {
console.log(err);
} else {
console.log("Owners checked for payment");
}
});
}
});
}, 60000);
app.use(cors());
app.options('*', cors());
app.use(bodyparser.json());
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS");
next();
});
app.use((req, res, next) => {
if (req.headers.authorization !== 'YourAPIkey' ) {
return res.status(401).send('Authorization required.');
} else {
next();
}
});
app.get('/api/countries', (req, res, next) => {
console.log('Get: /api/countries');
countryModel.find({}).then((documents) => {
if (documents != null) {
res.status(200).json({
message: 'Country data found',
countries: documents
});
} else {
res.status(204).json({
message: 'No country data available'
});
}
});
});
app.get('/api/users', (req, res, next) => {
console.log('Get: /api/users');
usermodel.find({'address': req.query.address, 'session': req.query.session, 'expired': { $ne: true}}).
then((documents) => {
if (documents != null) {
res.status(200).json({
message: 'Users found successfully',
users: documents
});
} else {
res.status(204).json({
message: 'User not found',
users: null
});
}
});
});
app.get('/api/pending', (req, res, next) => {
console.log('Get: /api/pending', req.query.session);
txmodel.find({'session': req.query.session, 'confirmations': {$lt: 10}}).
then((documents) => {
if (documents.length > 0) {
//console.log('pending', documents);
res.status(200).json({
message: 'Found pending txs',
txs: documents
});
} else {
//console.log('pending not found', documents);
res.status(204).json({
message: 'No txs found',
txs: null
});
}
});
});
app.get('/api/getuser', (req, res, next) => {
console.log('Get: /api/getuser/', req.query.session);
usermodel.find({'session': req.query.session, 'expired': { $ne: true }}).
then((documents) => {
if(documents.length > 0){
//console.log(documents);
console.log(' found user');
res.status(200).json({
message: 'User found!',
user: documents
});
} else {
console.log(' did not find user');
res.status(204).json({
message: 'User not found!',
user: null
});
}
});
});
app.get('/api/blockheight', (req, res, next) => {
console.log('Get: /api/blockheight');
rpc.getblock("-1", 1).then(block => {
res.status(200).json({
message: 'Found block',
height: block.height
});
});
});
app.get('/api/txs', (req, res, next) => {
console.log('Get: /api/txs');
rpc.z_listreceivedbyaddress(fullnode.addr, 10).then(txs => {
res.status(200).json({
message: 'Transactions found',
txs: txs
});
});
});
app.get('/api/getaddr', (req, res, next) => {
console.log('Get: /api/getaddr');
res.status(200).json({
message: 'Sending address',
addr: fullnode.addr
});
});
app.get('/api/getowner', (req, res, next) => {
console.log('Get: /api/getowner');
ownermodel.find({'address': req.query.address}).then((documents) => {
if(documents.length > 0){
//console.log(documents);
res.status(200).json({
message: 'Owner found!',
owner: documents
});
} else {
res.status(204).json({
message: 'Owner not found!',
owner: null
});
}
});
});
app.post('/api/addowner', (req, res, next) => {
console.log('Post: /api/addowner');
const owner = new ownermodel(req.body.owner);
owner.save();
res.status(201).json({
message: 'Owner added successfully'
});
});
app.post('/api/validateuser', (req, res, next) => {
console.log('Post: /api/validateuser');
usermodel.findByIdAndUpdate(req.body.user._id, req.body.user,
function(err, docs) {
if (err) {
console.log(err);
} else {
res.status(201).json({
message: 'User Validated',
user: docs
});
}
});
});
app.post('/api/updateowner', (req, res, next) => {
console.log('Post: /api/updateowner');
ownermodel.findByIdAndUpdate(req.body.owner._id, req.body.owner,
function(err, docs) {
if (err) {
console.log(err);
} else {
console.log(docs);
res.status(201).json({
message: 'Owner updated',
owner: docs
});
}
});
});
app.get('/api/getitems', (req, res, next) => {
console.log('Get: /api/getitems');
//console.log('getitems', req.query.address);
if (req.query.address.length > 0 ) {
const items = itemmodel.find({user: req.query.address}).then((documents) => {
if(documents.length > 0){
//console.log(documents);
res.status(200).json({
message: 'items found!',
items: documents
});
} else {
res.status(204).json({
message: 'items not found!',
items: []
});
}
});
} else {
res.status(204).json({
message: 'no address',
items: []
});
}
});
app.post('/api/item', (req, res, next) => {
console.log('Post: /api/item', req.body.item);
if ( req.body.item._id == null ) {
const item = new itemmodel(req.body.item);
item.save();
res.status(201).json({
message: 'Item added'
});
} else {
console.log('Editing', req.body.item._id);
itemmodel.findByIdAndUpdate(req.body.item._id, {'name': req.body.item.name, 'description': req.body.item.description, 'cost': req.body.item.cost},
function(err, docs) {
if (err) {
console.log(err);
} else {
res.status(201).json({
message: 'Item updated'
});
}
});
}
});
app.delete('/api/item/:id', (req, res, next) => {
console.log('delete endpoint', req.params.id);
itemmodel.findByIdAndDelete(req.params.id, function (err, docs) {
if (err) {
console.log(err);
} else {
res.status(200).json({
message: 'Item deleted'
});
}
});
});
app.delete('/api/user/:id', (req, res, next) => {
console.log("Delete user", req.params.id);
usermodel.findByIdAndUpdate(req.params.id, {expired: true}, function(err, docs) {
if (err) {
console.log(err);
} else {
res.status(200).json({
message: 'User session deleted'
});
}
});
});
app.get('/api/price', (req, res, next) => {
console.log('Get /api/price');
const price = pricemodel.findOne({currency: req.query.currency}).then((document) => {
if (document != null) {
res.status(200).json({
message: 'price found!',
price: document
});
} else {
res.status(204).json({
message: 'no price found!',
order: null
});
}
});
});
app.get('/api/allorders', (req, res, next) => {
console.log('Get /api/allorders');
if (req.query.address.length > 0) {
const orders = ordermodel.find({address: req.query.address, closed: true}).then((documents) => {
if (documents != null) {
res.status(200).json({
message: 'orders found!',
orders: documents
});
} else {
res.status(204).json({
message: 'no orders found',
orders: null
});
}
});
}
});
app.get('/api/receipt', (req, res, next) => {
console.log('Get /api/receipt');
if (req.query.id.length > 0) {
const order = ordermodel.findOne({_id: req.query.id}).then((documents) => {
if (documents != null) {
console.log(documents);
res.status(200).json({
message: 'order found!',
order: documents
});
} else {
res.status(204).json({
message: 'no order found!',
order: null
});
}
});
} else {
res.status(204).json({
message: 'no valid ID received',
order: null
});
}
});
app.get('/api/order', (req, res, next) => {
console.log('Get /api/order');
if (req.query.session.length > 0) {
const order = ordermodel.findOne({session: req.query.session, closed: false}).then((documents) => {
if (documents != null) {
console.log(documents);
res.status(200).json({
message: 'order found!',
order: documents
});
} else {
res.status(204).json({
message: 'no order found!',
order: null
});
}
});
} else {
res.status(204).json({
message: 'no session received',
order: null
});
}
});
app.post('/api/order', (req, res, next) => {
console.log('Post /api/order', req.body);
if(req.body.order._id == null) {
const order = new ordermodel(req.body.order);
order.save();
res.status(200).json({
message: 'Order added',
order: order
});
} else {
ordermodel.findByIdAndUpdate(req.body.order._id, {
address: req.body.order.address,
session: req.body.order.session,
price: req.body.order.price,
total: req.body.order.total,
currency: req.body.order.currency,
totalZec: req.body.order.totalZec,
closed: req.body.order.closed
}, function(err, docs) {
if(err) {
console.log(err);
} else {
res.status(200).json({
message: 'Order updated'
});
}
});
}
});
app.post('/api/lineitem', (req, res, next) => {
console.log('Post /api/lineitem');
ordermodel.findByIdAndUpdate(req.body.order_id, { $push: {lines: req.body.line}}, function(err,docs) {
if (err) {
console.log(err);
} else {
res.status(200).json({
message: 'Item added to order'
});
}
});
});
app.delete('/api/order/:id', (req, res, next) => {
console.log('delete order endpoint', req.params.id);
ordermodel.findByIdAndDelete(req.params.id, function (err, docs) {
if (err) {
console.log(err);
} else {
console.log(docs);
res.status(200).json({
message: 'Order deleted'
});
}
});
});
module.exports = app;

6
backend/config/db.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
server : 'localhost:27017',
user: 'dbuser',
password: 'dbpasswd',
database: 'zgo'
}

View File

@ -0,0 +1,6 @@
module.exports = {
url: 'http://localhost:8232',
username: 'user',
password: 'passwd',
addr: 'zaddress123123123123123123....'
}

View File

@ -0,0 +1,9 @@
const mongoose = require('mongoose');
const countrySchema = mongoose.Schema({
name: {type: String, required: true},
code: {type: Number, required: true}
});
module.exports = mongoose.model('Country', countrySchema);

10
backend/models/item.js Normal file
View File

@ -0,0 +1,10 @@
const mongoose = require('mongoose');
const itemSchema = mongoose.Schema({
name: {type: String, required: true},
description: {type: String, required: true},
user: {type: String, required: true},
cost: {type: Number, required: true}
});
module.exports = mongoose.model('Item', itemSchema);

19
backend/models/order.js Normal file
View File

@ -0,0 +1,19 @@
const mongoose = require('mongoose');
const orderSchema = mongoose.Schema({
address: {type: String, required: true},
session: {type: String, required: true},
timestamp: {type: Date, required: true, default: Date.now},
closed: { type: Boolean, required: true, default:false },
price: { type: Number, required: true},
currency: {type: String, required: true},
total: { type: Number},
totalZec: {type: Number},
lines: [{
qty: {type: Number, required: true, default: 1},
name: { type: String, required: true},
cost: { type: Number, required: true, default: 0}
}]
});
module.exports = mongoose.model('Order', orderSchema);

24
backend/models/owner.js Normal file
View File

@ -0,0 +1,24 @@
const mongoose = require('mongoose');
const ownerSchema = mongoose.Schema({
address: {type: String, required:true, unique:true},
name: {type: String, required:true},
currency: {type: String, required:true, default: 'usd'},
tax: {type: Boolean, required: true, default: false},
taxValue: {type: Number },
vat: {type: Boolean, required:true, default: false},
vatValue: {type: Number },
first: {type: String, required:true},
last: {type: String, required:true},
email: {type: String, required:true},
street: {type: String, required:true},
city: {type: String, required: true},
state: {type: String, required: true},
postal: {type: String, required: true},
phone: {type: String},
website: {type: String},
paid: {type: Boolean, required: true, default: false},
zats: {type: Boolean, required: true, default: false}
});
module.exports = mongoose.model('Owner', ownerSchema);

11
backend/models/payment.js Normal file
View File

@ -0,0 +1,11 @@
const mongoose = require('mongoose');
const paymentSchema = mongoose.Schema({
address: {type: String, required: true},
blocktime: {type: Number, required: true},
expiration: {type: Date, required: true},
amount: {type: Number, required: true},
session: {type: String, required: true}
});
module.exports = mongoose.model('Payment', paymentSchema);

8
backend/models/post.js Normal file
View File

@ -0,0 +1,8 @@
const mongoose = require('mongoose');
const postSchema = mongoose.Schema({
title: {type: String, required:true},
content: {type: String, required:true}
});
module.exports = mongoose.model('Post', postSchema);

10
backend/models/price.js Normal file
View File

@ -0,0 +1,10 @@
const mongoose = require('mongoose');
const priceSchema = mongoose.Schema({
currency: {type: String, required: true},
price: {type: Number, required: true},
timestamp: {type: Date, required: true, default: Date.now}
});
module.exports = mongoose.model('Price', priceSchema);

12
backend/models/tx.js Normal file
View File

@ -0,0 +1,12 @@
const mongoose = require('mongoose');
const txSchema = mongoose.Schema({
address: {type: String},
session: {type: String, required:true},
confirmations: {type: Number, required:true},
amount: {type: Number, required:true},
txid: {type:String, required:true, unique: true},
memo: {type:String}
});
module.exports = mongoose.model('Tx', txSchema);

12
backend/models/user.js Normal file
View File

@ -0,0 +1,12 @@
const mongoose = require('mongoose');
const userSchema = mongoose.Schema({
address: {type: String, required:true},
session: {type: String, required:true},
blocktime: {type: Number, required:true},
pin: {type: String, required:true},
validated: {type: Boolean, required:true},
expired: {type: Boolean, required:true, default: false}
});
module.exports = mongoose.model('User', userSchema);

12
backend/models/zectxs.js Normal file
View File

@ -0,0 +1,12 @@
const mongoose = require('mongoose');
const ZecTxSchema = mongoose.Schema({
address: {type: String},
session: {type: String, required:true},
confirmations: {type: Number, required:true},
amount: {type: Number, required:true},
txid: {type:String, required:true, unique: true},
memo: {type:String}
});
module.exports = mongoose.model('ZecTx', ZecTxSchema);

44
karma.conf.js Normal file
View File

@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/zgo'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

22386
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "zgo",
"version": "1.0.3",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "~13.0.2",
"@angular/cdk": "^12.2.8",
"@angular/common": "~13.0.2",
"@angular/compiler": "~13.0.2",
"@angular/core": "~13.0.2",
"@angular/forms": "~13.0.2",
"@angular/material": "^12.2.8",
"@angular/platform-browser": "~13.0.2",
"@angular/platform-browser-dynamic": "~13.0.2",
"@angular/router": "~13.0.2",
"@supercharge/request-ip": "^1.1.2",
"angular-local-storage": "^0.7.1",
"async": "^3.2.2",
"coingecko-api": "^1.0.10",
"easyqrcodejs": "^4.4.6",
"material-design-icons": "^3.0.1",
"mongoose": "^6.0.13",
"rxjs": "~6.6.0",
"stdrpc": "^1.3.0",
"tslib": "^2.3.0",
"urlsafe-base64": "^1.0.0",
"uuid": "^8.3.2",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.0.2",
"@angular/cli": "~13.0.2",
"@angular/compiler-cli": "~13.0.2",
"@types/jasmine": "~3.8.0",
"@types/node": "^12.20.33",
"@types/urlsafe-base64": "^1.0.28",
"@types/uuid": "^8.3.1",
"jasmine-core": "~3.8.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~4.4.4"
}
}

19
server.js Normal file
View File

@ -0,0 +1,19 @@
var fs = require('fs');
var http = require('http');
//var https = require('https');
//var certificate = fs.readFileSync('/etc/letsencrypt/live/zgo.cash/fullchain.pem');
//var privateKey = fs.readFileSync('/etc/letsencrypt/live/zgo.cash/privkey.pem');
//var credentials = {key: privateKey, cert: certificate};
const app = require('./backend/app');
const port = 3000;
app.set('port', port);
const httpServer = http.createServer(app);
//const httpsServer = https.createServer(credentials, app);
httpServer.listen(port);
//httpsServer.listen(port);

View File

@ -0,0 +1,26 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ViewerComponent } from './viewer/viewer.component';
import { ReceiptComponent } from './receipt/receipt.component';
import { LoginComponent } from './login/login.component';
import { BusinessComponent } from './business/business.component';
import { ListOrdersComponent } from './listorders/listorders.component';
import { AuthGuardService } from './auth-guard.service';
import { NodeResolverService } from './node-resolver.service';
const routes: Routes = [
{ path: '', component: LoginComponent, resolve: { response: NodeResolverService} },
//{ path: 'create', component: PostCreateComponent, canActivate: [AuthGuardService]},
{ path: 'shop', component: ViewerComponent, canActivate: [AuthGuardService]},
{ path: 'orders', component: ListOrdersComponent, canActivate: [AuthGuardService]},
{ path: 'biz', component: BusinessComponent, canActivate: [AuthGuardService]},
{ path: 'receipt/:orderId', component: ReceiptComponent},
{ path: 'login', component: LoginComponent, resolve: { response: NodeResolverService}}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: [AuthGuardService]
})
export class AppRoutingModule { }

12
src/app/app.component.css Normal file
View File

@ -0,0 +1,12 @@
main{
margin: 5px;
}
.footer{
font-family: 'Spartan', sans-serif;
font-size: 12px;
}
.tiny{
font-size: 10px;
}

View File

@ -0,0 +1,8 @@
<main>
<router-outlet></router-outlet>
</main>
<mat-divider></mat-divider>
<div class="footer" align="center">
<p>&copy; 2022 Vergara Technologies LLC</p>
<p class="tiny">Price data provided by CoinGecko API</p>
</div>

View File

@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'sell4zec'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('sell4zec');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('sell4zec app is running!');
});
});

16
src/app/app.component.ts Normal file
View File

@ -0,0 +1,16 @@
import { Component } from '@angular/core';
//import { Post} from './posts/post.model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
//StoredPosts: Post[] = [];
title = 'sell4zec';
//onPostAdded(post: Post){
//this.StoredPosts.push(post);
//}
}

101
src/app/app.module.ts Normal file
View File

@ -0,0 +1,101 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatExpansionModule } from '@angular/material/expansion';
import { HttpClientModule } from '@angular/common/http';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatDividerModule } from '@angular/material/divider';
import { MatListModule } from '@angular/material/list';
import { MatSelectModule } from '@angular/material/select';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatStepperModule } from '@angular/material/stepper';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { ViewerComponent } from './viewer/viewer.component';
import { LoginComponent } from './login/login.component';
import { ItemListComponent } from './items/item-list/item-list.component';
import { ItemCreateComponent } from './items/item-create/item-create.component';
import { ItemDeleteComponent } from './items/item-delete/item-delete.component';
import { ItemAddComponent} from './items/item-add/item-add.component';
import { OrderComponent } from './order/order.component';
import { CancelComponent } from './cancel/cancel.component';
import { CheckoutComponent } from './checkout/checkout.component';
import { SettingsComponent } from './settings/settings.component';
import { ListOrdersComponent } from './listorders/listorders.component';
import { ScanComponent } from './scan/scan.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { BusinessComponent } from './business/business.component';
import { SearchOptionsPipe } from './searchoptions.pipe';
import { TermsComponent } from './terms/terms.component';
import { ReceiptComponent } from './receipt/receipt.component';
import { ReceiptQRComponent } from './receipt-qr/receipt-qr.component';
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
ViewerComponent,
ItemListComponent,
LoginComponent,
OrderComponent,
ItemCreateComponent,
ItemDeleteComponent,
ItemAddComponent,
CancelComponent,
CheckoutComponent,
SettingsComponent,
ScanComponent,
ListOrdersComponent,
BusinessComponent,
SearchOptionsPipe,
TermsComponent,
ReceiptComponent,
ReceiptQRComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
MatIconModule,
HttpClientModule,
MatInputModule,
MatCardModule,
MatButtonModule,
MatToolbarModule,
MatExpansionModule,
MatDialogModule,
MatDividerModule,
MatListModule,
MatSelectModule,
MatProgressBarModule,
MatStepperModule,
MatAutocompleteModule,
MatSlideToggleModule,
BrowserAnimationsModule
],
exports: [
MatIconModule
],
providers: [],
bootstrap: [AppComponent],
entryComponents: [
ItemCreateComponent,
ItemDeleteComponent,
ItemAddComponent,
CancelComponent,
CheckoutComponent,
SettingsComponent,
ScanComponent
]
})
export class AppModule { }

View File

@ -0,0 +1,62 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import {HttpClient, HttpParams} from '@angular/common/http';
import { UserService } from './user.service';
import { Subscription, Observable } from 'rxjs';
import { Owner } from './owner.model';
@Injectable()
export class AuthGuardService implements CanActivate {
private UserSub: Subscription = new Subscription();
private addr = '';
private paid = false;
private paidUpdate: Observable<boolean>;
constructor(
private router: Router,
private http: HttpClient,
public userService: UserService
){
this.paidUpdate = this.userService.paidUpdate;
this.paidUpdate.subscribe((indicator) => {
this.paid = indicator;
});
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const token = localStorage.getItem('s4z_token');
var path = route.url[0].path;
if(token != null){
this.userService.uZaddrUpdate.
subscribe((addr) => {
if (addr != null) {
console.log(addr);
this.addr = addr;
} else {
console.log("No record for that token");
}
});
if (path === 'biz') {
if (this.addr.length > 0) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
} else {
if (this.addr != null && this.paid) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
}
} else {
console.log("Not logged in");
this.router.navigate(['/login']);
return false;
}
}
}

View File

@ -0,0 +1,18 @@
* {
font-family: 'Spartan', sans-serif;
}
mat-card.centercard{
max-width: 650px;
border: 1px solid #CCCCCC;
}
::ng-deep .mat-step-label{
font-size: 13px;
}
::ng-deep .mat-slide-toggle-content{
font-family: 'Spartan', sans-serif;
font-size: 11px;
}
a.link{
text-decoration: underline;
}

View File

@ -0,0 +1,90 @@
<app-header></app-header>
<div align="center">
<mat-card class="centercard">
<h3>Business sign-up</h3>
<mat-vertical-stepper #stepper linear>
<mat-step label="Provide business info" editable="false">
<p>We do not have a business associated with this Zcash address, please enter your information below:</p>
<mat-card [formGroup]="bizForm">
<mat-form-field appearance="outline" [style.width.px]=300>
<mat-label>Business Name</mat-label>
<input matInput placeholder="Business Name" formControlName="name">
</mat-form-field>
<br>
<mat-form-field appearance="outline">
<mat-label>Contact First Name</mat-label>
<input matInput placeholder="First Name" formControlName="first">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Contact Last Name</mat-label>
<input matInput placeholder="Last Name" formControlName="last">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Address</mat-label>
<input matInput placeholder="Address" formControlName="street">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>City</mat-label>
<input matInput placeholder="City" formControlName="city">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>State/Province</mat-label>
<input matInput placeholder="State or Province" formControlName="state">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Postal Code</mat-label>
<input matInput placeholder="Postal Code" formControlName="postal">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Country</mat-label>
<input matInput placeholder="Country" formControlName="country" [matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let ctry of countries | searchOptions:bizForm.get('country')!.value" [value]="ctry.name">
{{ctry.name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>E-mail</mat-label>
<input matInput type="email" placeholder="E-mail" formControlName="email">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Website</mat-label>
<input matInput placeholder="Website" formControlName="website">
</mat-form-field>
<mat-slide-toggle (change)="onChange($event)">
I accept the <a class="link" (click)="showTerms()">Terms of Use</a>
</mat-slide-toggle>
<mat-card-actions>
<button mat-raised-button color="primary" [disabled]="bizForm.invalid || !termsChecked" (click)="save()">Save</button>
</mat-card-actions>
</mat-card>
</mat-step>
<mat-step label="Select your session" editable="false">
<p>Please select the length of session that you need:</p>
<mat-card [formGroup]="payForm">
<mat-form-field appearance="outline">
<mat-label>Session</mat-label>
<mat-select formControlName="session">
<mat-option *ngFor="let ticket of tickets" [value]="ticket.value">
{{ticket.viewValue}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-card-actions>
<button mat-raised-button color="primary" [disabled]="payForm.invalid" (click)="pay()">Pay</button>
</mat-card-actions>
</mat-card>
</mat-step>
<mat-step label="ZGo confirms your payment" editable="false">
<p>{{barMessage}}</p>
<mat-progress-bar
[mode]="barMode"
[value]="barValue">
</mat-progress-bar>
</mat-step>
</mat-vertical-stepper>
</mat-card>
</div>

View File

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

View File

@ -0,0 +1,204 @@
import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core';
import { FormBuilder, Validators, FormGroup, FormControl } from '@angular/forms';
import { MatDialog, MatDialogConfig} from '@angular/material/dialog';
import { ProgressBarMode } from '@angular/material/progress-bar';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { filter, startWith, map, switchMap } from 'rxjs/operators';
import { MatStepper } from '@angular/material/stepper';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { Country } from '../country.model';
import { Owner } from '../owner.model';
import { User } from '../user.model';
import { UserService } from '../user.service';
import { FullnodeService } from '../fullnode.service';
import { SearchOptionsPipe } from '../searchoptions.pipe';
import { ScanComponent } from '../scan/scan.component';
import { TermsComponent } from '../terms/terms.component';
@Component({
selector: 'app-business',
templateUrl: './business.component.html',
styleUrls: ['./business.component.css']
})
export class BusinessComponent implements OnInit {
@ViewChild('stepper', { static: false}) stepper: MatStepper|undefined;
intervalHolder: any;
nodeAddress: string = '';
tickets = [
{
value: 0.005,
viewValue: '1 day: 0.005 ZEC'
},{
value: 0.025,
viewValue: '1 week: 0.025 ZEC'
},{
value: 0.1,
viewValue: '1 month: 0.1 ZEC'
}
];
bizForm: FormGroup;
payForm: FormGroup;
barMessage = 'Awaiting for transaction';
barMode: ProgressBarMode = 'indeterminate';
barValue = 0;
countries: Country[] = [];
owner: Owner = {
address: '',
currency: 'usd',
tax: false,
taxValue: 0,
vat: false,
vatValue: 0,
phone: '',
paid: false,
first: '',
last: '',
name: '',
street: '',
city: '',
state: '',
postal: '',
country: '',
email: '',
website: '',
zats: false
}
public countriesUpdate: Observable<Country[]>;
public ownerUpdate: Observable<Owner>;
public addrUpdate: Observable<string>;
public userUpdate: Observable<User>;
sessionId = '';
ownerKnown = false;
termsChecked = false;
constructor(
private fb: FormBuilder,
private userService: UserService,
private fullnodeService: FullnodeService,
private dialog: MatDialog,
private router: Router
) {
this.countriesUpdate = userService.countriesUpdate;
this.ownerUpdate = userService.ownerUpdate;
this.userUpdate = userService.userUpdate;
this.userUpdate.subscribe(userInfo => {
this.sessionId = userInfo.session;
});
this.addrUpdate = fullnodeService.addrUpdate;
this.addrUpdate.subscribe(nodeAdd => {
this.nodeAddress = nodeAdd;
});
this.bizForm = fb.group({
name: ['', Validators.required],
first: ['', Validators.required],
last: ['', Validators.required],
street: ['', Validators.required],
city: ['', Validators.required],
state: ['', Validators.required],
postal: ['', Validators.required],
country: ['', Validators.required],
email: ['', [Validators.email, Validators.required]],
website: [''],
terms: [false]
});
this.payForm= fb.group({
session: ['', Validators.required]
});
this.userService.getCountries();
this.countriesUpdate.subscribe((countries) => {
this.countries = countries;
});
this.ownerUpdate.subscribe((owner) => {
this.owner = owner;
});
}
ngOnInit(): void {
this.intervalHolder = setInterval(() => {
this.loginCheck();
}, 1000 * 60);
}
ngAfterViewInit(): void {
this.ownerUpdate.subscribe(ownerData => {
if(ownerData.name.length > 0 && this.stepper!.selectedIndex == 0){
this.stepper!.next();
}
});
}
onChange(ob: MatSlideToggleChange){
console.log(ob.checked);
this.termsChecked = ob.checked;
}
showTerms() {
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
const dialogRef = this.dialog.open(TermsComponent, dialogConfig);
dialogRef.afterClosed().subscribe(val => {
console.log('Terms read');
});
}
save() {
this.owner = {
address: '',
currency: 'usd',
tax: false,
taxValue: 0,
vat: false,
vatValue: 0,
first: this.bizForm.get('first')!.value,
last: this.bizForm.get('last')!.value,
phone: '',
paid: false,
name: this.bizForm.get('name')!.value,
street: this.bizForm.get('street')!.value,
city: this.bizForm.get('city')!.value,
state: this.bizForm.get('state')!.value,
postal: this.bizForm.get('postal')!.value,
country: this.bizForm.get('country')!.value,
email: this.bizForm.get('email')!.value,
website: this.bizForm.get('website')!.value,
zats: false
};
this.userService.addOwner(this.owner);
this.stepper!.next();
}
pay(){
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.data = {
totalZec: this.payForm.get('session')!.value,
addr: this.nodeAddress,
session: this.sessionId,
pay: true
};
const dialogRef = this.dialog.open(ScanComponent, dialogConfig);
dialogRef.afterClosed().subscribe(val => {
console.log('Awaiting payment');
if(val){
this.stepper!.next();
}
});
}
loginCheck(){
this.userService.findUser();
this.ownerUpdate.subscribe((owner) => {
if(owner.paid) {
this.router.navigate(['/shop']);
}
});
}
}

View File

@ -0,0 +1,7 @@
.text {
font-family: 'Spartan', sans-serif;
}
mat-dialog-content {
max-width: 400px;
}

View File

@ -0,0 +1,24 @@
<h3 mat-dialog-title class="text">{{title}}</h3>
<mat-dialog-content>
<p class="text">{{msg}}</p>
</mat-dialog-content>
<mat-dialog-actions>
<table cellspacing="0" width="100%">
<tr>
<td>
<button mat-raised-button color="primary" (click)="confirm()">
Yes
</button>
</td>
<td align="right">
<button mat-raised-button (click)="close()">
No
</button>
</td>
</tr>
</table>
</mat-dialog-actions>

View File

@ -0,0 +1,29 @@
import { Inject, Component, OnInit, ViewEncapsulation} from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
@Component({
selector: 'app-cancel',
templateUrl: './cancel.component.html',
styleUrls: ['./cancel.component.css']
})
export class CancelComponent {
title: string;
msg: string;
constructor(
private dialogRef: MatDialogRef<CancelComponent>,
@Inject(MAT_DIALOG_DATA) public data: { title: string, msg: string}
) {
this.title = data.title;
this.msg = data.msg;
}
confirm() {
this.dialogRef.close(true);
}
close() {
this.dialogRef.close(false);
}
}

View File

@ -0,0 +1,3 @@
.text {
font-family: 'Spartan', sans-serif;
}

View File

@ -0,0 +1,25 @@
<div align="center" mat-dialog-title>
<h4 class="text">Scan to make payment</h4>
</div>
<mat-dialog-content>
<div align="center" id="checkout-qr"></div>
</mat-dialog-content>
<mat-dialog-actions>
<table cellspacing="0" width="100%">
<tr>
<td>
<button mat-raised-button color="primary" (click)="confirm()">
<mat-icon>verified_user</mat-icon>
</button>
</td>
<td align="right">
<button mat-raised-button (click)="close()">
<mat-icon>close</mat-icon>
</button>
</td>
</tr>
</table>
</mat-dialog-actions>

View File

@ -0,0 +1,50 @@
import { Inject, Component, OnInit, ViewEncapsulation} from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
var QRCode = require('easyqrcodejs');
var URLSafeBase64 = require('urlsafe-base64');
var Buffer = require('buffer/').Buffer;
@Component({
selector: 'app-checkout',
templateUrl: './checkout.component.html',
styleUrls: ['./checkout.component.css']
})
export class CheckoutComponent implements OnInit{
address: string;
total: number;
orderId: string;
codeString: string = '';
zcashUrl: SafeUrl;
constructor(
private dialogRef: MatDialogRef<CheckoutComponent>,
private sanitizer: DomSanitizer,
@Inject(MAT_DIALOG_DATA) public data: { totalZec: number, addr: string, orderId: string}
) {
this.address = data.addr;
this.total = data.totalZec;
this.orderId = data.orderId;
this.codeString = `zcash:${this.address}?amount=${this.total.toFixed(6)}&memo=${URLSafeBase64.encode(Buffer.from('Z-Go Order '.concat(this.orderId)))}`;
this.zcashUrl = this.sanitizer.bypassSecurityTrustUrl(this.codeString);
}
ngOnInit() {
var qrcode = new QRCode(document.getElementById("checkout-qr"), {
text: this.codeString,
logo: "/assets/zcash.png",
logoWidth: 80,
logoHeight: 80
});
}
confirm() {
this.dialogRef.close(true);
}
close() {
this.dialogRef.close(false);
}
}

5
src/app/country.model.ts Normal file
View File

@ -0,0 +1,5 @@
export interface Country {
_id?: string;
name: string;
code: string;
}

View File

@ -0,0 +1,96 @@
import {Injectable} from '@angular/core';
import {Subject, Subscription, BehaviorSubject, Observable} from 'rxjs';
import {HttpClient, HttpParams, HttpHeaders} from '@angular/common/http';
import {UserService} from './user.service';
import { Owner } from './owner.model';
//import {User} from './user.model';
@Injectable({providedIn: 'root'})
export class FullnodeService{
beUrl = 'http://localhost:3000/';
private dataStore: { height: number, memoList: string[], addr: string, price: number } = { height: 0, memoList: [], addr: '', price:0 };
private _heightUpdated: BehaviorSubject<number> = new BehaviorSubject(this.dataStore.height);
private _memoUpdated: BehaviorSubject<string[]> = new BehaviorSubject(this.dataStore.memoList);
private _addrUpdated: BehaviorSubject<string> = new BehaviorSubject(this.dataStore.addr);
private _priceUpdated: BehaviorSubject<number> = new BehaviorSubject(this.dataStore.price);
public readonly addrUpdate: Observable<string> = this._addrUpdated.asObservable();
public readonly heightUpdate: Observable<number> = this._heightUpdated.asObservable();
public readonly memoUpdate: Observable<string[]> = this._memoUpdated.asObservable();
public readonly priceUpdate: Observable<number> = this._priceUpdated.asObservable();
public readonly ownerUpdate: Observable<Owner>;
private UserSub: Subscription = new Subscription();
private apiKey = 'YourApiKey';
private reqHeaders: HttpHeaders;
private owner: Owner = {
_id: '',
name: '',
address: '',
currency: 'usd',
tax: false,
taxValue: 0,
vat: false,
vatValue: 0,
first: '',
last: '',
email: '',
street: '',
city: '',
state: '',
postal: '',
phone: '',
paid: false,
website: '',
country: '',
zats: false
};
constructor(private http: HttpClient, public userService: UserService){
this.reqHeaders = new HttpHeaders().set('Authorization', this.apiKey);
this.ownerUpdate = userService.ownerUpdate;
this.getAddr();
this.getHeight();
this.ownerUpdate.subscribe((owner) => {
this.owner = owner;
this.getPrice(this.owner.currency);
});
}
getHeight(){
let obs = this.http.get<{message: string, height: number}>(this.beUrl+'api/blockheight', { headers: this.reqHeaders });
obs.subscribe((BlockData) => {
this.dataStore.height = BlockData.height;
this._heightUpdated.next(Object.assign({}, this.dataStore).height);
});
return obs;
}
getPrice(currency: string){
//var currency = 'usd';
const params = new HttpParams().append('currency', currency);
let obs = this.http.get<{message: string, price: any}>(this.beUrl+'api/price', { headers:this.reqHeaders, params: params, observe: 'response'});
obs.subscribe((PriceData) => {
if (PriceData.status == 200) {
this.dataStore.price = PriceData.body!.price.price;
console.log("price", this.dataStore.price);
this._priceUpdated.next(Object.assign({},this.dataStore).price);
} else {
console.log('No price found for currency', currency);
}
});
return obs;
}
getAddr() {
let obs = this.http.get<{message: string, addr: string}>(this.beUrl+'api/getaddr', { headers: this.reqHeaders });
obs.subscribe((AddrData) => {
this.dataStore.addr = AddrData.addr;
this._addrUpdated.next(Object.assign({}, this.dataStore).addr);
});
return obs;
}
}

View File

@ -0,0 +1,29 @@
.text {
font-family: 'Spartan', sans-serif;
}
a{
text-decoration: none;
color: white;
}
ul{
list-style: none;
padding: 0;
margin: 0;
}
.spacer{
flex: 1 1 auto;
}
.mini {
font-size: 10px;
line-height: 0.8;
}
.icon {
font-family: "Material Icons";
}
.logo {
margin-top: 5px;
}

View File

@ -0,0 +1,14 @@
<mat-toolbar color="primary">
<span align="center">
<img class="logo" src="/assets/logo-new-white.png" height="40px" />
</span>
<span class="spacer"></span>
<span align="center">
<p class="mini text">Currency: {{ getCurrency() }}</p>
<p class="mini text">Last block:</p>
<p class="mini text">{{heightUpdate | async}}</p>
</span>
<span>
<button mat-raised-button (click)="logout()"><mat-icon class="logbutton">logout</mat-icon></button>
</span>
</mat-toolbar>

View File

@ -0,0 +1,91 @@
import {Component, OnInit, OnDestroy} from '@angular/core';
import { MatDialog, MatDialogConfig} from '@angular/material/dialog';
import {FullnodeService} from '../fullnode.service';
import { Router } from '@angular/router';
import { UserService } from '../user.service';
import { CancelComponent } from '../cancel/cancel.component';
import {Subscription, Observable} from 'rxjs';
import {Owner} from '../owner.model';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit, OnDestroy {
public height = 0;
private owner: Owner= {
_id:'',
address: 'none',
name:'',
currency: 'usd',
tax: false,
taxValue: 0,
vat: false,
vatValue: 0,
first: '',
last: '',
email: '',
street: '',
city: '',
state: '',
postal: '',
phone: '',
paid: false,
website: '',
country: '',
zats: false
};
private session: string | null = '';
public heightUpdate: Observable<number>;
public ownerUpdate: Observable<Owner>;
public uZaddrUpdate: Observable<string>;
constructor(
public fullnodeService: FullnodeService,
public userService: UserService,
private dialog: MatDialog,
private router: Router
){
this.heightUpdate = fullnodeService.heightUpdate;
this.uZaddrUpdate = userService.uZaddrUpdate;
this.ownerUpdate = userService.ownerUpdate;
this.ownerUpdate.subscribe((owner) => {
this.owner = owner;
});
}
ngOnInit(){
}
ngOnDestroy(){
}
getCurrency(){
return this.owner.currency.toUpperCase();
}
logout(){
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.data = {title: 'Logout?', msg: 'Are you sure you want to disconnect from ZGo? You will have to re-link your wallet via shielded memo.'};
const dialogRef = this.dialog.open(CancelComponent, dialogConfig);
dialogRef.afterClosed().subscribe(val => {
if(val){
console.log('Logout!');
this.userService.deleteUser().subscribe(UserResponse => {
console.log('Rerouting');
this.userService.findUser();
this.router.navigate(['/login']);
});
}
});
}
}

7
src/app/item.model.ts Normal file
View File

@ -0,0 +1,7 @@
export interface Item {
_id?: any;
name: string;
description: string;
user: string;
cost: number;
}

View File

@ -0,0 +1,7 @@
.text {
font-family: 'Spartan', sans-serif;
}
input[type=number]{
text-align: right;
}

View File

@ -0,0 +1,13 @@
<h2 mat-dialog-title class="text">Add to Order</h2>
<mat-dialog-content [formGroup]="orderForm">
<p class="text">{{lineItem.name}}</p>
<mat-form-field>
<input matInput type="number" placeholder="Quantity" formControlName="qty">
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button class="text" (click)="close()">Cancel</button>
<button mat-raised-button class="text" color="primary" (click)="save()">Add</button>
</mat-dialog-actions>

View File

@ -0,0 +1,45 @@
import { Inject, Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { FormBuilder, Validators, FormGroup, FormControl } from '@angular/forms';
import { LineItem } from '../lineitem.model';
import { Order } from '../../order/order.model';
@Component({
selector: 'app-item-add',
templateUrl: './item-add.component.html',
styleUrls: ['./item-add.component.css']
})
export class ItemAddComponent implements OnInit {
orderForm: FormGroup;
lineItem: LineItem;
//order: Order;
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<ItemAddComponent>,
@Inject(MAT_DIALOG_DATA) public data: LineItem
) {
this.orderForm = fb.group({
qty: [data.qty, Validators.required]
});
this.lineItem = {
qty : data.qty,
name : data.name,
cost : data.cost
}
}
ngOnInit() {
}
close() {
this.dialogRef.close();
}
save() {
this.lineItem.qty = this.orderForm.value.qty;
this.dialogRef.close(this.lineItem);
}
}

View File

@ -0,0 +1,7 @@
.text {
font-family: 'Spartan', sans-serif;
}
input[type=number]{
text-align: right;
}

View File

@ -0,0 +1,25 @@
<h2 mat-dialog-title class="text">Add item</h2>
<mat-dialog-content [formGroup]="form">
<mat-form-field class="text" appearance="outline">
<mat-label>Item</mat-label>
<input type="hidden" formControlName="id">
<input matInput placeholder="Item" formControlName="name">
</mat-form-field>
<mat-form-field class="text" appearance="outline">
<mat-label>Description</mat-label>
<textarea matInput placeholder="Description" formControlName="description"></textarea>
</mat-form-field>
<mat-form-field class="text" appearance="outline">
<mat-label>Price</mat-label>
<input matInput type="number" placeholder="Price" formControlName="cost">
<div *ngIf="!form.controls['cost'].valid && form.controls['cost'].touched">
<div *ngIf="form.controls['cost'].errors?.pattern">Use only numbers</div>
</div>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button class="text" (click)="close()">Close</button>
<button mat-raised-button class="text" color="primary" (click)="save()">Save</button>
</mat-dialog-actions>

View File

@ -0,0 +1,58 @@
import { Inject, Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { FormBuilder, Validators, FormGroup, FormControl } from '@angular/forms';
import { Item } from '../item.model';
@Component({
selector: 'app-item-create',
templateUrl: './item-create.component.html',
styleUrls: ['./item-create.component.css']
})
export class ItemCreateComponent implements OnInit {
form: FormGroup;
id: string | undefined = '';
numberRegEx = /\d*\.?\d{1,2}/;
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<ItemCreateComponent>,
@Inject(MAT_DIALOG_DATA) public data: Item
){
if (data._id === '') {
this.form = fb.group({
id: [null],
name: [null, Validators.required],
description: [null, Validators.required],
cost: new FormControl('', {
validators: [Validators.required, Validators.pattern(this.numberRegEx)],
updateOn: "blur"
})
});
} else {
this.id = data._id;
this.form = fb.group({
id: [data._id],
name: [data.name, Validators.required],
description: [data.description, Validators.required],
cost: new FormControl(data.cost, {
validators: [Validators.required, Validators.pattern(this.numberRegEx)],
updateOn: "blur"
})
});
}
}
ngOnInit () {
}
save() {
this.dialogRef.close(this.form.value);
}
close () {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,3 @@
.text {
font-family: 'Spartan', sans-serif;
}

View File

@ -0,0 +1,10 @@
<h2 mat-dialog-title class="text">Delete item</h2>
<mat-dialog-content>
<p class="text">Are you sure you want to delete "{{item.name}}"?</p>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button (click)="close()" class="text">Cancel</button>
<button mat-raised-button color="primary" class="text" (click)="save()">Delete</button>
</mat-dialog-actions>

View File

@ -0,0 +1,32 @@
import { Inject, Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Item } from '../item.model';
@Component({
selector: 'app-item-delete',
templateUrl: './item-delete.component.html',
styleUrls: ['./item-delete.component.css']
})
export class ItemDeleteComponent implements OnInit{
item: Item;
constructor(
private dialogRef: MatDialogRef<ItemDeleteComponent>,
@Inject(MAT_DIALOG_DATA) public data: Item
) {
this.item = data;
}
ngOnInit() {
}
save() {
this.dialogRef.close(this.item._id);
}
close() {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,30 @@
.card {
font-family: 'Spartan', sans-serif;
}
.text {
font-family: 'Spartan', sans-serif;
}
.small {
font-size: 75%;
}
.icons {
font-family: 'Material Icons';
}
.spacer{
flex: 1 1 auto;
}
img.icon{
margin-bottom: -3px;
}
div.card{
margin-top: 3px;
}
p.price{
font-family: 'Roboto-Mono', monospace;
margin: 0px;
}

View File

@ -0,0 +1,42 @@
<div *ngIf="items.length > 0">
<div class="card" *ngFor="let item of itemsUpdate | async">
<mat-card>
<table cellspacing="0" width="100%" class="text">
<tr>
<td>{{item.name}}</td>
<td align="right">
<p class="price">{{item.cost | currency: getCurrency() }}</p>
<p class="price" *ngIf="!owner.zats"><img class="icon" src="/assets/spartan-zec.png" width="12px" />{{(item.cost/price) | number: '1.0-6'}}</p>
<p class="price" *ngIf="owner.zats">&#x24e9; {{(item.cost/price)*100000000 | number: '1.0-0'}}</p>
</td>
</tr>
</table>
<div align="center">
<p class="text small">{{item.description}}</p>
</div>
<mat-card-actions>
<table cellspacing="0" width="100%">
<tr>
<td>
<button mat-icon-button class="icons" (click)="edit(item._id!)">
<mat-icon inline=true>edit</mat-icon>
</button>
<button mat-icon-button class="icons" (click)="delete(item._id!)">
<mat-icon inline=true>delete</mat-icon>
</button>
</td>
<td align="right">
<button mat-raised-button color="primary" class="icons" (click)="addToOrder(item._id!)">
<mat-icon>shopping_cart</mat-icon>
</button>
</td>
</tr>
</table>
</mat-card-actions>
</mat-card>
</div>
</div>
<p *ngIf = "items.length <= 0">No items yet!</p>
<br>
<button class="text" mat-raised-button (click)="openDialog()">
<mat-icon class="icon">add</mat-icon>Add item</button>

View File

@ -0,0 +1,180 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog, MatDialogConfig} from '@angular/material/dialog';
import { Observable } from 'rxjs';
import { Item } from '../item.model';
import { Owner } from '../../owner.model';
import { FullnodeService } from '../../fullnode.service';
import { UserService } from '../../user.service';
import { ItemService } from '../items.service';
import { OrderService} from '../../order/order.service';
import { ItemCreateComponent } from '../item-create/item-create.component';
import { ItemDeleteComponent } from '../item-delete/item-delete.component';
import { ItemAddComponent } from '../item-add/item-add.component';
@Component({
selector: 'app-item-list',
templateUrl: './item-list.component.html',
styleUrls: ['./item-list.component.css']
})
export class ItemListComponent implements OnInit{
public items: Item[] = [];
owner: Owner = {
_id: '',
name: '',
address: '',
currency: 'usd',
tax: false,
taxValue: 0,
vat: false,
vatValue: 0,
first: '',
last: '',
email: '',
street: '',
city: '',
state: '',
postal: '',
phone: '',
paid: false,
website: '',
country: '',
zats: false
};
public price: number = 1;
public ownerUpdate: Observable<Owner>;
public itemsUpdate: Observable<Item[]>;
public priceUpdate: Observable<number>;
constructor(
public itemService: ItemService,
userService: UserService,
public orderService: OrderService,
public fullnodeService: FullnodeService,
private dialog: MatDialog
) {
this.ownerUpdate = userService.ownerUpdate;
this.itemsUpdate = itemService.itemsUpdated;
this.priceUpdate = fullnodeService.priceUpdate;
this.ownerUpdate.subscribe((owner) => {
this.owner = owner;
fullnodeService.getPrice(this.owner.currency);
itemService.getItems(this.owner.address);
this.itemsUpdate.subscribe((items) => {
this.items = items;
});
});
this.priceUpdate.subscribe((price) => {
this.price = price;
});
}
ngOnInit(){
this.itemsUpdate.subscribe((items) => {
this.items = items;
});
}
openDialog(){
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.data = {_id: '', name: '' , user: '', description: '', cost: 0}
const dialogRef = this.dialog.open(ItemCreateComponent, dialogConfig);
dialogRef.afterClosed().subscribe((val) => {
if(val != null) {
var item:Item = {name: val.name, description: val.description, cost: val.cost, user: this.owner.address};
this.itemService.addItem(item);
}
this.itemService.getItems(this.owner.address);
this.itemsUpdate.subscribe((items) => {
this.items = items;
});
});
}
edit(id: string) {
//console.log('Edit:', id);
const item = this.items.find(element => element._id == id);
//console.log(item);
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.data = item;
const dialogRef = this.dialog.open(ItemCreateComponent, dialogConfig);
dialogRef.afterClosed().subscribe((val) => {
if (val != null) {
var editItem: Item = {
_id: val.id,
name: val.name,
description: val.description,
cost: val.cost,
user: this.owner.address
};
//console.log('Edit:', editItem);
this.itemService.addItem(editItem).subscribe((response) => {
this.itemService.getItems(this.owner.address);
});;
}
this.itemService.getItems(this.owner.address);
});
}
delete(id: string) {
//console.log('Edit:', id);
const item = this.items.find(element => element._id == id);
//console.log(item);
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.data = item;
const dialogRef = this.dialog.open(ItemDeleteComponent, dialogConfig);
dialogRef.afterClosed().subscribe((val) => {
if (val != null) {
console.log('Deleting', val);
this.itemService.deleteItem(val);
this.items = [];
}
this.itemService.getItems(this.owner.address);
this.itemsUpdate.subscribe((items) => {
this.items = items;
});
});
}
addToOrder(id: string) {
const item = this.items.find(element => element._id == id);
//console.log(item);
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.data = {
qty: 1,
name: item!.name,
cost: item!.cost
};
const dialogRef = this.dialog.open(ItemAddComponent, dialogConfig);
dialogRef.afterClosed().subscribe((val) => {
if (val != null) {
console.log('Adding to order', val);
this.orderService.addToOrder(val);
}
this.itemService.getItems(this.owner.address);
});
}
getCurrency(){
return this.owner.currency.toUpperCase();
}
}

View File

@ -0,0 +1,7 @@
export interface Item {
_id?: string;
name: string;
description: string;
cost: number;
user: string;
}

View File

@ -0,0 +1,62 @@
import { Item } from './item.model';
import { Injectable } from '@angular/core';
import { Subject, BehaviorSubject, Observable } from 'rxjs';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
@Injectable({providedIn: 'root'})
export class ItemService{
beUrl = 'http://localhost:3000/';
private dataStore: { items: Item[] } = { items: [] } ;
private _itemsUpdated: BehaviorSubject<Item[]> = new BehaviorSubject(this.dataStore.items);
public readonly itemsUpdated: Observable<Item[]> = this._itemsUpdated.asObservable();
private address:string = '';
private apiKey = 'YourApiKey';
private reqHeaders: HttpHeaders;
constructor(private http: HttpClient){
this.reqHeaders = new HttpHeaders().set('Authorization', this.apiKey);
}
getItems(addr: string){
this.address = addr;
const params = new HttpParams().append('address', addr);
let obs = this.http.get<{message: string, items: any}>(this.beUrl+'api/getitems', { headers:this.reqHeaders, params: params, observe: 'response'});
obs.subscribe((ItemDataResponse) => {
if (ItemDataResponse.status == 200 ) {
this.dataStore.items = ItemDataResponse.body!.items;
this._itemsUpdated.next(Object.assign({},this.dataStore).items);
} else {
this.dataStore.items = [];
this._itemsUpdated.next(Object.assign({},this.dataStore).items);
console.log('No items found');
}
});
return obs;
}
addItem(item: Item) {
//const params = new HttpParams().append('item', JSON.stringify(item));
let obs = this.http.post<{message: string}>(this.beUrl+'api/item', { item: item }, { headers: this.reqHeaders });
obs.subscribe((ItemResponse) => {
console.log('Item added');
this.getItems(this.address);
});
return obs;
}
deleteItem(id: string) {
let obs = this.http.delete<{message: string}>(this.beUrl+'api/item/'+id, { headers: this.reqHeaders });
obs.subscribe((ItemResponse) => {
console.log('Item deleted');
this.getItems(this.address);
});
return obs;
}
}

View File

@ -0,0 +1,5 @@
export interface LineItem {
qty: number;
name: string;
cost: number;
}

View File

@ -0,0 +1,32 @@
.text{
font-family: 'Spartan', sans-serif;
}
.number{
font-family: 'Roboto Mono', monospace;
}
img.icon{
margin-bottom: -2px;
}
.price{
display: flex;
align-items: center;
}
.mini{
font-size: x-small;
align-items: center;
display: flex;
padding: 3px;
}
.small{
font-size: small;
}
.total{
font-size: large;
font-family: 'Roboto Mono', monospace;
}
img.total{
margin-bottom:-2px;
}
.central{
max-width: 450px;
}

View File

@ -0,0 +1,47 @@
<app-header></app-header>
<div align="center">
<h3 class="text">{{(ownerUpdate | async)!.name}}</h3>
<button class="text" mat-raised-button [routerLink]="['/shop']" color="primary">
Back to Shop
</button>
</div>
<mat-divider></mat-divider>
<div align="center">
<div class="central">
<table class="text" width="75%">
<tr>
<td width="50%" align="center">
<h3>Today's Total:</h3>
<p class="total"><img class="total" src="/assets/spartan-zec.png" height="18px" />{{todayTotal | number: '1.0-6'}}</p>
</td>
<td width="50%" align="center">
<h3>Overall Total:</h3>
<p class="total"><img class="total" src="/assets/spartan-zec.png" height="18px" />{{total | number: '1.0-6'}}</p>
</td>
</tr>
</table>
</div>
</div>
<mat-divider></mat-divider>
<div align="center">
<div class="central">
<mat-accordion *ngIf = "orders.length > 0">
<mat-expansion-panel class="text" *ngFor = "let order of orders">
<mat-expansion-panel-header class="text" >
<mat-panel-title>
<span class="price number"><img src="/assets/spartan-zec.png" height="50%" />{{order.totalZec}}</span>
</mat-panel-title>
<mat-panel-description>
{{order.timestamp | date: 'short'}}
</mat-panel-description>
</mat-expansion-panel-header>
<p class="text">Order: {{order._id}}</p>
<p class="text">Zcash price: {{order.price | currency: order.currency.toUpperCase()}}</p>
<mat-list>
<mat-list-item class="text small" *ngFor="let item of order.lines">{{item.qty}} x {{item.name}}</mat-list-item>
</mat-list>
</mat-expansion-panel>
</mat-accordion>
<p class="text" *ngIf = "orders.length <= 0">No orders</p>
</div>
</div>

View File

@ -0,0 +1,53 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs';
import { Order } from '../order/order.model';
import { FullnodeService } from '../fullnode.service';
import { UserService } from '../user.service';
import {Owner} from '../owner.model';
import { OrderService } from '../order/order.service';
@Component({
selector: 'app-list-orders',
templateUrl: './listorders.component.html',
styleUrls: ['./listorders.component.css']
})
export class ListOrdersComponent implements OnInit, OnDestroy{
public todayTotal: number = 0;
public total: number = 0;
public orders: Order[] = [];
public ownerUpdate: Observable<Owner>;
public ordersUpdate: Observable<Order[]>;
constructor(
public orderService: OrderService,
public userService: UserService
){
this.ownerUpdate = userService.ownerUpdate;
this.orderService.getAllOrders();
this.ordersUpdate = orderService.allOrdersUpdate;
}
ngOnInit(){
this.ordersUpdate.subscribe((orders) => {
this.total = 0;
this.todayTotal = 0;
var today = new Date();
this.orders = orders;
console.log('lisord', this.orders.length);
for (let i=0; i < this.orders.length; i++){
this.total += this.orders[i].totalZec;
var date = new Date(this.orders[i]!.timestamp!);
var diff = (today.getTime() / 1000) - (date.getTime()/1000);
if (diff < (24*3600)){
this.todayTotal += this.orders[i].totalZec;
}
}
});
}
ngOnDestroy(){
this.total = 0;
this.todayTotal = 0;
}
}

View File

@ -0,0 +1,31 @@
* {
font-family: 'Spartan', sans-serif;
}
mat-card.coolcard{
background-color: #FF5722;
color: #FFFFFF;
margin: 5px;
}
mat-card.centercard{
max-width: 450px;
border: 1px solid #CCCCCC;
}
.icon{
font-family: 'Material Icons';
}
.bigbutton{
display: inline-flex;
vertical-align: middle;
font-size: 120%;
padding: 3px;
}
.alert-success{
margin: 5px;
background-color: #BBFFBB
}
.numbers{
font-family: 'Roboto Mono', monospace;
}
::ng-deep .mat-step-label{
font-size: 13px;
}

View File

@ -0,0 +1,48 @@
<div align="center" class="text">
<mat-card class="coolcard">
<img src="/assets/logo-new-white.png" height="120px" />
<p class="text">Last block seen: <span class="numbers">{{ heightUpdate | async }}</span></p>
</mat-card>
</div>
<div align="center">
<mat-card class="centercard">
<h3>The Zcash Register</h3>
<mat-vertical-stepper #stepper linear>
<mat-step label="Connect your wallet to ZGo" editable="false">
<mat-card>
<div align="center" id="info">
<mat-card-actions>
<button mat-raised-button color="primary" (click)="login(stepper)">
<mat-icon class="icon">login</mat-icon><span class="bigbutton">Link wallet</span>
</button>
</mat-card-actions>
</div>
</mat-card>
</mat-step>
<mat-step label="ZGo confirms your login on the Zcash blockhain:" editable="false">
<p>{{barMessage}}</p>
<mat-progress-bar
[mode]="barMode"
[value]="barValue">
</mat-progress-bar>
</mat-step>
<mat-step label="Enter the PIN sent by ZGo to confirm wallet ownership:">
<mat-card [formGroup]="pinForm">
<h4>
Check your wallet
</h4>
<mat-form-field appearance="outline">
<mat-label>PIN</mat-label>
<input matInput formControlName="pinValue">
</mat-form-field>
<p *ngIf="pinError">Wrong pin!</p>
<mat-card-actions>
<button mat-raised-button color="primary" [disabled]="!pinForm.valid" (click)="confirmPin()">
Confirm
</button>
</mat-card-actions>
</mat-card>
</mat-step>
</mat-vertical-stepper>
</mat-card>
</div>

View File

@ -0,0 +1,220 @@
import { Component, OnInit, OnDestroy, Injectable, ChangeDetectorRef, ViewChild, AfterViewInit } from '@angular/core';
import { CanActivate, Router, RouterStateSnapshot, ActivatedRouteSnapshot, ActivatedRoute } from '@angular/router';
import { MatDialog, MatDialogConfig} from '@angular/material/dialog';
import { FormBuilder, Validators, FormGroup, FormControl } from '@angular/forms';
import { ProgressBarMode } from '@angular/material/progress-bar';
import { MatStepper } from '@angular/material/stepper';
import { UserService } from '../user.service';
import { FullnodeService } from '../fullnode.service';
import { ScanComponent} from '../scan/scan.component';
import { Tx } from '../tx.model';
import {User} from '../user.model';
import { Owner } from '../owner.model';
import { Subscription, Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { v4 as uuidv4 } from 'uuid';
var QRCode = require('easyqrcodejs');
var URLSafeBase64 = require('urlsafe-base64');
var Buffer = require('buffer/').Buffer;
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit, AfterViewInit {
txs: Tx[] = [];
intervalHolder: any;
nodeAddress: string = '';
localToken: string | null = '';
selectedValue: number = 0.001;
pinError: boolean = false;
public user:User = {
address: '',
session: '',
blocktime: 0,
expired: false,
pin: '',
validated: false
};
public owner:Owner = {
address: '',
name: '',
currency: '',
tax: false,
taxValue:0,
vat: false,
vatValue: 0,
first: '',
last: '',
email: '',
street: '',
city: '',
state: '',
postal: '',
phone: '',
paid: false,
website: '',
country: '',
zats: false
};
private FullnodeSub: Subscription = new Subscription();
private UserSub: Subscription = new Subscription();
public heightUpdate: Observable<number>;
public uZaddrUpdate: Observable<string>;
public userUpdate:Observable<User>;
public ownerUpdate:Observable<Owner>;
public txsUpdate: Observable<Tx[]>;
tickets = [
{
value: 0.001,
viewValue: '1 hour: 0.001 ZEC'
},{
value: 0.005,
viewValue: '1 day: 0.005 ZEC'
},{
value: 0.025,
viewValue: '1 week: 0.025 ZEC'
}
];
prompt: boolean = false;
confirmedMemo: boolean = false;
targetBlock: number = 0;
barMode: ProgressBarMode = 'indeterminate';
barValue = 0;
barMessage = 'Scanning blockchain for login memo, please wait.';
@ViewChild('stepper') private myStepper?: MatStepper;
entryForm: FormGroup;
pinForm: FormGroup;
constructor(
private fb: FormBuilder,
private activatedRoute: ActivatedRoute,
public fullnodeService: FullnodeService,
private router: Router,
public userService: UserService,
private dialog: MatDialog,
private _changeDetectorRef: ChangeDetectorRef
){
//this.fullnodeService.getAddr();
this.entryForm = fb.group({
selectedSession: [0.001, Validators.required]
});
this.pinForm = fb.group({
pinValue: [null, Validators.required]
});
this.heightUpdate = fullnodeService.heightUpdate;
this.uZaddrUpdate = userService.uZaddrUpdate;
this.userUpdate = userService.userUpdate;
this.ownerUpdate = userService.ownerUpdate;
this.userUpdate.subscribe((user) => {
this.user = user;
});
this.ownerUpdate.subscribe((owner) => {
this.owner = owner;
});
this.txsUpdate = userService.txUpdate;
this.txsUpdate.subscribe((txs) => {
this.txs = txs;
});
}
ngAfterViewInit(){
//console.log('Step', this.myStepper);
this.pinError = false;
//console.log('Activated route data in Component:::', this.activatedRoute.data);
this.activatedRoute.data.subscribe((addrData) => {
//console.log('FETCH ADDRESS', addrData);
this.nodeAddress = addrData.response.addr;
//console.log('Node addres ', this.nodeAddress);
this.localToken = localStorage.getItem('s4z_token');
//console.log(localToken);
if(this.localToken == null){
var token = uuidv4();
localStorage.setItem('s4z_token', token);
this.localToken = token;
}
this.loginCheck();
});
}
ngOnInit(){
this.intervalHolder = setInterval(() => {
this.fullnodeService.getHeight();
//this.userService.findUser();
this.loginCheck();
this._changeDetectorRef.markForCheck();
}, 1000 * 60);
}
loginCheck(){
var today = new Date().getTime() / 1000;
this.userService.findUser();
this.userService.findPending();
this.txsUpdate.subscribe((txs) => {
this.txs = txs;
});
this.userUpdate.subscribe((user) => {
if (user.blocktime > 0) {
if(this.myStepper!.selectedIndex === 1){
this.myStepper!.next();
}
//console.log('Log in found in blockchain!');
if (user.validated) {
if (this.owner.paid) {
this.router.navigate(['/shop']);
} else {
this.router.navigate(['/biz']);
}
}
}
});
if (this.txs.length > 0) {
this.barMode = 'determinate';
this.barValue = (this.txs[0].confirmations / 2) * 100;
this.confirmedMemo = true;
this.barMessage = 'Login memo found, awaiting confirmations';
}
}
login(stepper: MatStepper) {
//console.log('Dropdown:', this.entryForm.value.selectedSession);
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.data = {
totalZec: 1e-8,
addr: this.nodeAddress,
session: this.localToken,
pay: false
};
const dialogRef = this.dialog.open(ScanComponent, dialogConfig);
dialogRef.afterClosed().subscribe((val) => {
console.log('Awaiting confirmation');
if(val){
stepper.next();
}
});
}
confirmPin(){
if (this.user.pin === this.pinForm.value.pinValue) {
this.userService.validateUser();
this.router.navigate(['/shop']);
} else {
this.pinError = true;
}
}
ngOnDestroy(){
this.FullnodeSub.unsubscribe();
this.UserSub.unsubscribe();
clearInterval(this.intervalHolder);
}
}

View File

@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { FullnodeService} from './fullnode.service';
@Injectable({
providedIn: 'root'
})
export class NodeResolverService implements Resolve<any> {
constructor(private fullnode: FullnodeService) {}
resolve(route: ActivatedRouteSnapshot): Observable<any> {
console.log('Called getAddr in resolver...', route);
return this.fullnode.getAddr().pipe(
catchError(error => {
return of('No data');
})
);
}
}

View File

@ -0,0 +1,10 @@
.text {
font-family: 'Spartan', sans-serif;
}
.number{
font-family: 'Roboto Mono', monospace;
}
img.icon{
margin-bottom: -3px;
}

View File

@ -0,0 +1,41 @@
<div align="center">
<p *ngIf="order.address.length == 0">No open order!</p>
</div>
<mat-card class="text" *ngIf="order.address.length > 0">
<div align="center">
<mat-card-title>
<table cellspacing="0" width="100%">
<tr>
<td>Order Total:</td>
<td align="right">
<p class="number">{{total | currency: getCurrency()}}</p>
<p class="number"><img class="icon" src="/assets/spartan-zec.png" width="15px" />{{(total/price) | number: '1.0-6'}}</p>
</td>
</tr>
</table>
</mat-card-title>
<table>
<tr>
<th>Qty.</th>
<th>Items</th>
</tr>
<tr *ngFor="let item of order.lines">
<td align="right">{{item.qty}}</td>
<td>{{item.name}}</td>
</tr>
</table>
</div>
<mat-card-actions>
<table cellspacing="0" width="100%">
<tr>
<td>
<button mat-raised-button class="text" (click)="cancelOrder()">Cancel</button>
</td>
<td align="right">
<button mat-raised-button class="text" color="primary" (click)="checkout()">Checkout</button>
</td>
</tr>
</table>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,124 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog, MatDialogConfig} from '@angular/material/dialog';
import { Observable } from 'rxjs';
import { Order } from './order.model';
import { FullnodeService } from '../fullnode.service';
import { UserService } from '../user.service';
import { OrderService } from './order.service';
import { CancelComponent } from '../cancel/cancel.component';
import { CheckoutComponent} from '../checkout/checkout.component';
import { ReceiptQRComponent} from '../receipt-qr/receipt-qr.component';
@Component({
selector: 'app-order',
templateUrl: './order.component.html',
styleUrls: ['./order.component.css']
})
export class OrderComponent implements OnInit{
public order: Order = {
address: '',
session: '',
timestamp: '',
closed: false,
currency: '',
price:0,
total:0,
totalZec: 0,
lines: [
{
qty: 1,
name: '',
cost: 0
}
]
};
public price: number = 1;
public total: number = 0;
public orderUpdate: Observable<Order>;
public priceUpdate: Observable<number>;
public totalUpdate: Observable<number>;
constructor(
public fullnodeService: FullnodeService,
public orderService: OrderService,
private dialog: MatDialog
) {
this.priceUpdate = fullnodeService.priceUpdate;
this.priceUpdate.subscribe((price) => {
this.price = price;
});
this.orderUpdate = orderService.orderUpdate;
this.orderUpdate.subscribe((order) => {
this.order = order;
});
this.totalUpdate = orderService.totalUpdate;
this.totalUpdate.subscribe((total) => {
this.total = total;
});
}
ngOnInit() {
}
cancelOrder() {
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.data = {title: 'Cancel Order?', msg: 'Are you sure you want to cancel the order?'};
const dialogRef = this.dialog.open(CancelComponent, dialogConfig);
dialogRef.afterClosed().subscribe((val) => {
if (val) {
console.log('Canceling');
this.orderService.cancelOrder(this.order._id!).subscribe((response) => {
this.orderService.getOrder();
});;
} else {
console.log('Returning to page');
}
this.orderService.getOrder();
});
}
checkout() {
var zec = this.total/this.price;
this.order.totalZec = parseFloat(zec.toFixed(6));
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = true;
dialogConfig.autoFocus = true;
dialogConfig.data = {
totalZec: this.order.totalZec,
addr: this.order.address,
orderId: this.order._id
};
const dialogRef = this.dialog.open(CheckoutComponent, dialogConfig);
dialogRef.afterClosed().subscribe((val) => {
if (val) {
const dialogConfig2 = new MatDialogConfig();
dialogConfig2.disableClose = true;
dialogConfig2.autoFocus = true;
dialogConfig2.data = {
order: this.order._id
};
console.log('Payment confirmed!');
this.orderService.closeOrder();
const dialogRef2 = this.dialog.open(ReceiptQRComponent, dialogConfig2);
dialogRef2.afterClosed().subscribe( val => {
if (val) {
console.log('Receipt closed.');
}
});
} else {
console.log('Returning to order');
}
});
}
getCurrency(){
return this.order.currency.toUpperCase();
}
}

View File

@ -0,0 +1,14 @@
import { LineItem } from '../items/lineitem.model';
export interface Order {
_id?: string,
address: string,
session: string,
timestamp?: string,
closed: boolean,
currency: string,
price?: number,
total: number,
totalZec: number,
lines: LineItem[]
}

View File

@ -0,0 +1,225 @@
import { Injectable } from '@angular/core';
import { Subject, BehaviorSubject, Observable } from 'rxjs';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Order } from './order.model';
import { UserService } from '../user.service';
import { FullnodeService } from '../fullnode.service';
import { User } from '../user.model';
import { Owner } from '../owner.model';
import { LineItem} from '../items/lineitem.model';
@Injectable({providedIn: 'root'})
export class OrderService {
beUrl = 'http://localhost:3000/';
private dataStore: {allOrders: Order[], user: User, order: Order, owner: Owner } = {
allOrders: [],
user:{
address: '',
session: '',
blocktime: 0,
expired: false,
pin: '',
validated: false
},
owner: {
_id: '',
name: '',
address: '',
currency: 'usd',
tax: false,
taxValue: 0,
vat: false,
vatValue: 0,
first: '',
last: '',
email: '',
street: '',
city: '',
state: '',
postal: '',
phone: '',
paid: false,
website: '',
country: '',
zats: false
},
order: {
address: '',
session: '',
timestamp: '',
closed: false,
currency: '',
price: 0,
total: 0,
totalZec: 0,
lines: [
{
qty: 1,
name: '',
cost:0
}
]
}
};
private _orderUpdated: BehaviorSubject<Order> = new BehaviorSubject(this.dataStore.order);
public readonly orderUpdate: Observable<Order> = this._orderUpdated.asObservable();
private _totalUpdated: BehaviorSubject<number> = new BehaviorSubject(this.dataStore.order.total);
public readonly totalUpdate: Observable<number> = this._totalUpdated.asObservable();
private _allOrdersUpdated: BehaviorSubject<Order[]> = new BehaviorSubject(this.dataStore.allOrders);
public readonly allOrdersUpdate: Observable<Order[]> = this._allOrdersUpdated.asObservable();
public userUpdate: Observable<User>;
public ownerUpdate: Observable<Owner>;
private apiKey = 'YourApiKey';
private reqHeaders: HttpHeaders;
constructor(
private http: HttpClient,
public fullnodeService: FullnodeService,
public userService: UserService
) {
this.reqHeaders = new HttpHeaders().set('Authorization', this.apiKey);
this.userUpdate = userService.userUpdate;
this.ownerUpdate = userService.ownerUpdate;
this.userUpdate.subscribe((user) => {
this.dataStore.user = user;
//console.log('OS: const', user);
this.getOrder();
});
this.ownerUpdate.subscribe((owner) => {
this.dataStore.owner = owner;
});
}
getOrder() {
var session = this.dataStore.user.session;
const params = new HttpParams().append('session', session);
let obs = this.http.get<{message: string, order: any}>(this.beUrl+'api/order', { headers:this.reqHeaders, params:params, observe: 'response'});
obs.subscribe((OrderDataResponse) => {
if (OrderDataResponse.status == 200) {
this.dataStore.order = OrderDataResponse.body!.order;
this._orderUpdated.next(Object.assign({}, this.dataStore).order);
this.dataStore.order.total = 0;
for(var line of this.dataStore.order.lines) {
this.dataStore.order.total += line.qty * line.cost;
}
this._totalUpdated.next(Object.assign({}, this.dataStore).order.total);
} else {
console.log('No order found');
}
});
return obs;
}
getAllOrders(){
var address = this.dataStore.user.address;
const params = new HttpParams().append('address', address);
let obs = this.http.get<{message: string, orders: any}>(this.beUrl+'api/allorders', { headers:this.reqHeaders, params:params, observe: 'response'});
obs.subscribe((OrdersData) => {
if (OrdersData.status == 200 ){
console.log('getAllOrder:', OrdersData.body);
this.dataStore.allOrders = OrdersData.body!.orders;
this._allOrdersUpdated.next(Object.assign({}, this.dataStore).allOrders);
} else {
console.log('No orders');
}
});
return obs;
}
addToOrder(lineItem: LineItem) {
if(this.dataStore.order._id != null) {
let obs = this.http.post<{message: string}>(this.beUrl+'api/lineitem', { order_id: this.dataStore.order._id, line: lineItem }, { headers: this.reqHeaders });
obs.subscribe((orderData) => {
this.getOrder();
});
} else {
this.createOrder(lineItem);
}
}
createOrder(lineItem: LineItem) {
var order:Order = {
address: this.dataStore.user.address,
session: this.dataStore.user.session,
currency: this.dataStore.owner.currency,
closed: false,
totalZec: 0,
price: 0,
total: 0,
lines: []
};
let obs = this.http.post<{message: string, order: Order}>(this.beUrl+'api/order', {order: order}, { headers: this.reqHeaders });
obs.subscribe((orderData) => {
console.log('Create order', orderData);
this.dataStore.order = orderData.order;
this._orderUpdated.next(Object.assign({}, this.dataStore).order);
this.addToOrder(lineItem);
});
return obs;
}
cancelOrder(id: string) {
let obs = this.http.delete<{message: string}>(this.beUrl+'api/order/'+id, { headers: this.reqHeaders });
obs.subscribe((OrderResponse) => {
console.log('Order deleted');
//this.getOrder();
this.dataStore.order = {
address: '',
session: '',
timestamp: '',
closed: false,
currency: '',
total: 0,
totalZec: 0,
price: 0,
lines: [
{
qty: 1,
name: '',
cost:0
}
]
};
this._orderUpdated.next(Object.assign({}, this.dataStore).order);
});
return obs;
}
closeOrder(){
this.fullnodeService.priceUpdate.subscribe((price) => {
this.dataStore.order.price = price;
});
this.dataStore.order.closed = true;
let obs = this.http.post<{message: string, order: Order}>(this.beUrl+'api/order', {order: this.dataStore.order}, { headers: this.reqHeaders });
obs.subscribe((orderData) => {
console.log('Closed order', orderData);
this.dataStore.order = {
address: '',
session: '',
timestamp: '',
closed: false,
currency: '',
price: 0,
total: 0,
totalZec: 0,
lines: [
{
qty: 1,
name: '',
cost:0
}
]
};
this._orderUpdated.next(Object.assign({}, this.dataStore).order);
});
return obs;
}
}

22
src/app/owner.model.ts Normal file
View File

@ -0,0 +1,22 @@
export interface Owner {
_id?: string;
address: string;
name: string;
currency: string;
tax: boolean;
taxValue: number;
vat: boolean;
vatValue: number;
first: string;
last: string;
email: string;
street: string;
city: string;
state: string;
postal: string;
phone: string;
paid: boolean;
website: string;
country: string;
zats: boolean;
}

7
src/app/payment.model.ts Normal file
View File

@ -0,0 +1,7 @@
export interface Payment {
_id?: string;
address: string;
blocktime: number;
expiration: string;
amount: number;
}

View File

@ -0,0 +1,4 @@
.text {
font-family: 'Spartan', sans-serif;
}

View File

@ -0,0 +1,13 @@
<div align="center" mat-dialog-title>
<h4 class="text">Scan for your Receipt</h4>
</div>
<mat-dialog-content>
<div class="qrcode" id="receipt-qr"> </div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button class="text" (click)="close()">
<mat-icon class="icon">close</mat-icon>Close
</button>
</mat-dialog-actions>

View File

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

View File

@ -0,0 +1,38 @@
import { Inject, Component, OnInit, ViewEncapsulation} from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
var QRCode = require('easyqrcodejs');
@Component({
selector: 'app-receipt-qr',
templateUrl: './receipt-qr.component.html',
styleUrls: ['./receipt-qr.component.css']
})
export class ReceiptQRComponent implements OnInit {
receiptUrl: SafeUrl;
codeString: string = '';
constructor(
private dialogRef: MatDialogRef<ReceiptQRComponent>,
private sanitizer: DomSanitizer,
@Inject(MAT_DIALOG_DATA) public data: { order: string}
) {
this.codeString = `https://zgo.cash/receipt/${data.order}`;
this.receiptUrl = this.sanitizer.bypassSecurityTrustUrl(this.codeString);
}
ngOnInit(): void {
var qrcode = new QRCode(document.getElementById("receipt-qr"), {
text: this.codeString,
logo: "/assets/zgo-prp.png",
logoWidth: 80,
logoHeight: 80
});
}
close() {
this.dialogRef.close(true);
}
}

View File

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

View File

@ -0,0 +1,91 @@
import { Injectable } from '@angular/core';
import { Subject, BehaviorSubject, Observable } from 'rxjs';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Order } from './order/order.model';
import { Owner } from './owner.model';
import { UserService } from './user.service';
@Injectable({
providedIn: 'root'
})
export class ReceiptService {
beUrl = 'http://localhost:3000/';
private dataStore: {order: Order, owner: Owner } = {
owner: {
_id: '',
name: '',
address: '',
currency: 'usd',
tax: false,
taxValue: 0,
vat: false,
vatValue: 0,
first: '',
last: '',
email: '',
street: '',
city: '',
state: '',
postal: '',
phone: '',
paid: false,
website: '',
country: '',
zats: false
},
order: {
address: '',
session: '',
timestamp: '',
closed: false,
currency: '',
price: 0,
total: 0,
totalZec: 0,
lines: [
{
qty: 1,
name: '',
cost:0
}
]
}
};
private _orderUpdated: BehaviorSubject<Order> = new BehaviorSubject(this.dataStore.order);
public readonly orderUpdate: Observable<Order> = this._orderUpdated.asObservable();
public _nameUpdated: BehaviorSubject<string> = new BehaviorSubject(this.dataStore.owner.name);
public readonly nameUpdate: Observable<string>= this._nameUpdated.asObservable();
public readonly ownerUpdate;
private apiKey = 'YourApiKey';
private reqHeaders: HttpHeaders;
constructor(
private http: HttpClient,
public userService: UserService
) {
this.reqHeaders = new HttpHeaders().set('Authorization', this.apiKey);
this.ownerUpdate = userService.ownerUpdate;
}
getOrderById(id:string) {
const params = new HttpParams().append('id', id);
let obs = this.http.get<{message: string, order: any}>(this.beUrl+'api/receipt', { headers:this.reqHeaders, params:params, observe: 'response'});
obs.subscribe((OrderDataResponse) => {
if (OrderDataResponse.status == 200) {
this.dataStore.order = OrderDataResponse.body!.order;
this._orderUpdated.next(Object.assign({}, this.dataStore).order);
this.userService.getOwner(this.dataStore.order.address);
this.ownerUpdate.subscribe((owner) => {
this.dataStore.owner = owner;
this._nameUpdated.next(Object.assign({}, this.dataStore).owner.name);
});
} else {
console.log('No order found');
}
});
return obs;
}
}

View File

@ -0,0 +1,36 @@
* {
font-family: 'Spartan', sans-serif;
}
.spacer{
flex: 1 1 auto;
}
.mat-card{
font-family: 'Spartan', sans-serif;
border: 1px solid #FF7522;
width: 80%;
text-align: left;
margin: 5px;
max-width: 500px;
}
.mat-card-title{
font-size: 16px;
font-weight: 700;
}
.mat-card-subtitle{
font-size: 12px;
font-weight: 200;
}
.mat-card-content{
font-size: 14px;
text-align: justify;
}
span.tt{
font-family: 'Roboto-Mono', monospace;
}
img.total{
margin-bottom:-2px;
}
.small{
font-size: 10px;
}

View File

@ -0,0 +1,44 @@
<mat-toolbar color="primary">
<span align="center">
<img class="logo" src="/assets/logo-new-white.png" height="40px" />
</span>
<span class="spacer"></span>
<span align="center">
<p class="text">{{name}}</p>
</span>
</mat-toolbar>
<div align="center">
<mat-card>
<mat-card-title>
Total: <img class="total" src="/assets/spartan-zec.png" height="18px" />{{order.totalZec | number: '1.0-6'}}
</mat-card-title>
<mat-card-subtitle>
{{order.timestamp | date}}
</mat-card-subtitle>
<mat-card-content>
<p class="small">Order ID: {{orderId}}</p>
<p>Zcash Price: {{order.price | currency: order.currency.toUpperCase()}}</p>
<div align="center">
<table>
<tr>
<th align="left">
Item
</th>
<th align="center">
Qty.
</th>
<th align="right">
Price ({{order.currency.toUpperCase()}})
</th>
</tr>
<tr *ngFor="let item of order.lines">
<td align="left">{{item.name}}</td>
<td align="center">{{item.qty}}</td>
<td align="right">{{(item.qty * item.cost) | currency: order.currency.toUpperCase()}} </td>
<tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>

View File

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

View File

@ -0,0 +1,55 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Order} from '../order/order.model';
import { ReceiptService } from '../receipt.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-receipt',
templateUrl: './receipt.component.html',
styleUrls: ['./receipt.component.css']
})
export class ReceiptComponent implements OnInit {
orderId;
public orderUpdate: Observable<Order>;
public nameUpdate: Observable<string>;
name: string = '';
order:Order = {
address: '',
session: '',
timestamp: '',
closed: false,
currency: '',
price: 0,
total: 0,
totalZec: 0,
lines: [
{
qty: 1,
name: '',
cost:0
}
]
};
constructor(
private _ActiveRoute:ActivatedRoute,
public receiptService: ReceiptService
) {
this.orderId = this._ActiveRoute.snapshot.paramMap.get("orderId");
this.orderUpdate = receiptService.orderUpdate;
this.nameUpdate = receiptService.nameUpdate;
receiptService.getOrderById(this.orderId!);
this.orderUpdate.subscribe(order => {
this.order = order;
});
this.nameUpdate.subscribe(name => {
this.name = name;
});
}
ngOnInit(): void {
}
}

View File

@ -0,0 +1,12 @@
.text {
font-family: 'Spartan', sans-serif;
}
.mat-dialog-title{
line-height: 20px;
}
h4.text{
margin: 0px;
}
.small {
font-size: small;
}

View File

@ -0,0 +1,27 @@
<div align="center" mat-dialog-title>
<h4 class="text">Scan the QR code</h4>
</div>
<mat-dialog-content>
<div align="center">
<p class="text">Ensure to check the "Include Reply-To" box in your wallet before sending your memo.</p>
<div class="qrcode" id="checkout-qr"></div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<table cellspacing="0" width="100%">
<tr>
<td>
<button mat-raised-button class="text" color="primary" (click)="confirm()">
<mat-icon class="icon">verified_user</mat-icon>Memo Sent!
</button>
</td>
<td align="right">
<button mat-raised-button class="text" (click)="close()">
<mat-icon class="icon">close</mat-icon>Cancel
</button>
</td>
</tr>
</table>
<p class="small text">Can't scan? Use this <a [href]="zcashUrl">wallet link</a>.
</mat-dialog-actions>

View File

@ -0,0 +1,56 @@
import { Inject, Component, OnInit, ViewEncapsulation} from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
var QRCode = require('easyqrcodejs');
var URLSafeBase64 = require('urlsafe-base64');
var Buffer = require('buffer/').Buffer;
@Component({
selector: 'app-scan',
templateUrl: './scan.component.html',
styleUrls: ['./scan.component.css']
})
export class ScanComponent implements OnInit{
address: string;
total: number;
session: string;
codeString: string = '';
pay: boolean = false;
zcashUrl: SafeUrl;
constructor(
private dialogRef: MatDialogRef<ScanComponent>,
private sanitizer: DomSanitizer,
@Inject(MAT_DIALOG_DATA) public data: { totalZec: number, addr: string, session: string, pay: boolean}
) {
this.address = data.addr;
this.total = data.totalZec;
this.session = data.session;
this.pay = data.pay;
if (this.pay) {
this.codeString = `zcash:${this.address}?amount=${this.total.toFixed(8)}&memo=${URLSafeBase64.encode(Buffer.from('ZGOp::'.concat(this.session)))}`;
} else {
this.codeString = `zcash:${this.address}?amount=${this.total.toFixed(8)}&memo=${URLSafeBase64.encode(Buffer.from('ZGO::'.concat(this.session)))}`;
}
this.zcashUrl = this.sanitizer.bypassSecurityTrustUrl(this.codeString);
}
ngOnInit() {
var qrcode = new QRCode(document.getElementById("checkout-qr"), {
text: this.codeString,
logo: "/assets/zcash.png",
logoWidth: 80,
logoHeight: 80
});
}
confirm() {
this.dialogRef.close(true);
}
close() {
this.dialogRef.close(false);
}
}

View File

@ -0,0 +1,15 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'searchOptions'
})
export class SearchOptionsPipe implements PipeTransform {
transform(items: any[], filter: string): any {
if (!items || !filter) {
return items;
}
// This will search and match any option.value that contains the search term
return items.filter(item => item.name.toLowerCase().indexOf(filter.toLowerCase()) !== -1);
}
}

View File

@ -0,0 +1,7 @@
* {
font-family: 'Spartan', sans-serif;
}
.mat-dialog-content {
max-width: 300px;
}

View File

@ -0,0 +1,24 @@
<h2 mat-dialog-title class="text">Settings</h2>
<mat-dialog-content [formGroup]="settingsForm">
<mat-form-field appearance="outline">
<mat-label>Name</mat-label>
<input matInput placeholder="Name" formControlName="name">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Currency</mat-label>
<mat-select formControlName="currency">
<mat-option *ngFor="let coin of coins" [value]="coin.symbol">
{{coin.label}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-slide-toggle formControlName="useZats" (change)="onChange($event)">
Display Zcash amount in zatoshis?
</mat-slide-toggle>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button class="text" (click)="close()">Cancel</button>
<button mat-raised-button class="text" color="primary" (click)="save()">Save</button>
</mat-dialog-actions>

View File

@ -0,0 +1,69 @@
import { Inject, Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { FormBuilder, Validators, FormGroup, FormControl } from '@angular/forms';
import {User} from '../user.model';
import {Owner} from '../owner.model';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['/settings.component.css']
})
export class SettingsComponent implements OnInit {
settingsForm: FormGroup;
owner: Owner;
useZats: boolean;
coins = [
{
label: 'US Dollar',
symbol: 'usd'
},{
label: 'Euro',
symbol: 'eur'
},{
label: 'British Pound',
symbol: 'gbp'
},{
label: 'Canadian Dollar',
symbol: 'cad'
},{
label: 'Australian Dollar',
symbol: 'aud'
}
];
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<SettingsComponent>,
@Inject(MAT_DIALOG_DATA) public data: Owner
) {
this.useZats = data.zats;
this.settingsForm = fb.group({
name: [data.name, Validators.required],
currency: [data.currency, Validators.required],
useZats: [data.zats, Validators.required]
});
this.owner = data;
}
ngOnInit() {
}
close() {
this.dialogRef.close();
}
save() {
this.owner.name = this.settingsForm.value.name;
this.owner.currency = this.settingsForm.value.currency;
this.owner.zats = this.useZats;
this.dialogRef.close(this.owner);
}
onChange(ob: MatSlideToggleChange) {
this.useZats = ob.checked;
}
}

View File

@ -0,0 +1,8 @@
* {
font-family: 'Spartan', sans-serif;
}
mat-card.centercard{
max-width: 500px;
border: 1px solid #CCCCCC;
}

View File

@ -0,0 +1,106 @@
<h3 mat-dialog-title>Terms of Use</h3>
<mat-dialog-content [style.overflow]="'auto'" [style.height.px]="'400'">
<p>THIS TERMS OF USE AGREEMENT ("Agreement") is made between Vergara Technologies LLC ("Company") as operator of ZGo.cash and any person or entity ("User") who completes the process to utilize, or operate the software known as the ZGo application, and data processing service, communication service or other content or offered or provided with the software by the Company ("Software"). The Company and User are collectively referred to as the "Parties." BY CLICKING THE ACCEPTANCE BUTTON OR ACCESSING, USING ANY PART OF THE SOFTWARE, USER EXPRESSLY AGREES TO AND CONSENTS TO BE LEGALLY BOUND BY ALL OF THE TERMS OF THIS AGREEMENT. IF USER DOES NOT AGREE TO ALL OF THE TERMS OF THIS AGREEMENT, THE USER SHALL NOT BE AUTHORIZED TO ACCESS OR USE ANY PART OF THE SOFTWARE.
</p>
<h4>1. Rights and Obligations</h4>
<ol type="a">
<li>Description. The Software functions as an open source, digital point-of-sale application. The Software does not constitute an account by which the Company or any other third parties serve as financial intermediaries or custodians of User's Zcash or any other cryptocurrency. While the Software has undergone beta testing and continues to be improved by feedback from the developers community, open-source contributors and beta-testers, the Company cannot guarantee that there will be no bugs in the Software. User acknowledges that User's use of the Software is at User's risk, discretion and in compliance with all applicable laws. User is responsible for safekeeping User's passwords, PINs, private keys, viewing keys, backup recovery mnemonic passphrases and any other codes User uses to access the Software or any information, Zcash, or other cryptocurrency unit. IF USER LOSES ACCESS TO USER'S CRYPTOCURRENCY WALLET OR PRIVATE KEYS AND HAS NOT SEPARATELY STORED A BACKUP OF USER'S CRYPTOCURRENCY WALLET OR BACKUP RECOVERY MNEMONIC PHRASE(S) AND CORRESPONDING PASSWORD(S), USER ACKNOWLEDGES AND AGREES THAT ANY ORDERS OR ANY OTHER DATA USER HAS ASSOCIATED WITH THAT CRYPTOCURRENCY WALLET WILL BECOME INACCESSIBLE. All transaction requests are irreversible. The Company and its shareholders, directors, officers, employees, affiliates and agents cannot guarantee transaction confirmation or retrieve User's private keys or passwords if User loses or forgets them.</li>
<li>Accessibility. User agrees that from time to time the Software may be inaccessible or inoperable for any reason, including, without limitation: (i) equipment malfunctions; (ii) periodic maintenance procedures or repairs which the Company may undertake from time to time; or (iii) causes beyond the control of the Company or which are not reasonably foreseeable by the Company.</li>
<li>Equipment. User shall be solely responsible for providing, maintaining and ensuring compatibility with the Software, all hardware, software, electrical and other physical requirements for User's use of the Software, including, without limitation, telecommunications and internet access connections and links, web browsers or other equipment, compatible Zcash wallets, programs and services required to access and use the Software.</li>
<li>Security. User shall be solely responsible for the security, confidentiality and integrity of all information and content that User receives, transmits through or stores on the Software. User shall be solely responsible for any authorized or unauthorized access to any account of User by any person. User agrees to bear all responsibility for the confidentiality of User's security devices, information, keys, and passwords.</li>
<li>Privacy. When reasonably practicable, the Company will attempt to respect User's privacy. The Company will not monitor, edit, or disclose any personal information about User or User's account, including its contents or User's use of the Software, without User's prior consent unless the Company believes in good faith that such action is necessary to: (i) comply with legal process or other legal requirements of any governmental authority; (ii) protect and defend the rights or property of the Company; (iii) enforce this Agreement; (iv) protect the interests of users of the Software other than User or any other person; or (v) operate or conduct maintenance and repair of the Company's services or equipment, including the Software as authorized by law. User has no expectation of privacy with respect to the Internet generally.</li>
</ol>
<h4>2. Taxes and Fees</h4>
<p>All currency conversion charges, third party fees, sales, use, value-added, personal property or other tax, duty or levy of any kind, including interest and penalties thereon, whether imposed now or hereinafter by any governmental entity, and fees incurred by User by reason of User's access or use of the Software shall be the sole responsibility of User.</p>
<h4>3. User Representations</h4>
<p>User represents and warrants to the Company that:</p>
<ol type="a">
<li>If User is a natural person, User is over the age of eighteen (18).</li>
<li>User has the power and authority to enter into and perform User's obligations under this Agreement.</li>
<li>All information provided by User to the Company is truthful, accurate and complete</li>
<li>User will comply with all laws and regulations of any applicable jurisdiction with regard to User's access or use of the Software</li>
<li>User shall comply with all terms and conditions of this Agreement, including, without limitation, the provisions set forth at Section</li>
<li>User has provided and will provide accurate and complete information as required for access, use or installation of the Software.</li>
<li>User is not a citizen, national, or resident of Cuba, North Korea, Iran or Syria.</li>
</ol>
<h4>4. Prohibited uses</h4>
<p>User is solely responsible for any and all acts and omissions that occur under User's account, security information, keys or password, and User agrees not to engage in unacceptable use of the Software, which includes, without limitation, use of the Software for activities that:</p>
<ol type="a">
<li>Violate any law, statute, ordinance or regulation.</li>
<li>Create a false identity or to otherwise attempt to mislead any person as to the identity of the merchant.</li>
<li>Interfere, disrupt or attempt to gain unauthorized access to other accounts on the Software or any other computer network</li>
<li>Relate to transactions involving:</li>
<ol type="i">
<li>Narcotics, certain controlled substances or other products that present a risk to consumer safety.</li>
<li>Items that encourage, promote, facilitate or instruct others to engage in illegal activity.</li>
<li>Stolen goods including digital and virtual goods.</li>
<li>The promotion of hate, violence, racial or other forms of intolerance that is discriminatory.</li>
<li>Items that infringe or violate any copyright, trademark, right of publicity or privacy or any other proprietary right under the laws of any jurisdiction.</li>
<li>Certain weapons or knives regulated under applicable law.</li>
<li>Showing the personal information of third parties in violation of applicable law.</li>
<li>Pyramid or ponzi schemes, matrix programs, other "get rich quick" schemes or certain multi-level marketing programs.</li>
<li>Purchases of annuities or lottery contracts, lay-away systems, off-shore banking or transactions to finance or refinance debts funded by a credit card.</li>
<li>Sale of certain items before the seller has control or possession of the item.</li>
<li>Sale of traveler's checks or money orders.</li>
<li>Certain credit repair, debt settlement services, credit transactions or insurance activities.</li>
<li>Offering or receiving payments for the purpose of bribery or corruption.</li>
<li>Sales of products or services identified by government agencies to have a high likelihood of being fraudulent.</li>
<li>Any activity that requires pre-approval without having obtained said approval.</li>
</ol>
</ol>
<h4>5. Activities requiring approval</h4>
<p>ZGo.cash requires pre-approval to provide access to the application for certain services, including but not limited to:</p>
<ol type="a">
<li>Payment Facilitator. Providing payment services which would fall under the definition of a money service business or an electronic money institution. Services would also include the sale of stored value cards and escrow services.</li>
<li>Investments. Buying, selling, or brokering stocks, bonds, securities, options, figures, commodities, contracts for difference/forex, mutual funds or an investment interest in any entity or property.</li>
<li>Gambling, Gaming, Prize Draws and Contests. Activities involving gambling, gaming and/or any other activity with an entry fee and a prize, including, but not limited to property/real estate prizes, casino games, sports betting, horse or greyhound racing, fantasy sports, lottery tickets, other ventures that facilitate gambling, games of skill (whether or not legally defined as gambling) and sweepstakes, if the operator and customers are located exclusively in jurisdictions where such activities are permitted by law.</li>
<li>Prescription Items. The sale of any product(s) requiring a prescription or prescription dispensing services.</li>
<li>Mature Audience Content. Any adult content delivered digitally including video on demand (VOD) and web-cam activities.</li>
<li>Alcohol.</li>
<li>Tobacco.</li>
<li>Medical Items or Services. All items classified as medical devices, and all services or treatment provided by a person or organization holding itself out as a provider of health-care services, including, but not limited to, all health-care services for which government licensure is required in the providers jurisdiction or in the jurisdiction where services are being provided. This category includes “Medical Tourism” involving medical services to be provided to a patient outside of that patients home country.</li>
</ol>
<h4>6. Termination</h4>
<p>This Agreement is effective upon User's acceptance as set forth herein and shall continue in full force so long as User engages in any access, use or installation of the Software. The Company reserves the right, in its sole discretion and without notice, at any time and for any reason, to:</p>
<ol type="a">
<li>Remove or disable access to all or any portion of the Software</li>
<li>Suspend User's access to or use of all or any portion of the Software</li>
<li>Terminate this Agreement.</li>
</ol>
<h4>7. Disclaimer of Warranties</h4>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. USE OF THE SOFTWARE IS AT USER'S SOLE RISK. THE COMPANY DOES NOT WARRANT THAT THE SOFTWARE WILL BE UNINTERRUPTED OR ERROR FREE, NOR DOES THE COMPANY MAKE ANY WARRANTY AS TO ANY RESULTS THAT MAY BE OBTAINED BY USE OF THE SOFTWARE. THE COMPANY MAKES NO OTHER WARRANTIES, EXPRESS OR IMPLIED. THE COMPANY EXPRESSLY DISCLAIMS ANY WARRANTY OF MERCHANTABILITY, WARRANTY OF SUITABILITY FOR A PARTICULAR PURPOSE, WARRANTY OF TITLE OR INTEREST, OR WARRANTY OF NONINFRINGEMENT.</p>
<h4>8. Limitation of Liability</h4>
<p>IN NO EVENT SHALL THE COMPANY OR ITS SHAREHOLDERS, DIRECTORS, OFFICERS, EMPLOYEES, AFFILIATES OR AGENTS, OR ANY OF ITS OR THEIR RESPECTIVE SERVICE PROVIDERS, BE LIABLE TO USER OR ANY THIRD PARTY FOR ANY USE, INTERRUPTION, DELAY OR INABILITY TO USE THE SOFTWARE, LOST REVENUES OR PROFITS, DELAYS, INTERRUPTION OR LOSS OF SERVICES, BUSINESS OR GOODWILL, LOSS OR CORRUPTION OF DATA, LOSS RESULTING FROM SYSTEM OR SYSTEM SERVICE FAILURE, MALFUNCTION OR SHUTDOWN, FAILURE TO ACCURATELY TRANSFER, READ OR TRANSMIT INFORMATION, FAILURE TO UPDATE OR PROVIDE CORRECT INFORMATION, SYSTEM INCOMPATIBILITY OR PROVISION OF INCORRECT COMPATIBILITY INFORMATION OR BREACHES IN SYSTEM SECURITY, OR FOR ANY CONSEQUENTIAL, INCIDENTAL, INDIRECT, EXEMPLARY, SPECIAL OR PUNITIVE DAMAGES, WHETHER ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT, BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE) OR OTHERWISE, REGARDLESS OF WHETHER SUCH DAMAGES WERE FORESEEABLE AND WHETHER OR NOT WE WERE ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN NO EVENT SHALL THE COMPANY OR ITS SHAREHOLDERS, DIRECTORS, OFFICERS, EMPLOYEES, AFFILIATES OR AGENTS, OR ANY OF ITS OR THEIR RESPECTIVE SERVICE PROVIDERS, BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM OR IN ANY WAY RELATED TO USER'S ACCESS, USE OR INSTALLATION OF THE SOFTWARE. SOME JURISDICTIONS PROHIBIT THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, THUS THIS LIMITATION OF LIABILITY MAY NOT APPLY TO USER. IF USER IS DISSATISFIED WITH THE SOFTWARE, USER'S SOLE AND EXCLUSIVE REMEDY SHALL BE FOR USER TO DISCONTINUE USE OF THE SOFTWARE.</p>
<h4>8. Indemnification</h4>
<p>User agrees to indemnify, hold harmless and defend the Company, its shareholders, directors, officers, employees, affiliates and agents ("Indemnified Parties") from and against any action, cause, claim, damage, debt, demand or liability, including reasonable costs and attorney's fees, asserted by any person, arising out of or relating to:</p>
<ol type="a">
<li>This Agreement</li>
<li>User's access, use or installation of the Software, including any data or work transmitted or received by User</li>
<li>Any unacceptable use of the Software by any person, including, without limitation, any statement, data or content made, transmitted or republished by User or any person which is prohibited as unacceptable under Section 4.</li>
</ol>
<p>THIS INDEMNIFICATION INCLUDES THE EXPRESS INDEMNIFICATION OF THE COMPANY AND ALL INDEMNIFIED PARTIES FOR ANY ALLEGED NEGLIGENCE (INCLUDING ANY ALLEGED GROSS NEGLIGENCE). OR OTHER ALLEGED MISCONDUCT OF THE COMPANY OR ANY INDEMNIFIED PARTIES.</p>
<h4>9. Intellectual Property</h4>
<p>The Company retains all right, title, and interest in and to all of the Company's brands, logos, and trademarks, including, but not limited to, Vergara Technologies LLC, ZGo, ZGo.cash, ZGo Cash App, and variations of the wording of the aforementioned brands, logos, and trademarks.</p>
<h4>10. Warnings</h4>
<p>User acknowledges that the Company shall not be responsible for transferring, safeguarding, or maintaining private keys and/or User's Zcash or any other cryptocurrency. If User and/or any co-signing authorities lose, mishandle, or have stolen associated private keys, or if User's cosigners refuse to provide requisite authority, User acknowledges that User may not be able to recover User's Zcash or any other cryptocurrency, and that the Company shall not be responsible for such loss.</p>
<p>User acknowledges and agrees that Zcash or any other cryptocurrency transactions facilitated by the Software and/or the Company may be delayed, and that the Company shall not be responsible for any associated loss. User acknowledges and agrees that the Company shall not be responsible for any aspect of the information, content, or services contained in any third-party materials or on any third party sites accessible or linked to the Software and/or the Company.</p>
<p>By using the Software, User acknowledges and agrees: (i) that the Company is not responsible for the operation of the underlying protocols and that the Company makes no guarantee of their functionality, security, or availability; and (ii) that the underlying protocols are subject to sudden main-chain changes in operating rules ("forks"), and that such forks may materially affect the value, and/or function of Zcash or any other cryptocurrency that may be supported on the Software. In the event of a fork, User agrees that the Company may temporarily suspend the Software operations (with or without notice to User) and that the Company may, in its sole discretion,</p>
<ol type="a">
<li>Configure or reconfigure its systems, or</li>
<li>decide not to support (or cease supporting) the forked protocol entirely.</li>
</ol>
<p>User acknowledges and agrees that the Company assumes absolutely no responsibility whatsoever in respect of an unsupported branch of a forked protocol.</p>
<h4>11. Miscellaneous</h4>
<ol type="a">
<li>Amendment. The Company shall have the right, at any time and without notice, to add to or modify the terms of this Agreement, simply by delivering such amended terms to User by electronic message through any medium to any address provided to the Company by User. User's access to or use of the Software after the date such amended terms are delivered to User shall be deemed to constitute acceptance of such amended terms.</li>
<li>Severance. If any provision or part-provision of this Agreement is, or becomes invalid, illegal or unenforceable, it shall be deemed modified to the minimum extent necessary to make it valid, legal and enforceable. If such modification is not possible, the relevant provision or part-provision shall be deemed deleted. Any modification to or deletion of a provision or part-provision under this Article shall not affect the validity and enforceability of the rest of this Agreement.</li>
<li>Entire Agreement Disclaimer of Reliance. This Agreement constitutes the entire agreement between the Parties with respect to the subject matter hereof and supersedes all prior agreements or understandings between the Parties. User expressly represents and warrants that it is not relying upon any statements, understandings, representations, expectations or agreements other than those expressly set forth in this Agreement.</li>
<li>THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION. User agrees that any and all disputes or claims against any person arising out of or in any way related to this Agreement or the access, use or installation of the Software by User or any other person shall be subject to binding arbitration under the Rules of Arbitration of the International Chamber of Commerce by one or more arbitrators appointed in accordance with the said Rules. The location of the arbitration shall be the state of Minnesota in the United States. The language of the arbitration shall be English.</li>
<li>LANGUAGE. Any translation of this Agreement is made for purposes of local reference only and in the event of any inconsistency between the English and any non-English versions, the English version of this Agreement shall prevail and govern in all respects.</li>
</ol>
</mat-dialog-content>
<mat-dialog-actions>
<div align="center">
<button mat-raised-button color="primary" (click)="close()">Close</button>
</div>
</mat-dialog-actions>

View File

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

View File

@ -0,0 +1,22 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
@Component({
selector: 'app-terms',
templateUrl: './terms.component.html',
styleUrls: ['./terms.component.css']
})
export class TermsComponent implements OnInit {
constructor(
private dialogRef: MatDialogRef<TermsComponent>
) { }
ngOnInit(): void {
}
close() {
this.dialogRef.close(false);
}
}

7
src/app/tx.model.ts Normal file
View File

@ -0,0 +1,7 @@
export interface Tx {
_id?: string;
address: string;
session: string;
confirmations: number;
amount: number;
}

9
src/app/user.model.ts Normal file
View File

@ -0,0 +1,9 @@
export interface User {
_id?: string;
address: string;
session: string;
blocktime: number;
expired: boolean;
pin: string;
validated: boolean;
}

208
src/app/user.service.ts Normal file
View File

@ -0,0 +1,208 @@
import {Injectable} from '@angular/core';
import {Subject, BehaviorSubject, Observable} from 'rxjs';
import {HttpClient, HttpParams, HttpHeaders} from '@angular/common/http';
import {User} from './user.model';
import {Owner} from './owner.model';
import { Country } from './country.model';
import {Tx} from './tx.model';
@Injectable({providedIn: 'root'})
export class UserService{
beUrl = 'http://localhost:3000/';
private dataStore: { user: User, owner: Owner, txs: Tx[], countries: Country[]} = {
user: {
address: '',
session: '',
blocktime: 0,
expired: false,
pin: '',
validated: false
},
owner: {
address: '',
name: '',
currency: 'usd',
tax: false,
taxValue: 0,
vat: false,
vatValue: 0,
first: '',
last: '',
email: '',
street: '',
city: '',
state: '',
postal: '',
phone: '',
paid: false,
website: '',
country: '',
zats: false
},
txs : [],
countries: []
};
private uZaddr = '';
private oZaddr = '';
private uName = '';
private session: string | null = '';
private _uZaddrUpdated: BehaviorSubject<string> = new BehaviorSubject(this.uZaddr);
private _userUpdated: BehaviorSubject<User> = new BehaviorSubject(this.dataStore.user);
private uNameUpdated = new Subject<string>();
private _ownerUpdated: BehaviorSubject<Owner> = new BehaviorSubject(this.dataStore.owner);
private _txsUpdated: BehaviorSubject<Tx[]> = new BehaviorSubject(this.dataStore.txs);
private _paidUpdated: BehaviorSubject<boolean> = new BehaviorSubject(this.dataStore.owner.paid);
private _countriesUpdated: BehaviorSubject<Country[]> = new BehaviorSubject(this.dataStore.countries);
public readonly uZaddrUpdate: Observable<string> = this._uZaddrUpdated.asObservable();
public readonly ownerUpdate: Observable<Owner> = this._ownerUpdated.asObservable();
public readonly userUpdate: Observable<User> = this._userUpdated.asObservable();
public readonly txUpdate: Observable<Tx[]> = this._txsUpdated.asObservable();
public readonly paidUpdate: Observable<boolean> = this._paidUpdated.asObservable();
public readonly countriesUpdate: Observable<Country[]> = this._countriesUpdated.asObservable();
private reqHeaders: HttpHeaders;
private apiKey = 'YourApiKey';
constructor(private http: HttpClient){
this.reqHeaders = new HttpHeaders().set('Authorization', this.apiKey);
//console.log('US:', this.reqHeaders);
this.session = localStorage.getItem('s4z_token');
if (this.session != null) {
this.findUser();
this.findPending();
}
}
getCountries() {
let obs = this.http.get<{message: string, countries: any}>(this.beUrl+'api/countries', { headers: this.reqHeaders, observe: 'response'});
obs.subscribe((CountryResponse) => {
if (CountryResponse.status == 200) {
this.dataStore.countries = CountryResponse.body!.countries;
this._countriesUpdated.next(Object.assign({}, this.dataStore).countries);
}
});
}
findUser() {
this.session = localStorage.getItem('s4z_token');
if (this.session != null) {
const params = new HttpParams().append('session', this.session!);
let obs = this.http.get<{message: string, user: any}>(this.beUrl+'api/getuser', { headers: this.reqHeaders, params: params, observe: 'response'});
obs.subscribe((UserDataResponse) => {
console.log(UserDataResponse.status);
if (UserDataResponse.status == 200){
this.dataStore.user = UserDataResponse.body!.user[0];
console.log(`US: Found user, returning it`);
this._uZaddrUpdated.next(Object.assign({},this.dataStore).user.address);
this._userUpdated.next(Object.assign({}, this.dataStore).user);
this.getOwner(Object.assign({},this.dataStore.user).address);
} else {
this.dataStore.user = {
address: '',
session: '',
blocktime: 0,
expired: false,
pin: '',
validated: false
};
this._uZaddrUpdated.next(Object.assign({},this.dataStore).user.address);
this._userUpdated.next(Object.assign({}, this.dataStore).user);
console.log('US: Did not find user');
}
});
return obs;
} else {
console.log('No session loaded');
return null;
}
}
findPending() {
this.session = localStorage.getItem('s4z_token');
if (this.session != null) {
const params = new HttpParams().append('session', this.session!);
let obs = this.http.get<{message: string, txs: any}>(this.beUrl+'api/pending', { headers: this.reqHeaders, params: params, observe: 'response'});
obs.subscribe((TxDataResponse) => {
//console.log('US Tx', TxDataResponse);
if (TxDataResponse.status == 200){
this.dataStore.txs = TxDataResponse.body!.txs;
console.log(`US: Pending logins found`);
this._txsUpdated.next(Object.assign({},this.dataStore).txs);
} else {
console.log('US: Did not find pending txs');
this.dataStore.txs = [];
this._txsUpdated.next(Object.assign({},this.dataStore).txs);
}
});
return obs;
} else {
console.log('No session loaded');
return null;
}
}
validateUser(){
var validatedUser: User = this.dataStore.user;
validatedUser.validated = true;
this.http.post<{message: string, user: User}>(this.beUrl+'api/validateuser', {user: validatedUser}, {headers: this.reqHeaders}).
subscribe((responseData) => {
console.log(responseData.message);
});
}
addOwner(owner: Owner) {
owner.address = this.dataStore.user.address;
let obs = this.http.post<{message: string}>(this.beUrl+'api/addowner', {owner: owner}, {headers: this.reqHeaders});
obs.subscribe((responseData) => {
console.log(responseData.message);
this.getOwner(this.dataStore.user.address);
});
return obs;
}
updateOwner(owner: Owner) {
this.http.post<{message: string, owner: Owner}>(this.beUrl+'api/updateowner', {owner: owner}, {headers: this.reqHeaders}).
subscribe((responseData) => {
console.log(responseData.message);
this.dataStore.owner = responseData.owner;
this._ownerUpdated.next(Object.assign({},this.dataStore).owner);
});
}
getOwner(address: string) {
console.log('getOwner', address);
const ownParams = new HttpParams().append('address', address);
let obs = this.http.get<{message:string, owner: any}>(this.beUrl+'api/getowner', { headers: this.reqHeaders, params: ownParams, observe: 'response'});
obs.subscribe((OwnerDataResponse) => {
console.log('api/getowner', OwnerDataResponse.status);
if (OwnerDataResponse.status == 200) {
this.dataStore.owner = OwnerDataResponse.body!.owner[0];
//console.log('getOwner object', this.dataStore.owner);
this._ownerUpdated.next(Object.assign({},this.dataStore).owner);
this._paidUpdated.next(Object.assign({}, this.dataStore).owner.paid);
}
});
return obs;
}
deleteUser() {
let obs = this.http.delete<{message: string}>(this.beUrl+'api/user/'+this.dataStore.user._id, {headers: this.reqHeaders });
obs.subscribe(UserResponse => {
console.log('User delete request sent.');
this.findUser();
});
return obs;
}
}

View File

@ -0,0 +1,13 @@
* {
font-family: 'Spartan', sans-serif;
}
.icon{
font-family: 'Material Icons';
}
.small{
font-size: x-small;
margin-top: 0px;
}
h3{
margin-bottom: 0px;
}

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