Compare commits
74 Commits
v2.0.0-beta.1
...
v2.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
bd199fcf26
|
|||
| 0777c213ba | |||
|
071922d979
|
|||
|
fc044df9fd
|
|||
|
dd2bdd5332
|
|||
|
0cd05d14bc
|
|||
|
3ee140723c
|
|||
|
49b3825af4
|
|||
|
af309ff682
|
|||
|
53f13e4a63
|
|||
|
bcdd033f34
|
|||
|
b9e7737589
|
|||
|
57b9bccb4a
|
|||
|
5c9fbf5043
|
|||
|
205d2d2afc
|
|||
| 895bf569ff | |||
|
dbedf1dbe8
|
|||
| 26f2f2afe9 | |||
|
6c456b1184
|
|||
|
03e7f14d1c
|
|||
|
e94e747ba1
|
|||
|
3679412b3c
|
|||
|
ea15e69d79
|
|||
|
8cc3da40b7
|
|||
|
bdeab512b6
|
|||
| 97ebebe28c | |||
|
d5edb93ec5
|
|||
|
0ecc4079c2
|
|||
|
3f080cf553
|
|||
|
0bdb4d2021
|
|||
|
1e7d8491f9
|
|||
|
4356288497
|
|||
|
87bf8ff2df
|
|||
|
26975636a0
|
|||
| dad56a7b31 | |||
|
d069fa02ab
|
|||
|
d219abc358
|
|||
|
13989bf973
|
|||
|
b3ab69d599
|
|||
|
fe3816501c
|
|||
| 854402aa73 | |||
|
abc19c715b
|
|||
|
b6434ac8d2
|
|||
|
4788b8c51c
|
|||
|
6a72a5b418
|
|||
|
d90ddd464a
|
|||
|
2626543f3e
|
|||
| 088db24534 | |||
| f7b7daa024 | |||
| bb11e2267b | |||
| 7cd023a21b | |||
|
1095bf0218
|
|||
|
138cec0389
|
|||
|
a93be41e08
|
|||
|
89f6fa0b5c
|
|||
|
e2a80eafd1
|
|||
|
662e76979b
|
|||
|
6c7de97e38
|
|||
|
fe27b010da
|
|||
|
ff716c68ea
|
|||
|
84dded2f10
|
|||
|
ee664c06c1
|
|||
|
62b618b657
|
|||
| 348dc03429 | |||
|
d159fca816
|
|||
|
a582f41fde
|
|||
|
2082b51c5b
|
|||
|
e543708b42
|
|||
|
0b0dea095f
|
|||
|
c7eb81450c
|
|||
|
ca1ccf6d93
|
|||
| 0b79d48be9 | |||
|
f8cc453d7e
|
|||
|
f86190030f
|
@@ -0,0 +1,30 @@
|
|||||||
|
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [16.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm install
|
||||||
|
- run: npm test
|
||||||
@@ -48,6 +48,12 @@ module.exports = {
|
|||||||
"no-action"
|
"no-action"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/expense-list/template",
|
||||||
|
"only": [
|
||||||
|
"no-invalid-role"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"moduleId": "app/components/topbar-account-panel/template",
|
"moduleId": "app/components/topbar-account-panel/template",
|
||||||
"only": [
|
"only": [
|
||||||
|
|||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- "14"
|
|
||||||
- "16"
|
|
||||||
|
|
||||||
sudo: false
|
|
||||||
dist: precise
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
||||||
|
|
||||||
addons:
|
|
||||||
chrome: stable
|
|
||||||
|
|
||||||
cache:
|
|
||||||
npm: false
|
|
||||||
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
[](https://travis-ci.org/67P/kredits-web)
|
[](https://github.com/67P/kredits-web/actions/workflows/ci.yml?query=branch%3Amaster)
|
||||||
|
|
||||||
# kredits-web
|
# Kredits Web
|
||||||
|
|
||||||
This README outlines the details of collaborating on this Ember application.
|
This is the main Web UI for Kosmos [Kredits](https://wiki.kosmos.org/Kredits).
|
||||||
A short introduction of this app could easily go here.
|
It provides an overview of contributors and contributions, as well as the
|
||||||
|
community's budget, expenses, and reimbursements.
|
||||||
|
|
||||||
|
It is an unhosted Web app, fetching all data from
|
||||||
|
[Rootstock](https://rootstock.io/) and [IPFS](https://ipfs.tech/), and caching
|
||||||
|
it in the browser's local database. As such, you can run the app directly from
|
||||||
|
the `/release` directory on any Web server that can serve static assets.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -24,9 +30,9 @@ You will need the following things properly installed on your computer.
|
|||||||
|
|
||||||
### Building/running for development
|
### Building/running for development
|
||||||
|
|
||||||
* `ember serve` - by default kredits-web connects to the Ethreum Kovan network
|
* `npm start` - by default Kredits Web connects to the Rootstock testnet network
|
||||||
* Visit your app at [http://localhost:4200](http://localhost:4200).
|
* Visit the app at [http://localhost:4200](http://localhost:4200).
|
||||||
* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).
|
* Visit the tests at [http://localhost:4200/tests](http://localhost:4200/tests).
|
||||||
|
|
||||||
See [working with locally deployed contracts](https://github.com/67P/kredits-web#working-with-locally-deployed-contracts) for details on how to develop with locally deployed contracts.
|
See [working with locally deployed contracts](https://github.com/67P/kredits-web#working-with-locally-deployed-contracts) for details on how to develop with locally deployed contracts.
|
||||||
|
|
||||||
@@ -48,7 +54,7 @@ Make use of the many generators for code, try `ember help generate` for more det
|
|||||||
### Building
|
### Building
|
||||||
|
|
||||||
* `ember build` (development)
|
* `ember build` (development)
|
||||||
* `ember build --environment production` (production)
|
* `npm run build` (production)
|
||||||
|
|
||||||
### Deploying
|
### Deploying
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<form onsubmit={{action "submit"}}>
|
<form onsubmit={{action "submit"}}>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-account">Ethereum account</label>
|
<label for="c-account">Rootstock account</label>
|
||||||
<Input @type="text"
|
<Input @type="text"
|
||||||
@value={{this.account}}
|
@value={{this.account}}
|
||||||
name="account" id="c-account"
|
name="account" id="c-account"
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ export default class AddExpenseItemComponent extends Component {
|
|||||||
|
|
||||||
let dateInput = (this.date instanceof Array) ?
|
let dateInput = (this.date instanceof Array) ?
|
||||||
this.date[0] : this.date;
|
this.date[0] : this.date;
|
||||||
const [ date ] = dateInput.toISOString().split('T');
|
|
||||||
|
const [ date ] = moment(dateInput).utcOffset(0, true)
|
||||||
|
.toISOString()
|
||||||
|
.split('T');
|
||||||
|
|
||||||
const isValid = this.validateForm();
|
const isValid = this.validateForm();
|
||||||
if (!isValid) return false;
|
if (!isValid) return false;
|
||||||
@@ -78,7 +81,9 @@ export default class AddExpenseItemComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isPresent(this.tags)) {
|
if (isPresent(this.tags)) {
|
||||||
expense.tags = this.tags.split(',').map(t => t.trim());
|
expense.tags = this.tags.split(',')
|
||||||
|
.map(t => t.trim())
|
||||||
|
.filter(t => t.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.args.addExpenseItem(expense);
|
this.args.addExpenseItem(expense);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { action } from '@ember/object';
|
|||||||
import { A } from '@ember/array';
|
import { A } from '@ember/array';
|
||||||
import { scheduleOnce } from '@ember/runloop';
|
import { scheduleOnce } from '@ember/runloop';
|
||||||
import isValidAmount from 'kredits-web/utils/is-valid-amount';
|
import isValidAmount from 'kredits-web/utils/is-valid-amount';
|
||||||
|
import readFileContent from 'kredits-web/utils/read-file-content';
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
export default class AddReimbursementComponent extends Component {
|
export default class AddReimbursementComponent extends Component {
|
||||||
@@ -84,6 +85,23 @@ export default class AddReimbursementComponent extends Component {
|
|||||||
this.total = btcAmount.toFixed(8);
|
this.total = btcAmount.toFixed(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO use ember-concurrency here
|
||||||
|
// https://github.com/67P/kredits-web/pull/209#discussion_r1064234421
|
||||||
|
@action
|
||||||
|
async addExpensesFromFile (evt) {
|
||||||
|
const content = await readFileContent(evt.target.files[0]);
|
||||||
|
const expenses = JSON.parse(content);
|
||||||
|
|
||||||
|
if (expenses instanceof Array) {
|
||||||
|
for (const item of expenses) {
|
||||||
|
this.addExpenseItem(item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Expenses in file must be a list of items:");
|
||||||
|
console.debug(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateContributor (event) {
|
updateContributor (event) {
|
||||||
this.recipientId = event.target.value;
|
this.recipientId = event.target.value;
|
||||||
|
|||||||
@@ -44,26 +44,10 @@
|
|||||||
|
|
||||||
<h3>Expense items</h3>
|
<h3>Expense items</h3>
|
||||||
{{#if this.expenses}}
|
{{#if this.expenses}}
|
||||||
<ul class="expense-list">
|
<ExpenseList @expenses={{this.expenses}}
|
||||||
{{#each this.expenses as |expense|}}
|
@removeExpenseItem={{this.removeExpenseItem}}
|
||||||
<li>
|
@deletable={{true}} />
|
||||||
<div class="description" rowspan="2">
|
|
||||||
<h4>
|
|
||||||
<span class="date">{{expense.date}}:</span>
|
|
||||||
<span class="title">{{expense.title}}</span>
|
|
||||||
</h4>
|
|
||||||
<p class="description">{{expense.description}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="amount">
|
|
||||||
{{fmt-fiat-currency expense.amount expense.currency}}
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<button {{on "click" (fn this.removeExpenseItem expense)}}
|
|
||||||
class="danger small" type="button">delete</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
<button {{on "click" this.showExpenseForm}}
|
<button {{on "click" this.showExpenseForm}}
|
||||||
class="green small" type="button">+ Add another item</button>
|
class="green small" type="button">+ Add another item</button>
|
||||||
@@ -87,3 +71,9 @@
|
|||||||
<AddExpenseItem @addExpenseItem={{fn this.addExpenseItem}} />
|
<AddExpenseItem @addExpenseItem={{fn this.addExpenseItem}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</form>
|
</form>
|
||||||
|
<form id="add-expenses-from-file">
|
||||||
|
<h3>Add expense items from file</h3>
|
||||||
|
<input type="file" multiple="false"
|
||||||
|
onchange={{fn this.addExpensesFromFile}}
|
||||||
|
accept="application/json" />
|
||||||
|
</form>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
|
|
||||||
export default class BudgetBalancesComponent extends Component {
|
export default class BudgetBalancesComponent extends Component {
|
||||||
@service communityFunds
|
@service communityFunds;
|
||||||
@alias('communityFunds.balances') balances;
|
|
||||||
|
get balancesSorted () {
|
||||||
|
return this.communityFunds.balances
|
||||||
|
.sortBy('confirmed_balance').reverse();
|
||||||
|
}
|
||||||
|
|
||||||
get loading () {
|
get loading () {
|
||||||
return !this.communityFunds.balancesLoaded;
|
return !this.communityFunds.balancesLoaded;
|
||||||
|
|||||||
@@ -7,11 +7,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{#each this.balances as |balance|}}
|
{{#each this.balancesSorted as |balance|}}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{balance.token.symbol}}</th>
|
<th>
|
||||||
<td class="amount">{{balance.confirmed_balance}}</td>
|
<img src={{balance.token.icon}}
|
||||||
<td class="fiat-amount">~{{balance.balanceUSD}} USD</td>
|
alt={{balance.token.description}}
|
||||||
|
title={{balance.token.description}} />
|
||||||
|
</th>
|
||||||
|
<td class="amount">
|
||||||
|
{{fmt-number balance.confirmed_balance}} <span class="unit">sats</span>
|
||||||
|
</td>
|
||||||
|
<td class="fiat-amount">
|
||||||
|
~{{balance.balanceUSD}} USD
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export default class ConfirmedInComponent extends Component {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
get confirmedInBlocks () {
|
||||||
|
return this.args.confirmedAtBlock - this.kredits.currentBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
get confirmedInSeconds () {
|
||||||
|
// A new block is mined every 30 seconds on average
|
||||||
|
return this.confirmedInBlocks * 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
get confirmedInHumanTime () {
|
||||||
|
return moment.duration(this.confirmedInSeconds, "seconds").humanize();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConfirmed () {
|
||||||
|
return this.confirmedInBlocks <= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{{#if this.isConfirmed}}
|
||||||
|
Confirmed at block <strong>{{@confirmedAtBlock}}</strong> (~ {{this.confirmedInHumanTime}} ago)
|
||||||
|
{{else}}
|
||||||
|
Confirming in <strong>{{this.confirmedInBlocks}}</strong> blocks (~ {{this.confirmedInHumanTime}})
|
||||||
|
{{/if}}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
|
||||||
|
export default class ExpenseListComponent extends Component {
|
||||||
|
|
||||||
|
get showDeleteButton () {
|
||||||
|
return !!this.args.deletable;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<ul class="expense-list">
|
||||||
|
{{#each @expenses as |expense|}}
|
||||||
|
<li class="expense-item">
|
||||||
|
<h4>
|
||||||
|
<span class="date">{{fmt-date-localized expense.date}}:</span>
|
||||||
|
<span class="title">{{expense.title}}</span>
|
||||||
|
</h4>
|
||||||
|
<div class="amount">
|
||||||
|
{{fmt-fiat-currency expense.amount expense.currency}}
|
||||||
|
</div>
|
||||||
|
<p class="description">
|
||||||
|
{{expense.description}}
|
||||||
|
</p>
|
||||||
|
<p class="tags">
|
||||||
|
{{#each expense.tags as |tag|}}
|
||||||
|
<button type="button" class="small yellow" role="none">
|
||||||
|
<IconTag />{{tag}}
|
||||||
|
</button>
|
||||||
|
{{/each}}
|
||||||
|
</p>
|
||||||
|
{{#if this.showDeleteButton}}
|
||||||
|
<div class="actions">
|
||||||
|
<button {{on "click" (fn @removeExpenseItem expense)}}
|
||||||
|
class="danger small" type="button">delete</button>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<nav id="main-menu">
|
||||||
|
<ul>
|
||||||
|
<li><LinkTo @route="dashboard">Dashboard</LinkTo></li>
|
||||||
|
<li><LinkTo @route="budget">Budget</LinkTo></li>
|
||||||
|
<li><LinkTo @route="about">About</LinkTo></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
import fmtDateLocalized from 'kredits-web/helpers/fmt-date-localized';
|
||||||
|
|
||||||
|
export default class ReimbursementItemComponent extends Component {
|
||||||
|
@service kredits;
|
||||||
|
@tracked showExpenseDetails = false;
|
||||||
|
|
||||||
|
constructor(owner, args) {
|
||||||
|
super(owner, args);
|
||||||
|
if (this.isUnconfirmed && !this.isVetoed) {
|
||||||
|
this.showExpenseDetails = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get ipfsGatewayUrl () {
|
||||||
|
return config.ipfs.gatewayUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConfirmed () {
|
||||||
|
return (this.args.reimbursement.confirmedAt - this.kredits.currentBlock) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUnconfirmed () {
|
||||||
|
return !this.isConfirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isVetoed () {
|
||||||
|
return this.args.reimbursement.vetoed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showVetoButton () {
|
||||||
|
return this.isUnconfirmed && this.kredits.currentUserIsCore;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showConfirmedIn () {
|
||||||
|
return !this.isVetoed &&
|
||||||
|
(this.showExpenseDetails || this.isUnconfirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
get expenses () {
|
||||||
|
return this.args.reimbursement.expenses;
|
||||||
|
}
|
||||||
|
|
||||||
|
get expensesDateRange () {
|
||||||
|
const dates = this.expenses.map(e => e.date).uniq().sort();
|
||||||
|
let out = fmtDateLocalized.compute(dates.firstObject)
|
||||||
|
if (dates.length > 1) {
|
||||||
|
out += ' - ' + fmtDateLocalized.compute(dates.lastObject)
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleExpenseDetails () {
|
||||||
|
this.showExpenseDetails = !this.showExpenseDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
veto (id) {
|
||||||
|
this.kredits.vetoReimbursement(id).then(transaction => {
|
||||||
|
console.debug('[controllers:budget] Veto submitted to chain: '+transaction.hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<li data-reimbursement-id={{@reimbursement.id}}
|
||||||
|
class="{{item-status @reimbursement}}"
|
||||||
|
role="button" {{on "click" this.toggleExpenseDetails}}>
|
||||||
|
<p class="meta">
|
||||||
|
<span class="recipient">
|
||||||
|
<UserAvatar @contributor={{@reimbursement.contributor}} />
|
||||||
|
</span>
|
||||||
|
<span class="title">
|
||||||
|
Expenses covered by {{@reimbursement.contributor.name}}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="token-amount">
|
||||||
|
<span class="amount">
|
||||||
|
{{sats-to-btc @reimbursement.amount}}</span> <span class="symbol">BTC</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{#if this.showExpenseDetails}}
|
||||||
|
<ExpenseList @expenses={{@reimbursement.expenses}} />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
<p>
|
||||||
|
{{#if this.showConfirmedIn}}
|
||||||
|
<ConfirmedIn @confirmedAtBlock={{@reimbursement.confirmedAt}} />
|
||||||
|
{{else}}
|
||||||
|
{{#unless this.isVetoed}}{{this.expensesDateRange}}{{/unless}}
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
<p class="actions">
|
||||||
|
<a href="{{this.ipfsGatewayUrl}}/{{@reimbursement.ipfsHash}}"
|
||||||
|
class="button small" target="_blank" rel="noopener noreferrer">
|
||||||
|
Inspect IPFS data
|
||||||
|
</a>
|
||||||
|
{{#if this.showVetoButton}}
|
||||||
|
<button {{on "click" (fn this.veto @reimbursement.id)}}
|
||||||
|
disabled={{@reimbursement.vetoed}}
|
||||||
|
class="button small danger" type="button">veto</button>
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { sort } from '@ember/object/computed';
|
import { sort } from '@ember/object/computed';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default class ReimbursementListComponent extends Component {
|
export default class ReimbursementListComponent extends Component {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
itemSorting = Object.freeze(['pendingStatus:asc', 'id:desc']);
|
itemSorting = Object.freeze(['pendingStatus:asc', 'id:desc']);
|
||||||
@sort('args.items', 'itemSorting') itemsSorted;
|
@sort('args.items', 'itemSorting') itemsSorted;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,5 @@
|
|||||||
<ul class="item-list spaced reimbursement-list {{if @loading 'loading'}}">
|
<ul class="item-list collapsible spaced reimbursement-list {{if @loading 'loading'}}">
|
||||||
{{#each this.itemsSorted as |reimbursement|}}
|
{{#each this.itemsSorted as |item|}}
|
||||||
<li data-reimbursement-id={{reimbursement.id}}
|
<ReimbursementItem @reimbursement={{item}} />
|
||||||
class="{{item-status reimbursement}}">
|
|
||||||
<p class="meta">
|
|
||||||
<span class="recipient"><UserAvatar @contributor={{reimbursement.contributor}} /></span>
|
|
||||||
<span class="title">Expenses covered by {{reimbursement.contributor.name}}</span>
|
|
||||||
</p>
|
|
||||||
<p class="token-amount">
|
|
||||||
<span class="amount">
|
|
||||||
{{sats-to-btc reimbursement.amount}}</span> <span class="symbol">BTC</span>
|
|
||||||
</p>
|
|
||||||
<ul class="expense-list">
|
|
||||||
{{#each reimbursement.expenses as |expense|}}
|
|
||||||
<li>
|
|
||||||
<div class="description" rowspan="2">
|
|
||||||
<h4>
|
|
||||||
<span class="date">{{expense.date}}:</span>
|
|
||||||
<span class="title">{{expense.title}}</span>
|
|
||||||
</h4>
|
|
||||||
<p class="description">{{expense.description}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="amount">
|
|
||||||
{{fmt-fiat-currency expense.amount expense.currency}}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -1,45 +1,37 @@
|
|||||||
import Component from '@ember/component';
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { computed } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { isPresent } from '@ember/utils';
|
import { isPresent } from '@ember/utils';
|
||||||
|
|
||||||
export default Component.extend({
|
export default class TopbarAccountPanelComponent extends Component {
|
||||||
|
@service router;
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
tagName: '',
|
@tracked setupInProgress = false;
|
||||||
|
|
||||||
kredits: service(),
|
get userHasWallet () {
|
||||||
router: service(),
|
|
||||||
|
|
||||||
setupInProgress: false,
|
|
||||||
|
|
||||||
userHasEthereumWallet: computed(function() {
|
|
||||||
return isPresent(window.ethereum);
|
return isPresent(window.ethereum);
|
||||||
}),
|
}
|
||||||
|
|
||||||
showConnectButton: computed('userHasEthereumWallet',
|
get walletConnected () {
|
||||||
'kredits.hasAccounts', function() {
|
return this.userHasWallet && this.kredits.hasAccounts;
|
||||||
return this.userHasEthereumWallet &&
|
}
|
||||||
!this.kredits.hasAccounts;
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
get walletDisconnected () {
|
||||||
|
return this.userHasWallet && !this.kredits.hasAccounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
signup () {
|
signup () {
|
||||||
this.router.transitionTo('signup');
|
this.router.transitionTo('signup');
|
||||||
},
|
|
||||||
|
|
||||||
async connectAccount() {
|
|
||||||
try {
|
|
||||||
await window.ethereum.enable();
|
|
||||||
this.set('setupInProgress', true);
|
|
||||||
await this.kredits.setup();
|
|
||||||
this.set('setupInProgress', false);
|
|
||||||
} catch (error) {
|
|
||||||
this.set('setupInProgress', false);
|
|
||||||
console.log('Opening Ethereum wallet failed:', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async connectWallet () {
|
||||||
|
this.setupInProgress = true;
|
||||||
|
await this.kredits.connectWallet();
|
||||||
|
this.setupInProgress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -2,16 +2,15 @@
|
|||||||
{{#if this.setupInProgress}}
|
{{#if this.setupInProgress}}
|
||||||
Connecting account...
|
Connecting account...
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if (and this.kredits.hasAccounts this.kredits.currentUser)}}
|
{{#if (and this.walletConnected this.kredits.currentUser)}}
|
||||||
{{this.kredits.currentUser.name}}
|
{{this.kredits.currentUser.name}}
|
||||||
{{#if this.kredits.currentUserIsCore}}
|
|
||||||
<span class="core-flag">(core)</span>
|
|
||||||
{{/if}}
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="anonymous">Anonymous</span>
|
<span class="anonymous">Anonymous</span>
|
||||||
<button onclick={{action "signup"}} class="small" type="button">Sign up</button>
|
<button onclick={{action "signup"}} id="signup"
|
||||||
{{#if this.showConnectButton}}
|
class="small" type="button">Sign up</button>
|
||||||
<button onclick={{action "connectAccount"}} class="small green" type="button">Connect account</button>
|
{{#if this.walletDisconnected}}
|
||||||
|
<button onclick={{action "connectWallet"}} id="connect"
|
||||||
|
class="small green" type="button">Connect wallet</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default class ApplicationController extends Controller {
|
||||||
kredits: service(),
|
@service kredits;
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { computed } from '@ember/object';
|
import { computed } from '@ember/object';
|
||||||
import { alias, not, sort } from '@ember/object/computed';
|
import { alias, gt, not, sort } from '@ember/object/computed';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
@@ -33,9 +33,7 @@ export default Controller.extend({
|
|||||||
showQuickFilterUnconfirmed: false,
|
showQuickFilterUnconfirmed: false,
|
||||||
showQuickFilterConfirmed: false,
|
showQuickFilterConfirmed: false,
|
||||||
|
|
||||||
showIntroText: computed('kredits.{hasAccounts,currentUser}', function(){
|
showFullContributionSync: gt('kredits.missingHistoricContributionsCount', 0),
|
||||||
return (!this.kredits.hasAccounts || !this.kredits.currentUser);
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
import { isAddress } from 'web3-utils';
|
||||||
|
|
||||||
|
export default class AccountController extends Controller {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
@tracked accountAddress = null;
|
||||||
|
|
||||||
|
get isValidEthAccount () {
|
||||||
|
return isAddress(this.accountAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
get signupButtonDisabled () {
|
||||||
|
return !this.isValidEthAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
completeSignup () {
|
||||||
|
const payload = {
|
||||||
|
accessToken: this.kredits.githubAccessToken,
|
||||||
|
account: this.accountAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(config.githubSignupUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
alert('Creating profile failed. We have been notified about this error and will take a look soon. Sorry!');
|
||||||
|
console.warn('Creating contributor profile failed:',
|
||||||
|
JSON.parse(data.error.body).error.message)
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
console.log('[signup/account] Created contributor:', data);
|
||||||
|
|
||||||
|
this.kredits.githubAccessToken = null;
|
||||||
|
this.accountAddress = null;
|
||||||
|
|
||||||
|
this.transitionToRoute('signup.complete');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import Controller from '@ember/controller';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
import { alias, not } from '@ember/object/computed';
|
|
||||||
import { isAddress } from 'web3-utils';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import config from 'kredits-web/config/environment';
|
|
||||||
|
|
||||||
export default Controller.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
ethAddress: null,
|
|
||||||
githubAccessToken: alias('kredits.githubAccessToken'),
|
|
||||||
|
|
||||||
isValidEthAccount: computed('ethAddress', function() {
|
|
||||||
return isAddress(this.ethAddress);
|
|
||||||
}),
|
|
||||||
|
|
||||||
signupButtonDisabled: not('isValidEthAccount'),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
completeSignup () {
|
|
||||||
const payload = {
|
|
||||||
accessToken: this.githubAccessToken,
|
|
||||||
account: this.ethAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(config.githubSignupUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
console.log('Created contributor:', data);
|
|
||||||
|
|
||||||
this.setProperties({
|
|
||||||
githubAccessToken: null,
|
|
||||||
ethAddress: null
|
|
||||||
});
|
|
||||||
|
|
||||||
this.transitionToRoute('signup.complete');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
|
import { action } from '@ember/object';
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default class IndexController extends Controller {
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
|
@action
|
||||||
connectGithub () {
|
connectGithub () {
|
||||||
window.location = config.githubConnectUrl;
|
window.location = config.githubConnectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
import getLocale from 'kredits-web/utils/get-locale';
|
||||||
|
|
||||||
|
export default helper(function fmtDateLocalized(dateStr) {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const locale = getLocale();
|
||||||
|
return new Intl.DateTimeFormat(locale).format(date);
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default helper(function fmtNumber(number) {
|
||||||
|
const lang = navigator.language || navigator.userLanguage;
|
||||||
|
return number.toLocaleString(lang);
|
||||||
|
});
|
||||||
+2
-4
@@ -11,7 +11,6 @@ Router.map(function() {
|
|||||||
this.route('contributors', function() {
|
this.route('contributors', function() {
|
||||||
this.route('show', { path: ':id' });
|
this.route('show', { path: ':id' });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('contributions', function() {
|
this.route('contributions', function() {
|
||||||
this.route('show', { path: ':id' });
|
this.route('show', { path: ':id' });
|
||||||
});
|
});
|
||||||
@@ -26,16 +25,15 @@ Router.map(function() {
|
|||||||
});
|
});
|
||||||
this.route('signup', function() {
|
this.route('signup', function() {
|
||||||
this.route('github');
|
this.route('github');
|
||||||
this.route('eth-account');
|
this.route('account');
|
||||||
this.route('complete');
|
this.route('complete');
|
||||||
});
|
});
|
||||||
this.route('budget', function() {
|
this.route('budget', function() {
|
||||||
this.route('expenses');
|
this.route('expenses');
|
||||||
|
|
||||||
this.route('reimbursements', function() {});
|
this.route('reimbursements', function() {});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('reimbursements', function() {
|
this.route('reimbursements', function() {
|
||||||
this.route('new');
|
this.route('new');
|
||||||
});
|
});
|
||||||
|
this.route('about');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class AboutRoute extends Route {
|
||||||
|
}
|
||||||
@@ -11,8 +11,11 @@ export default class DashboardRoute extends Route {
|
|||||||
schedule('afterRender', this.kredits.syncContributions,
|
schedule('afterRender', this.kredits.syncContributions,
|
||||||
this.kredits.syncContributions.perform);
|
this.kredits.syncContributions.perform);
|
||||||
}
|
}
|
||||||
schedule('afterRender', this.kredits.fetchMissingContributions,
|
// TODO fetch automatically under a certain threshold
|
||||||
this.kredits.fetchMissingContributions.perform);
|
// The browser might delete cached data and we don't need manual re-syncs
|
||||||
|
// depending on how little is missing
|
||||||
|
// schedule('afterRender', this.kredits.fetchMissingContributions,
|
||||||
|
// this.kredits.fetchMissingContributions.perform);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ export default Route.extend({
|
|||||||
contributors: alias('kredits.contributors'),
|
contributors: alias('kredits.contributors'),
|
||||||
|
|
||||||
model (params) {
|
model (params) {
|
||||||
return this.contributors.findBy('id', params.id);
|
const contributor = this.contributors.findBy('id', params.id);
|
||||||
|
|
||||||
|
if (contributor) {
|
||||||
|
return contributor;
|
||||||
|
} else {
|
||||||
|
return this.kredits.fetchContributor(params.id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController (controller, model) {
|
setupController (controller, model) {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { isEmpty } from '@ember/utils';
|
||||||
|
|
||||||
|
export default class SignupAccountRoute extends Route {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
async setupController (controller) {
|
||||||
|
if (!window.ethereum) return;
|
||||||
|
|
||||||
|
if (this.kredits.hasAccounts) {
|
||||||
|
controller.accountAddress = this.kredits.currentUserAccounts.firstObject;
|
||||||
|
} else {
|
||||||
|
return this.kredits.connectWallet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect () {
|
||||||
|
if (isEmpty(this.kredits.githubAccessToken)) {
|
||||||
|
this.transitionTo('signup.index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import { isEmpty } from '@ember/utils';
|
|
||||||
|
|
||||||
export default Route.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
redirect () {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
if (isEmpty(this.kredits.githubAccessToken)) {
|
|
||||||
this.transitionTo('signup.index');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -22,7 +22,7 @@ export default Route.extend({
|
|||||||
|
|
||||||
this.kredits.set('githubAccessToken', accessToken);
|
this.kredits.set('githubAccessToken', accessToken);
|
||||||
|
|
||||||
this.transitionTo('signup.eth-account');
|
this.transitionTo('signup.account');
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import config from 'kredits-web/config/environment';
|
|||||||
|
|
||||||
function createStore(name) {
|
function createStore(name) {
|
||||||
let networkName;
|
let networkName;
|
||||||
if (config.web3RequiredNetworkName) {
|
if (config.web3NetworkName) {
|
||||||
networkName = config.web3RequiredNetworkName.toLocaleLowerCase().replace(' ', '-');
|
networkName = config.web3NetworkName.toLocaleLowerCase().replace(' ', '-');
|
||||||
} else {
|
} else {
|
||||||
networkName = 'custom';
|
networkName = 'custom';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Service, { inject as service } from '@ember/service';
|
|
||||||
import { tracked } from '@glimmer/tracking';
|
|
||||||
import { A } from '@ember/array';
|
import { A } from '@ember/array';
|
||||||
import { task } from 'ember-concurrency-decorators';
|
import { task } from 'ember-concurrency-decorators';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import Service, { inject as service } from '@ember/service';
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
export default class CommunityFundsService extends Service {
|
export default class CommunityFundsService extends Service {
|
||||||
@@ -12,9 +12,20 @@ export default class CommunityFundsService extends Service {
|
|||||||
|
|
||||||
@task
|
@task
|
||||||
*fetchBalances () {
|
*fetchBalances () {
|
||||||
yield fetch(config.btcBalanceAPI).then(res => res.json())
|
const promises = [];
|
||||||
.then(res => {
|
const balances = config.communityFundsAPI.balances;
|
||||||
return this.processBalances(res);
|
|
||||||
|
for (const item of Object.keys(balances)) {
|
||||||
|
const c = balances[item];
|
||||||
|
promises.push(
|
||||||
|
this.fetchBalance(c.url)
|
||||||
|
.then(res => { return this.processBalance(res, c) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
yield Promise.all(promises)
|
||||||
|
.then(() => {
|
||||||
|
this.balancesLoaded = true;
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.log(`[community-funds] Fetching balances failed:`);
|
console.log(`[community-funds] Fetching balances failed:`);
|
||||||
@@ -22,18 +33,21 @@ export default class CommunityFundsService extends Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async processBalances (res) {
|
async fetchBalance(url) {
|
||||||
|
return fetch(url).then(res => res.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async processBalance (res, config) {
|
||||||
await this.exchangeRates.fetchRates();
|
await this.exchangeRates.fetchRates();
|
||||||
|
|
||||||
// Format and round the approximate USD value
|
// Format and round the approximate USD value
|
||||||
const lang = navigator.language || navigator.userLanguage;
|
const lang = navigator.language || navigator.userLanguage;
|
||||||
const balanceUSD = res.confirmed_balance * this.exchangeRates.btcusd;
|
const balanceUSD = (res.confirmed_balance / 100000000) * this.exchangeRates.btcusd;
|
||||||
res.balanceUSD = Math.round(balanceUSD).toLocaleString(lang);
|
res.balanceUSD = Math.round(balanceUSD).toLocaleString(lang);
|
||||||
|
|
||||||
this.balances.pushObject({
|
this.balances.pushObject({
|
||||||
...res,
|
...res,
|
||||||
...{ token: { name: 'BTC', symbol: 'BTC'} }
|
...{ token: { icon: `/img/${config.icon}`, symbol: config.symbol, description: config.description } }
|
||||||
});
|
});
|
||||||
|
|
||||||
this.balancesLoaded = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,13 @@ export default class ExchangeRatesService extends Service {
|
|||||||
@tracked btceur = 0;
|
@tracked btceur = 0;
|
||||||
@tracked btcusd = 0;
|
@tracked btcusd = 0;
|
||||||
|
|
||||||
|
get exchangeRatesLoaded () {
|
||||||
|
return (this.btceur !== 0) && (this.btcusd !== 0);
|
||||||
|
}
|
||||||
|
|
||||||
async fetchRates (source='bitstamp') {
|
async fetchRates (source='bitstamp') {
|
||||||
|
if (this.exchangeRatesLoaded) return;
|
||||||
|
|
||||||
switch(source) {
|
switch(source) {
|
||||||
case 'bitstamp':
|
case 'bitstamp':
|
||||||
this.btceur = await fetchFromBitstamp('btceur');
|
this.btceur = await fetchFromBitstamp('btceur');
|
||||||
|
|||||||
+94
-44
@@ -5,7 +5,7 @@ import Service from '@ember/service';
|
|||||||
import EmberObject from '@ember/object';
|
import EmberObject from '@ember/object';
|
||||||
import { computed } from '@ember/object';
|
import { computed } from '@ember/object';
|
||||||
import { alias, filterBy, notEmpty, sort } from '@ember/object/computed';
|
import { alias, filterBy, notEmpty, sort } from '@ember/object/computed';
|
||||||
import { isEmpty, isPresent } from '@ember/utils';
|
import { isEmpty } from '@ember/utils';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
import { task, taskGroup } from 'ember-concurrency';
|
import { task, taskGroup } from 'ember-concurrency';
|
||||||
@@ -15,6 +15,7 @@ import processContributorData from 'kredits-web/utils/process-contributor-data';
|
|||||||
import processContributionData from 'kredits-web/utils/process-contribution-data';
|
import processContributionData from 'kredits-web/utils/process-contribution-data';
|
||||||
import processReimbursementData from 'kredits-web/utils/process-reimbursement-data';
|
import processReimbursementData from 'kredits-web/utils/process-reimbursement-data';
|
||||||
import formatKredits from 'kredits-web/utils/format-kredits';
|
import formatKredits from 'kredits-web/utils/format-kredits';
|
||||||
|
import switchNetwork from 'kredits-web/utils/switch-network';
|
||||||
|
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
import Contributor from 'kredits-web/models/contributor';
|
import Contributor from 'kredits-web/models/contributor';
|
||||||
@@ -28,7 +29,7 @@ export default Service.extend({
|
|||||||
browserCache: service(),
|
browserCache: service(),
|
||||||
|
|
||||||
currentBlock: null,
|
currentBlock: null,
|
||||||
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
|
currentUserAccounts: null, // default to not having an account. this is the when web3 is loaded.
|
||||||
currentUser: null,
|
currentUser: null,
|
||||||
contributors: null,
|
contributors: null,
|
||||||
contributions: null,
|
contributions: null,
|
||||||
@@ -48,11 +49,28 @@ export default Service.extend({
|
|||||||
contributionsNeedSync: false,
|
contributionsNeedSync: false,
|
||||||
reimbursementsNeedSync: false,
|
reimbursementsNeedSync: false,
|
||||||
|
|
||||||
|
missingHistoricContributionsCount: 0,
|
||||||
|
|
||||||
init () {
|
init () {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.set('contributors', []);
|
this.set('contributors', []);
|
||||||
this.set('contributions', []);
|
this.set('contributions', []);
|
||||||
this.set('reimbursements', []);
|
this.set('reimbursements', []);
|
||||||
|
|
||||||
|
if (window.ethereum) {
|
||||||
|
window.ethereum.on('chainChanged', this.handleUserChainChanged);
|
||||||
|
window.ethereum.on('accountsChanged', this.handleAccountsChanged);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUserChainChanged (chainId) {
|
||||||
|
console.log('User-provided chain ID changed to', chainId);
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleAccountsChanged (accounts) {
|
||||||
|
console.log('User-provided accounts changed to', accounts);
|
||||||
|
window.location.reload();
|
||||||
},
|
},
|
||||||
|
|
||||||
// This is called in the application route's beforeModel(). So it is
|
// This is called in the application route's beforeModel(). So it is
|
||||||
@@ -62,7 +80,7 @@ export default Service.extend({
|
|||||||
let ethProvider;
|
let ethProvider;
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
function instantiateWithoutAccount () {
|
function instantiateWithoutWallet () {
|
||||||
console.debug('[kredits] Creating new instance from npm module class');
|
console.debug('[kredits] Creating new instance from npm module class');
|
||||||
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
|
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
|
||||||
ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl);
|
ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl);
|
||||||
@@ -72,44 +90,50 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function instantiateWithAccount (web3Provider, context) {
|
async function instantiateWithWallet (web3Provider, context) {
|
||||||
console.debug('[kredits] Using user-provided Web3 instance, e.g. from Metamask');
|
console.debug('[kredits] Using user-provided Web3 instance, e.g. from Metamask');
|
||||||
ethProvider = new ethers.providers.Web3Provider(web3Provider);
|
ethProvider = new ethers.providers.Web3Provider(web3Provider);
|
||||||
|
|
||||||
const network = await ethProvider.getNetwork();
|
const network = await ethProvider.getNetwork();
|
||||||
if (isPresent(config.web3RequiredChainId) &&
|
const accounts = await ethProvider.listAccounts();
|
||||||
network.chainId !== config.web3RequiredChainId) {
|
const chainId = config.web3ChainId;
|
||||||
return instantiateWithoutAccount();
|
|
||||||
}
|
|
||||||
|
|
||||||
ethProvider.listAccounts().then(accounts => {
|
if (isEmpty(accounts)) return instantiateWithoutWallet();
|
||||||
|
|
||||||
|
if (network.chainId !== chainId) {
|
||||||
|
return switchNetwork();
|
||||||
|
} else {
|
||||||
context.set('currentUserAccounts', accounts);
|
context.set('currentUserAccounts', accounts);
|
||||||
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
|
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
|
||||||
resolve({
|
resolve({
|
||||||
ethProvider,
|
ethProvider,
|
||||||
ethSigner
|
ethSigner
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.ethereum) {
|
if (window.ethereum) {
|
||||||
if (window.ethereum.isConnected()) {
|
instantiateWithWallet(window.ethereum, this);
|
||||||
instantiateWithAccount(window.ethereum, this);
|
|
||||||
} else {
|
} else {
|
||||||
instantiateWithoutAccount();
|
instantiateWithoutWallet();
|
||||||
}
|
|
||||||
}
|
|
||||||
// Legacy dapp browsers...
|
|
||||||
else if (window.web3) {
|
|
||||||
instantiateWithAccount(window.web3.currentProvider, this);
|
|
||||||
}
|
|
||||||
// Non-dapp browsers...
|
|
||||||
else {
|
|
||||||
instantiateWithoutAccount();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async connectWallet () {
|
||||||
|
const provider = new ethers.providers.Web3Provider(window.ethereum);
|
||||||
|
const network = await provider.getNetwork();
|
||||||
|
const chainId = config.web3ChainId;
|
||||||
|
const chainIdHex = `0x${Number(chainId).toString(16)}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
if (network.chainId !== chainId) await switchNetwork(chainIdHex);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Connecting wallet failed:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async setup () {
|
async setup () {
|
||||||
const kredits = await this.getEthProvider().then(providerAndSigner => {
|
const kredits = await this.getEthProvider().then(providerAndSigner => {
|
||||||
return new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
|
return new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
|
||||||
@@ -118,9 +142,12 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await kredits.init();
|
await kredits.init();
|
||||||
|
|
||||||
this.set('kredits', kredits);
|
this.set('kredits', kredits);
|
||||||
this.set('currentBlock', await kredits.provider.getBlockNumber());
|
this.set('currentBlock', await this.kredits.provider.getBlockNumber());
|
||||||
|
this.kredits.provider.on('block', blockNumber => {
|
||||||
|
console.debug('[kredits] New block mined:', blockNumber);
|
||||||
|
this.set('currentBlock', blockNumber)
|
||||||
|
});
|
||||||
|
|
||||||
if (this.currentUserAccounts && this.currentUserAccounts.length > 0) {
|
if (this.currentUserAccounts && this.currentUserAccounts.length > 0) {
|
||||||
this.getCurrentUser.then(contributorData => {
|
this.getCurrentUser.then(contributorData => {
|
||||||
@@ -227,12 +254,20 @@ export default Service.extend({
|
|||||||
await this.loadObjectsFromCache('Contribution');
|
await this.loadObjectsFromCache('Contribution');
|
||||||
this.set('contributionsNeedSync', true);
|
this.set('contributionsNeedSync', true);
|
||||||
} else {
|
} else {
|
||||||
await this.fetchContributions({ page: { size: 30 } });
|
await this.fetchContributions({ page: { size: 40 } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.updateMissingHistoricContributionsCount();
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async updateMissingHistoricContributionsCount () {
|
||||||
|
const contributionsCount = await this.kredits.Contribution.count;
|
||||||
|
this.set('missingHistoricContributionsCount', contributionsCount - this.contributions.length);
|
||||||
|
console.debug(`Missing ${this.missingHistoricContributionsCount} historic contributions (out of ${contributionsCount} overall)`)
|
||||||
|
},
|
||||||
|
|
||||||
addContributor (attributes) {
|
addContributor (attributes) {
|
||||||
if (attributes.github_uid) {
|
if (attributes.github_uid) {
|
||||||
const uidInt = parseInt(attributes.github_uid);
|
const uidInt = parseInt(attributes.github_uid);
|
||||||
@@ -261,6 +296,12 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetchContributor (id) {
|
||||||
|
console.debug(`[kredits] Fetching contributor from the network`);
|
||||||
|
return this.kredits.Contributor.getById(id)
|
||||||
|
.then(data => this.loadContributorFromData(data))
|
||||||
|
},
|
||||||
|
|
||||||
fetchContributors () {
|
fetchContributors () {
|
||||||
console.debug(`[kredits] Fetching all contributors from the network`);
|
console.debug(`[kredits] Fetching all contributors from the network`);
|
||||||
return this.kredits.Contributor.all()
|
return this.kredits.Contributor.all()
|
||||||
@@ -368,11 +409,12 @@ export default Service.extend({
|
|||||||
syncContributions: task(function * () {
|
syncContributions: task(function * () {
|
||||||
yield this.fetchNewContributions.perform();
|
yield this.fetchNewContributions.perform();
|
||||||
yield this.syncUnconfirmedContributions.perform();
|
yield this.syncUnconfirmedContributions.perform();
|
||||||
|
yield this.updateMissingHistoricContributionsCount();
|
||||||
this.set('contributionsNeedSync', false);
|
this.set('contributionsNeedSync', false);
|
||||||
}).group('contributionTasks'),
|
}).group('contributionTasks'),
|
||||||
|
|
||||||
fetchNewContributions: task(function * () {
|
fetchNewContributions: task(function * () {
|
||||||
const count = yield this.kredits.Contribution.functions.contributionsCount();
|
const count = yield this.kredits.Contribution.count;
|
||||||
const lastKnownContributionId = Math.max.apply(null, this.contributions.mapBy('id'));
|
const lastKnownContributionId = Math.max.apply(null, this.contributions.mapBy('id'));
|
||||||
const toFetch = count - lastKnownContributionId;
|
const toFetch = count - lastKnownContributionId;
|
||||||
|
|
||||||
@@ -389,7 +431,7 @@ export default Service.extend({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
fetchMissingContributions: task(function * () {
|
fetchMissingContributions: task(function * () {
|
||||||
const count = yield this.kredits.Contribution.functions.contributionsCount();
|
const count = yield this.kredits.Contribution.count;
|
||||||
const allIds = [...Array(count+1).keys()];
|
const allIds = [...Array(count+1).keys()];
|
||||||
allIds.shift(); // remove first item, which is 0
|
allIds.shift(); // remove first item, which is 0
|
||||||
const loadedContributions = new Set(this.contributions.mapBy('id'));
|
const loadedContributions = new Set(this.contributions.mapBy('id'));
|
||||||
@@ -617,6 +659,18 @@ export default Service.extend({
|
|||||||
yield this.fetchMissingObjects.perform('Reimbursement');
|
yield this.fetchMissingObjects.perform('Reimbursement');
|
||||||
}).group('syncTaskGroup'),
|
}).group('syncTaskGroup'),
|
||||||
|
|
||||||
|
vetoReimbursement (id) {
|
||||||
|
console.debug('[kredits] veto against reimbursement', id);
|
||||||
|
const reimbursement = this.reimbursements.findBy('id', id);
|
||||||
|
|
||||||
|
return this.kredits.Reimbursement.functions.veto(id, { gasLimit: 300000 })
|
||||||
|
.then(data => {
|
||||||
|
console.debug('[kredits] veto response', data);
|
||||||
|
reimbursement.set('pendingTx', data);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
// Contract events
|
// Contract events
|
||||||
//
|
//
|
||||||
@@ -676,7 +730,7 @@ export default Service.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async handleContributionVetoed (contributionId) {
|
async handleContributionVetoed (contributionId) {
|
||||||
console.debug('[kredits] ContributionVetoed event received for ', contributionId);
|
console.debug('[kredits] ContributionVetoed event received for #', contributionId);
|
||||||
const c = this.contributions.findBy('id', contributionId);
|
const c = this.contributions.findBy('id', contributionId);
|
||||||
|
|
||||||
if (c) {
|
if (c) {
|
||||||
@@ -687,9 +741,6 @@ export default Service.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
|
||||||
// TODO test when reimbursement txs are successful
|
|
||||||
//
|
|
||||||
async handleReimbursementAdded (id, addedByAccount, amount) {
|
async handleReimbursementAdded (id, addedByAccount, amount) {
|
||||||
console.debug('[kredits] ReimbursementAdded event received', { id, addedByAccount, amount });
|
console.debug('[kredits] ReimbursementAdded event received', { id, addedByAccount, amount });
|
||||||
|
|
||||||
@@ -703,20 +754,19 @@ export default Service.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await this.kredits.Reimbursement.getById(id);
|
const data = await this.kredits.Reimbursement.getById(id);
|
||||||
this.loadReimbursementFromData(data);
|
const r = this.loadReimbursementFromData(data);
|
||||||
|
this.browserCache.reimbursements.setItem(r.id.toString(), r.serialize());
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
async handleReimbursementVetoed (id) {
|
||||||
// TODO test when reimbursement txs are successful and veto is implemented
|
console.debug(`[kredits] ReimbursementVetoed received for #${id}`);
|
||||||
//
|
const r = this.reimbursements.findBy('id', id);
|
||||||
handleReimbursementVetoed (id) {
|
console.debug('[kredits] reimbursement', r);
|
||||||
console.debug('[kredits] ReimbursementVetoed received for ', id);
|
|
||||||
const reimbursement = this.reimbursements.findBy('id', id);
|
|
||||||
console.debug('[kredits] reimbursement', this.reimbursement);
|
|
||||||
|
|
||||||
if (reimbursement) {
|
if (r) {
|
||||||
reimbursement.set('vetoed', true);
|
r.set('vetoed', true);
|
||||||
reimbursement.set('pendingTx', null);
|
r.set('pendingTx', null);
|
||||||
|
this.browserCache.reimbursements.setItem(r.id.toString(), r.serialize());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -730,5 +780,5 @@ export default Service.extend({
|
|||||||
this.contributors
|
this.contributors
|
||||||
.findBy('address', to)
|
.findBy('address', to)
|
||||||
.incrementProperty('balance', value);
|
.incrementProperty('balance', value);
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+32
-11
@@ -31,32 +31,53 @@ button, input[type=submit], .button {
|
|||||||
&.small {
|
&.small {
|
||||||
font-size: 0.86rem;
|
font-size: 0.86rem;
|
||||||
padding: 0.2rem 0.8rem;
|
padding: 0.2rem 0.8rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.danger:not(:disabled) {
|
&.danger:not(:disabled) {
|
||||||
color: $red;
|
color: $red;
|
||||||
background-color: rgba(40, 21, 21, 0.6);
|
background-color: rgba(40, 21, 21, 0.6);
|
||||||
border-color: rgba(40, 21, 21, 1);
|
border-color: rgba(40, 21, 21, 1);
|
||||||
|
&:hover { background-color: rgba(40, 21, 21, 0.8); }
|
||||||
&:hover {
|
&:focus, &:active, &.active { border-color: $red; }
|
||||||
background-color: rgba(40, 21, 21, 0.8);
|
|
||||||
}
|
|
||||||
&:focus, &:active, &.active {
|
|
||||||
border-color: $red;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.green:not(:disabled) {
|
&.green:not(:disabled) {
|
||||||
color: $green;
|
color: $green;
|
||||||
background-color: rgba(21, 40, 21, 0.6);
|
background-color: rgba(21, 40, 21, 0.6);
|
||||||
border-color: rgba(21, 40, 21, 1);
|
border-color: rgba(21, 40, 21, 1);
|
||||||
|
&:hover { background-color: rgba(21, 40, 21, 0.8); }
|
||||||
|
&:focus, &:active, &.active { border-color: $green; }
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&.pink:not(:disabled) {
|
||||||
background-color: rgba(21, 40, 21, 0.8);
|
color: $pink;
|
||||||
|
background-color: rgba(40, 21, 40, 0.6);
|
||||||
|
border-color: rgba(40, 21, 40, 1);
|
||||||
|
&:hover { background-color: rgba(40, 21, 40, 0.8); }
|
||||||
|
&:focus, &:active, &.active { border-color: $pink; }
|
||||||
}
|
}
|
||||||
&:focus, &:active, &.active {
|
|
||||||
border-color: $green;
|
&.purple:not(:disabled) {
|
||||||
|
color: $purple;
|
||||||
|
background-color: rgba(24, 21, 40, 0.6);
|
||||||
|
border-color: rgba(24, 21, 40, 1);
|
||||||
|
&:hover { background-color: rgba(24, 21, 40, 0.8); }
|
||||||
|
&:focus, &:active, &.active { border-color: $purple; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.yellow:not(:disabled) {
|
||||||
|
color: $yellow;
|
||||||
|
background-color: rgba(40, 40, 21, 0.6);
|
||||||
|
border-color: rgba(40, 40, 21, 1);
|
||||||
|
&:hover { background-color: rgba(40, 40, 21, 0.8); }
|
||||||
|
&:focus, &:active, &.active { border-color: $yellow; }
|
||||||
}
|
}
|
||||||
|
|
||||||
&.icon {
|
&.icon {
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ section#signup {
|
|||||||
p {
|
p {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
|
||||||
&.mg-bottom-md {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.label {
|
&.label {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ ul.item-list {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.collapsible {
|
||||||
|
> li {
|
||||||
|
border-left: 1px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-left: 1px solid $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.loading {
|
&.loading {
|
||||||
@include loading-border-top;
|
@include loading-border-top;
|
||||||
|
|
||||||
|
|||||||
+23
-35
@@ -1,3 +1,7 @@
|
|||||||
|
body {
|
||||||
|
margin-bottom: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
|
|
||||||
@@ -14,7 +18,7 @@ main {
|
|||||||
"contributions";
|
"contributions";
|
||||||
}
|
}
|
||||||
|
|
||||||
&#budget {
|
&#budget, &#about {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-row-gap: 2rem;
|
grid-row-gap: 2rem;
|
||||||
@@ -56,9 +60,9 @@ main {
|
|||||||
|
|
||||||
&.text-lg {
|
&.text-lg {
|
||||||
p {
|
p {
|
||||||
font-size: 1.2rem;
|
font-size: 1.35rem;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 2rem;
|
||||||
line-height: 1.5em;
|
line-height: 150%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,50 +71,29 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
&.mg-bottom-md {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.actions {
|
&.actions {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#intro {
|
|
||||||
padding: 2rem;
|
|
||||||
background-color: rgba(0,0,0,.2);
|
|
||||||
font-size: 1.6rem;
|
|
||||||
|
|
||||||
@include media-max(small) {
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
&.text {
|
||||||
h2 {
|
h2 {
|
||||||
font-size: inherit;
|
margin: 4rem 0 2rem 0;
|
||||||
margin-bottom: 1em;
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 1em;
|
font-size: 1.35rem;
|
||||||
line-height: 1.5em;
|
margin-bottom: 2rem;
|
||||||
font-size: 1.2rem;
|
line-height: 150%;
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover, &:active {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +114,7 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&#budget {
|
&#budget, &#about {
|
||||||
grid-column-gap: 3rem;
|
grid-column-gap: 3rem;
|
||||||
grid-template-columns: 2fr 4fr 2fr;
|
grid-template-columns: 2fr 4fr 2fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
@@ -177,4 +160,9 @@ main section {
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Remove after switch to Tailwind CSS
|
||||||
|
.mb-4 { margin-bottom: 1rem; }
|
||||||
|
.mb-8 { margin-bottom: 2rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ section {
|
|||||||
@import "components/contributor-profile";
|
@import "components/contributor-profile";
|
||||||
@import "components/expense-list";
|
@import "components/expense-list";
|
||||||
@import "components/external-account-link";
|
@import "components/external-account-link";
|
||||||
|
@import "components/main-navigation-menu";
|
||||||
@import "components/loading-spinner";
|
@import "components/loading-spinner";
|
||||||
@import "components/reimbursement-list";
|
@import "components/reimbursement-list";
|
||||||
@import "components/topbar";
|
@import "components/topbar";
|
||||||
|
|||||||
@@ -16,8 +16,12 @@ section#funds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
font-size: 1.2rem;
|
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 1.5rem;
|
||||||
|
max-width: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
@@ -35,8 +39,13 @@ section#funds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.fiat-amount {
|
&.fiat-amount {
|
||||||
|
font-size: 1.2rem;
|
||||||
color: rgba(255,255,255,0.8);
|
color: rgba(255,255,255,0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.unit {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,32 +17,30 @@ ul.expense-list {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
|
||||||
grid-row-start: 1;
|
|
||||||
grid-row-end: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount {
|
|
||||||
justify-self: end;
|
|
||||||
// font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
justify-self: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.amount {
|
||||||
line-height: 2rem;
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
&.description {
|
.description {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
grid-column-start: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
button {
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
nav#main-menu {
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: rgba(0,0,0,.2);
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
@include media-max(small) {
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&.active, &:hover {
|
||||||
|
color: $yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,11 @@ ul.reimbursement-list {
|
|||||||
grid-row-gap: 0.5rem;
|
grid-row-gap: 0.5rem;
|
||||||
padding-top: 1.6rem;
|
padding-top: 1.6rem;
|
||||||
|
|
||||||
|
&.vetoed {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.token-amount {
|
.token-amount {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
@@ -26,4 +31,21 @@ ul.reimbursement-list {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.meta {
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-column-end: 3;
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
border-top: 1px solid $item-border-color;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
p {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.6rem 0 1rem 0;
|
||||||
|
|
||||||
|
&.actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
h1 {
|
h1 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<main id="about">
|
||||||
|
<div id="aside">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<section class="text">
|
||||||
|
<h2>What is this?</h2>
|
||||||
|
<p>
|
||||||
|
You have found the contribution dashboard and budget management app of the
|
||||||
|
<a href="https://kosmos.org" target="_blank" rel="noreferrer noopener">Kosmos</a>
|
||||||
|
open-source co-operative.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We use this app to track what people contribute to all parts of our
|
||||||
|
projects (not just code), as well as for managing our community's budget.
|
||||||
|
</p>
|
||||||
|
<h2>Why?</h2>
|
||||||
|
<p>
|
||||||
|
We are trying out a new form of co-operative, native to the Internet,
|
||||||
|
creating and sharing digital resources the same way traditional
|
||||||
|
co-operatives share physical ones.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
By knowing roughly how much people contribute, we are able to reward
|
||||||
|
co-operative members who contribute their time with money contributed
|
||||||
|
by the ones who don't.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In addition to open-source grant payouts, all contributors can also propose
|
||||||
|
reimbursements for specific expenses they covered on behalf of the
|
||||||
|
community.
|
||||||
|
</p>
|
||||||
|
<h2>How?</h2>
|
||||||
|
<p>
|
||||||
|
Instead of notaries, lawyers, courts, or banks, we record data and manage
|
||||||
|
certain decisions on a decentralized ledger called
|
||||||
|
<a href="https://rootstock.io/" target="_blank" rel="noreferrer noopener">Rootstock</a>,
|
||||||
|
which is cryptographically tied to the bitcoin timechain.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
By doing this, we can ensure community control over the budget, as well as
|
||||||
|
full transparency and verifiability of everything that happens. It also
|
||||||
|
allows us to quickly, cheaply, and reliably send our open-source grants to
|
||||||
|
anyone who's contributing, no matter where they are on this beautiful
|
||||||
|
planet.
|
||||||
|
</p>
|
||||||
|
<h2>Kredits</h2>
|
||||||
|
<p>
|
||||||
|
All contributions are rewarded with so-called kredits. They are both
|
||||||
|
credits in the traditional sense of public attribution, as well as a
|
||||||
|
measure of how much and how regularly someone has added value to the
|
||||||
|
community and its products and services.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Kredits are considered for example for grant payouts, as well as
|
||||||
|
permissions for certain actions like vetos or votes. They may also be used
|
||||||
|
to access <a href="https://kosmos.org/services/">hosted services</a> for free,
|
||||||
|
or to unlock additional features on otherwise free services.
|
||||||
|
</p>
|
||||||
|
<h2>Getting started</h2>
|
||||||
|
<p>
|
||||||
|
We'd be delighted to welcome you as a new contributor!
|
||||||
|
If you'd like to start collecting kredits for your contributions, you can
|
||||||
|
<LinkTo @route="signup">create a contributor profile</LinkTo> now.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Next, you could learn more about
|
||||||
|
<a href="https://community.kosmos.org/t/how-kredits-for-contributions-are-proposed-and-confirmed/176">how kredits are proposed and issued</a>.
|
||||||
|
If you want to dive deeper into how this all works, head over to the
|
||||||
|
<a href="https://wiki.kosmos.org/Kredits">Kredits documentation</a>
|
||||||
|
page on our (soon to be phased out) wiki.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="empty">
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<header id="topbar">
|
<header id="topbar">
|
||||||
<h1><LinkTo @route="dashboard">Kosmos Kredits</LinkTo></h1>
|
<h1><LinkTo @route="dashboard">Kredits</LinkTo></h1>
|
||||||
<TopbarAccountPanel />
|
<TopbarAccountPanel />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<MainNavigationMenu />
|
||||||
|
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
{{#if this.reimbursementsConfirmed}}
|
{{#if this.reimbursementsConfirmed}}
|
||||||
<section id="expenses-confirmed">
|
<section id="expenses-confirmed">
|
||||||
<header class="with-nav">
|
<header class="with-nav">
|
||||||
<h2>Confirmed Expenses</h2>
|
<h2>Confirmed Reimbursements</h2>
|
||||||
<nav>
|
<nav>
|
||||||
<LinkTo @route="reimbursements.new" @title="Submit a reimbursement" class="button small green">add</LinkTo>
|
<LinkTo @route="reimbursements.new" @title="Submit a reimbursement" class="button small green">add</LinkTo>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tag"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>
|
||||||
|
After Width: | Height: | Size: 355 B |
+30
-16
@@ -1,19 +1,3 @@
|
|||||||
{{#if this.showIntroText}}
|
|
||||||
<div id="intro" class={{if this.showDetailsPane "with-details"}}>
|
|
||||||
<h2>
|
|
||||||
Welcome to the contribution dashboard of the
|
|
||||||
<a href="https://kosmos.org" target="_blank" rel="noreferrer noopener">Kosmos</a> project!
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
If you want to learn more about what the numbers mean and how this works,
|
|
||||||
check out the
|
|
||||||
<a href="https://wiki.kosmos.org/Kredits" target="_blank" rel="noreferrer noopener">Kredits documentation</a>.
|
|
||||||
If you want to start earning kredits for your contributions,
|
|
||||||
<LinkTo @route="signup">create a contributor profile</LinkTo>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<main id="dashboard" class={{if this.showDetailsPane "with-details"}}>
|
<main id="dashboard" class={{if this.showDetailsPane "with-details"}}>
|
||||||
|
|
||||||
<div id="stats">
|
<div id="stats">
|
||||||
@@ -45,6 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{{#if this.contributionsConfirmed}}
|
||||||
<section id="contributions-by-type">
|
<section id="contributions-by-type">
|
||||||
<header>
|
<header>
|
||||||
<h2>Contributions by type</h2>
|
<h2>Contributions by type</h2>
|
||||||
@@ -53,6 +38,7 @@
|
|||||||
<ChartContributionsByType @contributions={{this.contributions}} />
|
<ChartContributionsByType @contributions={{this.contributions}} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="contributions">
|
<div id="contributions">
|
||||||
@@ -87,6 +73,7 @@
|
|||||||
</section>
|
</section>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.contributionsConfirmed}}
|
||||||
<section id="contributions-confirmed">
|
<section id="contributions-confirmed">
|
||||||
<header class="with-nav">
|
<header class="with-nav">
|
||||||
<h2>Confirmed Contributions</h2>
|
<h2>Confirmed Contributions</h2>
|
||||||
@@ -105,6 +92,33 @@
|
|||||||
@showQuickFilter={{this.showQuickFilterConfirmed}} />
|
@showQuickFilter={{this.showQuickFilterConfirmed}} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.showFullContributionSync}}
|
||||||
|
<section id="sync-all-contributions">
|
||||||
|
{{#if this.kredits.fetchMissingContributions.isIdle}}
|
||||||
|
<p class="mb-4">
|
||||||
|
There are
|
||||||
|
<strong>{{this.kredits.missingHistoricContributionsCount}}</strong>
|
||||||
|
earlier contributions, which are not currently loaded/displayed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can fetch all historic data in one go, and have it stored locally in
|
||||||
|
your browser:
|
||||||
|
<button type="button" {{on "click" (perform this.kredits.fetchMissingContributions)}} class="small">
|
||||||
|
fetch all data
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="mb-4">
|
||||||
|
Syncing data. Please be patient...
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
(You can leave this website anytime and sync missing data when you come back.)
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="details">
|
<div id="details">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<p>
|
<p>
|
||||||
<a href="https://rinkeby.etherscan.io/address/{{this.model.account}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect Ethereum transactions</a>
|
<a href="https://explorer.testnet.rsk.co/address/{{this.model.account}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect Rootstock transactions</a>
|
||||||
{{#if this.model.ipfsHash}}
|
{{#if this.model.ipfsHash}}
|
||||||
<a href="{{this.ipfsGatewayUrl}}/{{this.model.ipfsHash}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect IPFS profile</a>
|
<a href="{{this.ipfsGatewayUrl}}/{{this.model.ipfsHash}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect IPFS profile</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<header>
|
||||||
|
<h2>Complete your contributor profile</h2>
|
||||||
|
</header>
|
||||||
|
<div class="content text-lg">
|
||||||
|
<p class="mb-8">
|
||||||
|
Kredits allow you to to earn rewards for your contributions, in the form of
|
||||||
|
dynamic open-source grants. As a regular contributor, you can also take
|
||||||
|
part in the community's project governance and finances.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In order to interact with the system you will need a
|
||||||
|
<a href="https://rootstock.io" target="_blank" rel="noopener noreferrer">Rootstock</a>
|
||||||
|
wallet/account.
|
||||||
|
</p>
|
||||||
|
<form>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
Rootstock address:<br>
|
||||||
|
<Input @type="text" @value={{this.accountAddress}} placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4" class={{if this.isValidEthAccount "valid" ""}} />
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<p class="actions">
|
||||||
|
<button {{on "click" (fn this.completeSignup)}}
|
||||||
|
disabled={{this.signupButtonDisabled}}
|
||||||
|
type="button">
|
||||||
|
Complete my profile
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Why not say hi to your fellow contributors
|
Why not say hi to your fellow contributors
|
||||||
<a href="https://wiki.kosmos.org/Main_Page#Community_.2F_Getting_in_touch_.2F_Getting_involved">in one of our chat rooms</a>?.
|
<a href="https://wiki.kosmos.org/Main_Page#Community_.2F_Getting_in_touch_.2F_Getting_involved"
|
||||||
|
target="_blank" rel="noopener noreferrer">in one of our chat rooms</a>?.
|
||||||
</p>
|
</p>
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
<LinkTo @route="dashboard" class="button small">Return to dashboard</LinkTo>
|
<LinkTo @route="dashboard" class="button small">Return to dashboard</LinkTo>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
<header>
|
|
||||||
<h2>Complete your contributor profile</h2>
|
|
||||||
</header>
|
|
||||||
<div class="content text-lg">
|
|
||||||
<p class="mg-bottom-md">
|
|
||||||
Kredits allow you to take part in project governance, and to earn rewards for
|
|
||||||
your contributions. For both, you will need an Ethereum wallet/account.
|
|
||||||
</p>
|
|
||||||
<form>
|
|
||||||
<p>
|
|
||||||
<label>
|
|
||||||
Ethereum account:<br>
|
|
||||||
<Input @type="text" @value={{this.ethAddress}} placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4" class={{if this.isValidEthAccount "valid" ""}} />
|
|
||||||
</label>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
<p class="actions">
|
|
||||||
<button disabled={{this.signupButtonDisabled}}
|
|
||||||
onclick={{action "completeSignup"}}
|
|
||||||
type="button">
|
|
||||||
Complete my profile
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default function() {
|
||||||
|
return (navigator.languages && navigator.languages.length) ?
|
||||||
|
navigator.languages[0] :
|
||||||
|
navigator.language;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default function (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reader.onload = event => resolve(event.target.result)
|
||||||
|
reader.onerror = error => reject(error)
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
export default async function () {
|
||||||
|
const networkName = config.web3NetworkName;
|
||||||
|
const chainId = config.web3ChainId;
|
||||||
|
const chainIdHex = `0x${Number(chainId).toString(16)}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_switchEthereumChain',
|
||||||
|
params: [{ chainId: chainIdHex }]
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// This error code indicates that the chain has not been added to MetaMask
|
||||||
|
if (err.code === 4902) {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_addEthereumChain',
|
||||||
|
params: [{
|
||||||
|
chainId: chainIdHex,
|
||||||
|
chainName: networkName,
|
||||||
|
rpcUrls: [ config.web3ProviderUrl ],
|
||||||
|
nativeCurrency: { name: 'tRBTC', symbol: 'tRBTC', decimals: 18 }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to switch chains:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-6
@@ -26,11 +26,11 @@ module.exports = function(environment) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
web3ProviderUrl: 'https://rsk-testnet.kosmos.org',
|
web3ProviderUrl: 'https://rsk-testnet.kosmos.org',
|
||||||
web3RequiredChainId: 31,
|
web3ChainId: 31,
|
||||||
web3RequiredNetworkName: 'RSK Testnet',
|
web3NetworkName: 'RSK Testnet',
|
||||||
|
|
||||||
githubConnectUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/connect/github',
|
githubConnectUrl: 'https://hal8000.kosmos.chat/kredits/signup/connect/github',
|
||||||
githubSignupUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/github',
|
githubSignupUrl: 'https://hal8000.kosmos.chat/kredits/signup/github',
|
||||||
|
|
||||||
ipfs: {
|
ipfs: {
|
||||||
host: 'ipfs.kosmos.org',
|
host: 'ipfs.kosmos.org',
|
||||||
@@ -45,7 +45,22 @@ module.exports = function(environment) {
|
|||||||
'BTC': '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'
|
'BTC': '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'
|
||||||
},
|
},
|
||||||
|
|
||||||
btcBalanceAPI: 'https://api.kosmos.org/kredits/onchain_btc_balance',
|
communityFundsAPI: {
|
||||||
|
balances: {
|
||||||
|
onchain: {
|
||||||
|
icon: 'icon-btc.png',
|
||||||
|
symbol: 'BTC',
|
||||||
|
description: 'BTC on chain',
|
||||||
|
url: 'https://api.kosmos.org/btcpay/onchain_btc_balance'
|
||||||
|
},
|
||||||
|
lightning: {
|
||||||
|
icon: 'icon-btc-lightning.png',
|
||||||
|
symbol: 'BTC',
|
||||||
|
description: 'BTC on Lightning Network',
|
||||||
|
url: 'https://api.kosmos.org/btcpay/lightning_btc_balance'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
corsProxy: 'https://cors.5apps.com/?uri='
|
corsProxy: 'https://cors.5apps.com/?uri='
|
||||||
};
|
};
|
||||||
@@ -66,6 +81,10 @@ module.exports = function(environment) {
|
|||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
gatewayUrl: 'http://localhost:8080/ipfs'
|
gatewayUrl: 'http://localhost:8080/ipfs'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Uncomment if you want to use local akkounts
|
||||||
|
// ENV.communityFundsAPI.balances.onchain.url = 'http://localhost:3000/api/btcpay/onchain_btc_balance';
|
||||||
|
// ENV.communityFundsAPI.balances.lightning.url = 'http://localhost:3000/api/btcpay/lightning_btc_balance';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (environment === 'test') {
|
if (environment === 'test') {
|
||||||
@@ -82,7 +101,12 @@ module.exports = function(environment) {
|
|||||||
|
|
||||||
if (process.env.WEB3_PROVIDER_URL) {
|
if (process.env.WEB3_PROVIDER_URL) {
|
||||||
ENV.web3ProviderUrl = process.env.WEB3_PROVIDER_URL;
|
ENV.web3ProviderUrl = process.env.WEB3_PROVIDER_URL;
|
||||||
ENV.web3RequiredChainId = null;
|
}
|
||||||
|
if (process.env.WEB3_CHAIN_ID) {
|
||||||
|
ENV.web3ChainId = parseInt(process.env.WEB3_CHAIN_ID);
|
||||||
|
}
|
||||||
|
if (process.env.WEB3_NETWORK_NAME) {
|
||||||
|
ENV.web3NetworkName = process.env.WEB3_NETWORK_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ENV;
|
return ENV;
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ module.exports = function(defaults) {
|
|||||||
path: "empty" // needed for kosmos-schemas dependency
|
path: "empty" // needed for kosmos-schemas dependency
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
fingerprint: {
|
||||||
|
exclude: [ 'img/icon-btc' ]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Generated
+9
-9
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "kredits-web",
|
"name": "kredits-web",
|
||||||
"version": "2.0.0-beta.1",
|
"version": "2.2.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "kredits-web",
|
"name": "kredits-web",
|
||||||
"version": "2.0.0-beta.1",
|
"version": "2.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.19.1",
|
"@babel/eslint-parser": "^7.19.1",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"@ember/optional-features": "^1.3.0",
|
"@ember/optional-features": "^1.3.0",
|
||||||
"@glimmer/component": "^1.0.0",
|
"@glimmer/component": "^1.0.0",
|
||||||
"@glimmer/tracking": "^1.0.0",
|
"@glimmer/tracking": "^1.0.0",
|
||||||
"@kredits/contracts": "^7.0.1",
|
"@kredits/contracts": "^7.3.0",
|
||||||
"babel-preset-es2015": "^6.22.0",
|
"babel-preset-es2015": "^6.22.0",
|
||||||
"broccoli-asset-rev": "^3.0.0",
|
"broccoli-asset-rev": "^3.0.0",
|
||||||
"ember-auto-import": "^1.12.1",
|
"ember-auto-import": "^1.12.1",
|
||||||
@@ -3067,9 +3067,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@kredits/contracts": {
|
"node_modules/@kredits/contracts": {
|
||||||
"version": "7.0.1",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.3.0.tgz",
|
||||||
"integrity": "sha512-4IhNQoPc1BCT63NKyDOmV+ryb82FKNbIqfDyTdant6WTI9lfkIAnI8Et+ynQmqxx3Efez5hqoDZtEhCijcuIng==",
|
"integrity": "sha512-7Bo2LFEqGIK39G54AStas1RWouBOp+r7Vcv2vP1uLbY01Usea8SHvd2OjLP6nFgsQa0n/rto9oremHv5IAylrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kosmos/schemas": "^3.1.0",
|
"@kosmos/schemas": "^3.1.0",
|
||||||
@@ -26066,9 +26066,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@kredits/contracts": {
|
"@kredits/contracts": {
|
||||||
"version": "7.0.1",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.3.0.tgz",
|
||||||
"integrity": "sha512-4IhNQoPc1BCT63NKyDOmV+ryb82FKNbIqfDyTdant6WTI9lfkIAnI8Et+ynQmqxx3Efez5hqoDZtEhCijcuIng==",
|
"integrity": "sha512-7Bo2LFEqGIK39G54AStas1RWouBOp+r7Vcv2vP1uLbY01Usea8SHvd2OjLP6nFgsQa0n/rto9oremHv5IAylrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@kosmos/schemas": "^3.1.0",
|
"@kosmos/schemas": "^3.1.0",
|
||||||
|
|||||||
+3
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kredits-web",
|
"name": "kredits-web",
|
||||||
"version": "2.0.0-beta.1",
|
"version": "2.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Contribution dashboard of the Kosmos project",
|
"description": "Contribution dashboard of the Kosmos project",
|
||||||
"repository": "https://github.com/67P/kredits-web",
|
"repository": "https://github.com/67P/kredits-web",
|
||||||
@@ -17,12 +17,11 @@
|
|||||||
"start": "ember serve",
|
"start": "ember serve",
|
||||||
"test": "npm-run-all lint:* test:*",
|
"test": "npm-run-all lint:* test:*",
|
||||||
"test:ember": "ember test",
|
"test:ember": "ember test",
|
||||||
"start:local": "WEB3_PROVIDER_URL=http://localhost:8545 ember serve",
|
"start:local": "WEB3_PROVIDER_URL=http://localhost:8545 WEB3_CHAIN_ID=1337 WEB3_NETWORK_NAME='Hardhat Devchain' ember serve",
|
||||||
"build": "ember build --environment=production",
|
"build": "ember build --environment=production",
|
||||||
"build-prod": "rm -rf release/* && ember build -prod --output-path release",
|
"build-prod": "rm -rf release/* && ember build -prod --output-path release",
|
||||||
"preversion": "npm test",
|
"preversion": "npm test",
|
||||||
"version": "npm run build-prod && git add release/",
|
"version": "npm run build-prod && git add release/",
|
||||||
"postversion": "git push origin master",
|
|
||||||
"deploy": "git push 5apps master"
|
"deploy": "git push 5apps master"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -31,7 +30,7 @@
|
|||||||
"@ember/optional-features": "^1.3.0",
|
"@ember/optional-features": "^1.3.0",
|
||||||
"@glimmer/component": "^1.0.0",
|
"@glimmer/component": "^1.0.0",
|
||||||
"@glimmer/tracking": "^1.0.0",
|
"@glimmer/tracking": "^1.0.0",
|
||||||
"@kredits/contracts": "^7.0.1",
|
"@kredits/contracts": "^7.3.0",
|
||||||
"babel-preset-es2015": "^6.22.0",
|
"babel-preset-es2015": "^6.22.0",
|
||||||
"broccoli-asset-rev": "^3.0.0",
|
"broccoli-asset-rev": "^3.0.0",
|
||||||
"ember-auto-import": "^1.12.1",
|
"ember-auto-import": "^1.12.1",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+536
-296
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
+4
-4
@@ -8,10 +8,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
|
||||||
|
|
||||||
<meta name="kredits-web/config/environment" content="%7B%22modulePrefix%22%3A%22kredits-web%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22auto%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_DEFAULT_ASYNC_OBSERVERS%22%3Atrue%2C%22_JQUERY_INTEGRATION%22%3Afalse%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22name%22%3A%22kredits-web%22%2C%22version%22%3A%222.0.0-0%2B7c8ca45b%22%7D%2C%22web3ProviderUrl%22%3A%22https%3A%2F%2Frsk-testnet.kosmos.org%22%2C%22web3RequiredChainId%22%3A31%2C%22web3RequiredNetworkName%22%3A%22RSK%20Testnet%22%2C%22githubConnectUrl%22%3A%22https%3A%2F%2Fhal8000.chat.kosmos.org%2Fkredits%2Fsignup%2Fconnect%2Fgithub%22%2C%22githubSignupUrl%22%3A%22https%3A%2F%2Fhal8000.chat.kosmos.org%2Fkredits%2Fsignup%2Fgithub%22%2C%22ipfs%22%3A%7B%22host%22%3A%22ipfs.kosmos.org%22%2C%22port%22%3A%225444%22%2C%22protocol%22%3A%22https%22%2C%22gatewayUrl%22%3A%22https%3A%2F%2Fipfs.kosmos.org%2Fipfs%22%7D%2C%22tokens%22%3A%7B%22BTC%22%3A%220x2260fac5e5542a773aa44fbcfedf7c193bc2c599%22%7D%2C%22btcBalanceAPI%22%3A%22https%3A%2F%2Fapi.kosmos.org%2Fkredits%2Fonchain_btc_balance%22%2C%22corsProxy%22%3A%22https%3A%2F%2Fcors.5apps.com%2F%3Furi%3D%22%2C%22exportApplicationGlobal%22%3Afalse%7D" />
|
<meta name="kredits-web/config/environment" content="%7B%22modulePrefix%22%3A%22kredits-web%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22auto%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_DEFAULT_ASYNC_OBSERVERS%22%3Atrue%2C%22_JQUERY_INTEGRATION%22%3Afalse%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22name%22%3A%22kredits-web%22%2C%22version%22%3A%222.2.0%2B0777c213%22%7D%2C%22web3ProviderUrl%22%3A%22https%3A%2F%2Frsk-testnet.kosmos.org%22%2C%22web3ChainId%22%3A31%2C%22web3NetworkName%22%3A%22RSK%20Testnet%22%2C%22githubConnectUrl%22%3A%22https%3A%2F%2Fhal8000.kosmos.chat%2Fkredits%2Fsignup%2Fconnect%2Fgithub%22%2C%22githubSignupUrl%22%3A%22https%3A%2F%2Fhal8000.kosmos.chat%2Fkredits%2Fsignup%2Fgithub%22%2C%22ipfs%22%3A%7B%22host%22%3A%22ipfs.kosmos.org%22%2C%22port%22%3A%225444%22%2C%22protocol%22%3A%22https%22%2C%22gatewayUrl%22%3A%22https%3A%2F%2Fipfs.kosmos.org%2Fipfs%22%7D%2C%22tokens%22%3A%7B%22BTC%22%3A%220x2260fac5e5542a773aa44fbcfedf7c193bc2c599%22%7D%2C%22communityFundsAPI%22%3A%7B%22balances%22%3A%7B%22onchain%22%3A%7B%22icon%22%3A%22icon-btc.png%22%2C%22symbol%22%3A%22BTC%22%2C%22description%22%3A%22BTC%20on%20chain%22%2C%22url%22%3A%22https%3A%2F%2Fapi.kosmos.org%2Fbtcpay%2Fonchain_btc_balance%22%7D%2C%22lightning%22%3A%7B%22icon%22%3A%22icon-btc-lightning.png%22%2C%22symbol%22%3A%22BTC%22%2C%22description%22%3A%22BTC%20on%20Lightning%20Network%22%2C%22url%22%3A%22https%3A%2F%2Fapi.kosmos.org%2Fbtcpay%2Flightning_btc_balance%22%7D%7D%7D%2C%22corsProxy%22%3A%22https%3A%2F%2Fcors.5apps.com%2F%3Furi%3D%22%2C%22exportApplicationGlobal%22%3Afalse%7D" />
|
||||||
|
|
||||||
<link integrity="" rel="stylesheet" href="/assets/vendor-ca96b2e19fc9f356e3dbad90c9f3f323.css">
|
<link integrity="" rel="stylesheet" href="/assets/vendor-ca96b2e19fc9f356e3dbad90c9f3f323.css">
|
||||||
<link integrity="" rel="stylesheet" href="/assets/kredits-web-bdbc10028be8d48749a246964068e561.css">
|
<link integrity="" rel="stylesheet" href="/assets/kredits-web-1ce689f6aada92a7d79a1947300931cf.css">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
||||||
<script src="/assets/vendor-4ee536ded971436a83d35253df25f2cb.js" integrity="sha256-yb+pC9iuZ8WcQwk9VuDNhSlWguL7BEABsEOEhBpwUC0= sha512-50LApx5XqeKHu/FMaljzqhAMPgkqSiy2rDhUcViYBcXWBBYmilFirup/iWvLiNCdfr5ct9wk7+tU9iShdSgDww==" ></script>
|
<script src="/assets/vendor-4d87b3e0995c5bb18e46836a089900a4.js" integrity="sha256-5WmWY05Kd+n98Sw0GwuaWodmDVBZ2yVGGtC/2owVfQE= sha512-tiGPA5zvfhnSC9LPTmpvXJmOu30yhsdPqSiZJMPGmE/ez/xJLMaM29vjs4Cs74cDf6fX4uYSGRIu1KfOyGpJYA==" ></script>
|
||||||
<script src="/assets/kredits-web-597bd2c84f295cea39c51678944cb39f.js" integrity="sha256-Ctb96mli0/Rkuej7AJB6wLegTJElHqGYpBOiKkB3spw= sha512-pwHfdUGsokaPO3vAZV4qYG+ljL+5ceExlHARAoyTsd15+yCDCbKsmJzuwLXnagNn/O1IzkDlEN0wed5l3FbuhQ==" ></script>
|
<script src="/assets/kredits-web-dab5f8c2df81891a0eb6d4d2bbf24a27.js" integrity="sha256-5XKNFa8JPKQWJztf69+Qhuszl/RV5uIYlg8/aPYp0ZY= sha512-0NYDa2a5cJBY6LxPDrEtaqD9YpuDEH7oR17jLvoMzWHBx4WIwerNRc9uadXwg+7UMnua3ysxt8M6i9uaJKRhTA==" ></script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -3,37 +3,90 @@ import { setupRenderingTest } from 'ember-qunit';
|
|||||||
import { render } from '@ember/test-helpers';
|
import { render } from '@ember/test-helpers';
|
||||||
import hbs from 'htmlbars-inline-precompile';
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
|
||||||
|
function stubWalletApi () {
|
||||||
|
window.ethereum = {
|
||||||
|
on: function() { return true; },
|
||||||
|
request: function() { return Promise.resolve(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module('Integration | Component | topbar-account-panel', function(hooks) {
|
module('Integration | Component | topbar-account-panel', function(hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
test('unknown user without wallet (or no permission to get wallet/account info)', async function(assert) {
|
test('Unknown user without wallet', async function(assert) {
|
||||||
await render(hbs`<TopbarAccountPanel />`);
|
await render(hbs`<TopbarAccountPanel />`);
|
||||||
|
|
||||||
assert.ok(this.element.textContent.trim().match(/^Anonymous/));
|
assert.ok(
|
||||||
|
this.element.textContent.trim().match(/^Anonymous/),
|
||||||
|
'shows current user as anonymous'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#signup').length, 1,
|
||||||
|
'signup button is visible'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#connect').length, 0,
|
||||||
|
'connect button is not visible'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('unknown user with Ethereum wallet', async function(assert) {
|
test('Unknown user with disconnected wallet', async function(assert) {
|
||||||
|
stubWalletApi();
|
||||||
|
let service = this.owner.lookup('service:kredits');
|
||||||
|
service.set('currentUserAccounts', []);
|
||||||
|
await render(hbs`<TopbarAccountPanel />`);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
this.element.textContent.trim().match(/^Anonymous/),
|
||||||
|
'shows current user as anonymous'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#signup').length, 1,
|
||||||
|
'signup button is visible'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#connect').length, 1,
|
||||||
|
'connect button is visible'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unknown user with connected wallet', async function(assert) {
|
||||||
|
stubWalletApi();
|
||||||
let service = this.owner.lookup('service:kredits');
|
let service = this.owner.lookup('service:kredits');
|
||||||
service.set('currentUserAccounts', [{ foo: 'bar' }]);
|
service.set('currentUserAccounts', [{ foo: 'bar' }]);
|
||||||
await render(hbs`<TopbarAccountPanel />`);
|
await render(hbs`<TopbarAccountPanel />`);
|
||||||
|
|
||||||
assert.ok(this.element.textContent.trim().match(/^Anonymous/));
|
assert.ok(
|
||||||
|
this.element.textContent.trim().match(/^Anonymous/),
|
||||||
|
'shows current user as anonymous'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#signup').length, 1,
|
||||||
|
'signup button is visible'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#connect').length, 0,
|
||||||
|
'connect button is not visible'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('known contributor', async function(assert) {
|
test('Known contributor', async function(assert) {
|
||||||
|
stubWalletApi();
|
||||||
let service = this.owner.lookup('service:kredits');
|
let service = this.owner.lookup('service:kredits');
|
||||||
service.set('currentUserAccounts', [{ foo: 'bar' }]);
|
service.set('currentUserAccounts', [{ foo: 'bar' }]);
|
||||||
service.set('currentUser', {
|
service.set('currentUser', { name: 'Dorian Nakamoto', isCore: false });
|
||||||
name: 'Dorian Nakamoto',
|
|
||||||
isCore: false
|
|
||||||
});
|
|
||||||
await render(hbs`<TopbarAccountPanel />`);
|
await render(hbs`<TopbarAccountPanel />`);
|
||||||
|
|
||||||
assert.dom(this.element).hasText('Dorian Nakamoto');
|
assert.dom(this.element).hasText(
|
||||||
|
'Dorian Nakamoto', 'shows current user\'s name'
|
||||||
service.set('currentUser.isCore', true);
|
);
|
||||||
await render(hbs`<TopbarAccountPanel />`);
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#signup').length, 0,
|
||||||
assert.equal(this.element.querySelectorAll('span.core-flag').length, 1);
|
'signup button is not visible'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#connect').length, 0,
|
||||||
|
'connect button is not visible'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupTest } from 'ember-qunit';
|
||||||
|
import createComponent from 'kredits-web/tests/helpers/create-component';
|
||||||
|
// import moment from 'moment';
|
||||||
|
|
||||||
|
module('Unit | Component | confirmed-in', function(hooks) {
|
||||||
|
setupTest(hooks);
|
||||||
|
|
||||||
|
test('it exists', function(assert) {
|
||||||
|
let component = createComponent('component:confirmed-in');
|
||||||
|
assert.ok(component);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#confirmedInBlocks', function(assert) {
|
||||||
|
const kredits = this.owner.lookup('service:kredits');
|
||||||
|
kredits.set('currentBlock', 419550);
|
||||||
|
let component = createComponent('component:confirmed-in');
|
||||||
|
component.args.confirmedAtBlock = 420000;
|
||||||
|
|
||||||
|
assert.equal(component.confirmedInBlocks, 450);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#confirmedInSeconds', function(assert) {
|
||||||
|
const kredits = this.owner.lookup('service:kredits');
|
||||||
|
kredits.set('currentBlock', 419550);
|
||||||
|
let component = createComponent('component:confirmed-in');
|
||||||
|
component.args.confirmedAtBlock = 420000;
|
||||||
|
|
||||||
|
assert.equal(component.confirmedInSeconds, 13500);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#confirmedInHumanTime', function(assert) {
|
||||||
|
const kredits = this.owner.lookup('service:kredits');
|
||||||
|
kredits.set('currentBlock', 419550);
|
||||||
|
let component = createComponent('component:confirmed-in');
|
||||||
|
component.args.confirmedAtBlock = 420000;
|
||||||
|
|
||||||
|
assert.equal(component.confirmedInHumanTime, '4 hours');
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#isConfirmed', function(assert) {
|
||||||
|
const kredits = this.owner.lookup('service:kredits');
|
||||||
|
kredits.set('currentBlock', 430000);
|
||||||
|
let component = createComponent('component:confirmed-in');
|
||||||
|
component.args.confirmedAtBlock = 420000;
|
||||||
|
|
||||||
|
assert.ok(component.isConfirmed);
|
||||||
|
})
|
||||||
|
});
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupTest } from 'ember-qunit';
|
import { setupTest } from 'ember-qunit';
|
||||||
|
|
||||||
module('Unit | Route | signup/eth-account', function(hooks) {
|
module('Unit | Route | signup/account', function(hooks) {
|
||||||
setupTest(hooks);
|
setupTest(hooks);
|
||||||
|
|
||||||
test('it exists', function(assert) {
|
test('it exists', function(assert) {
|
||||||
let route = this.owner.lookup('route:signup/eth-account');
|
let route = this.owner.lookup('route:signup/account');
|
||||||
assert.ok(route);
|
assert.ok(route);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user