Compare commits

...

71 Commits

Author SHA1 Message Date
raucao 1b78cb4cb4 2.2.1 2024-03-21 12:26:21 +01:00
Râu Cao b32f805cbd Merge pull request #223 from 67P/bugfix/220-reimbursement_sums
Reimbursement form fixes and improvements
2024-03-21 11:38:45 +01:00
raucao 3110fd347e Remove unused variable 2024-03-20 18:31:16 +01:00
raucao 5612ac4e95 Add button type 2024-03-20 18:28:13 +01:00
raucao 69ab4913aa Require connected account for adding reimbursement
Just in the UI, so nothing gets reloaded by accident later
2024-03-20 18:18:10 +01:00
raucao 39e01e4558 Remove commented code 2024-03-20 17:46:26 +01:00
raucao 7afc75dbff Fix wrong BTC sum when adding multiple line items
closes #220
2024-03-20 17:24:37 +01:00
raucao f5f74ae27f Automatically select connected user/contributor as recipient 2024-03-20 17:24:07 +01:00
raucao 55f7baecf6 Update caniuse 2024-03-20 17:21:21 +01:00
raucao bd199fcf26 2.2.0 2024-03-20 15:17:01 +01:00
Râu Cao 0777c213ba Merge pull request #222 from 67P/feature/reimbursement-lists
Improve reimbursement lists
2024-03-20 14:50:33 +01:00
raucao 071922d979 Remove obsolete debug log 2024-03-20 14:35:01 +01:00
raucao fc044df9fd Add correct role to interactive element
Co-authored-by: Garret Alfert <alfert@wevelop.de>
2024-03-20 14:32:37 +01:00
raucao dd2bdd5332 Show dates in collapsed entries of confirmed reimbursements 2024-03-14 19:15:10 +01:00
raucao 0cd05d14bc Don't fail linting for click handlers on arbitrary HTML elements 2024-03-14 17:55:46 +01:00
raucao 3ee140723c Hide line items by default for confirmed reimbursements 2024-03-14 17:49:12 +01:00
raucao 49b3825af4 Show confirmation block for confirmed reimbursements 2024-03-14 17:48:23 +01:00
raucao af309ff682 Hide veto button for confirmed reimbursements 2024-03-14 16:43:57 +01:00
raucao 53f13e4a63 Move reimbursement items to separate component 2024-03-14 16:43:57 +01:00
raucao bcdd033f34 Use prod budget endpoints in dev by default 2024-03-14 16:43:29 +01:00
raucao b9e7737589 Fix typo 2024-03-14 16:43:21 +01:00
raucao 57b9bccb4a 2.1.1 2024-01-22 15:48:12 +03:00
raucao 5c9fbf5043 Prevent fingerprinting for filenames from config
Ember has no way of finding them in the code and thus cannot turn hrefs
into fingerprinted URLs
2024-01-22 15:46:39 +03:00
raucao 205d2d2afc 2.1.0 2024-01-22 14:08:21 +03:00
Râu Cao 895bf569ff Merge pull request #217 from 67P/feature/budget-balances
Use new balance APIs from akkounts, add Lightning balance
2024-01-14 17:29:07 +01:00
raucao dbedf1dbe8 Use new balance APIs from akkounts
* Use sats integer values instead of floats
* Add lightning Network account balance
* Use icons and titles/descriptions to discern between balance types
* Use more flexible config for balances (to add e.g. RBTC or BTC ecash
  later)
2024-01-12 14:59:57 +03:00
Râu Cao 26f2f2afe9 Merge pull request #216 from 67P/feature/main_nav
Add main navigation menu, replace intro blurb with full About page
2023-12-05 09:17:31 +01:00
Râu Cao 6c456b1184 Update release build 2023-11-17 14:29:24 +01:00
Râu Cao 03e7f14d1c Use native class 2023-08-30 17:18:33 +02:00
Râu Cao e94e747ba1 Wording 2023-08-30 17:18:26 +02:00
Râu Cao 3679412b3c Add main navigation menu, replace intro blurb with full About page 2023-08-30 17:17:39 +02:00
Râu Cao ea15e69d79 Update README 2023-08-15 10:27:27 +02:00
Râu Cao 8cc3da40b7 2.0.0 2023-08-14 18:28:02 +02:00
Râu Cao bdeab512b6 Upgrade kredits-contracts 2023-08-14 17:57:04 +02:00
Râu Cao 97ebebe28c Merge pull request #214 from 67P/chore/remove_last_ethereum_bits
Remove mentions of Ethereum from UI and code, refactor wallet connect
2023-08-14 17:53:44 +02:00
Râu Cao d5edb93ec5 Use new hubot URL 2023-08-14 17:50:47 +02:00
Râu Cao 0ecc4079c2 Ember code updates 2023-08-14 17:50:10 +02:00
Râu Cao 3f080cf553 Open external link in new tab
closes #170
2023-02-02 12:32:50 +08:00
Râu Cao 0bdb4d2021 Connect wallet and fill in address during signup
In case there's a wallet available, ask the user to connect it and fill
in their account address in the form field.

Also, check the status code when submitting it to the back-end and show
an error message instead of the success/completion page in case the
contributor could not be created.
2023-01-21 12:39:14 +08:00
Râu Cao 1e7d8491f9 Refactor wallet connect, automatically propose network 2023-01-20 17:29:20 +08:00
Râu Cao 4356288497 Rename signup route, address property 2023-01-20 17:29:20 +08:00
Râu Cao 87bf8ff2df Inspect user txs on RSK explorer 2023-01-20 17:29:20 +08:00
Râu Cao 26975636a0 Update README 2023-01-20 17:29:17 +08:00
Râu Cao dad56a7b31 Merge pull request #212 from 67P/bugfix/load_contributor
Fix dashboard hanging when loading app from contributor URL
2023-01-20 17:29:05 +08:00
Râu Cao d069fa02ab Add links 2023-01-15 14:41:25 +08:00
Râu Cao d219abc358 RSK -> Rootstock 2023-01-15 14:39:54 +08:00
Râu Cao 13989bf973 Update README
refs #18
2023-01-15 14:38:43 +08:00
Râu Cao b3ab69d599 Update CI badge in README 2023-01-15 14:28:19 +08:00
Râu Cao fe3816501c Fix dashboard hanging when loading app from contributor URL
Loading the app from /dashboard/contributors/:id is failing, because the
contributors are loaded later. This fetches the requested contributor
separately when launching the app from a contributor profile URL.
2023-01-15 14:24:51 +08:00
Râu Cao 854402aa73 Merge pull request #213 from 67P/feature/github_actions_ci
Switch from Travis CI to GitHub Actions
2023-01-15 14:24:14 +08:00
Râu Cao abc19c715b Fix double builds in CI 2023-01-15 14:20:48 +08:00
Râu Cao b6434ac8d2 Remove Travis config 2023-01-15 14:17:30 +08:00
Râu Cao 4788b8c51c Add GitHub Actions CI config 2023-01-15 14:17:09 +08:00
Râu Cao 6a72a5b418 2.0.0-beta.3 2023-01-15 13:18:42 +08:00
Râu Cao d90ddd464a Hide confirmed-in content when confirmed 2023-01-15 13:16:38 +08:00
Râu Cao 2626543f3e Fix linter errors 2023-01-15 13:09:39 +08:00
Râu Cao 088db24534 Merge pull request #207 from 67P/feature/optional_full_sync
Make full data sync optional, start via button
2023-01-15 12:36:59 +08:00
Râu Cao f7b7daa024 Merge pull request #210 from 67P/feature/confirmation-eta
Add confirmed-in component, show ETA for proposed reimbursements
2023-01-15 12:36:43 +08:00
Râu Cao bb11e2267b Merge pull request #211 from 67P/feature/expense-list
Expense list UI improvements
2023-01-15 12:35:17 +08:00
Râu Cao 7cd023a21b Update app/components/add-reimbursement/template.hbs
Co-authored-by: Manuel Wiedenmann <manuel@funkensturm.de>
2023-01-15 12:34:40 +08:00
Râu Cao 1095bf0218 Add more button colors, icon style for small buttons
Plus tag icon for the tag buttons
2023-01-13 15:58:39 +08:00
Râu Cao 138cec0389 Localize date in expense list 2023-01-13 15:58:39 +08:00
Râu Cao a93be41e08 Add expense-list component, DRY up code, add tags 2023-01-13 15:58:39 +08:00
Râu Cao 89f6fa0b5c Use component argument in tests 2023-01-13 15:57:50 +08:00
Râu Cao e2a80eafd1 Remove dev logs 2023-01-13 14:14:24 +08:00
Râu Cao 662e76979b Show confirmation ETA for proposed reimbursements 2023-01-11 14:19:03 +08:00
Râu Cao 6c7de97e38 Get duration in human time 2023-01-11 14:10:35 +08:00
Râu Cao fe27b010da Add confirmed-in component 2023-01-11 13:59:41 +08:00
Râu Cao ff716c68ea Update current block when new one is mined 2023-01-11 13:26:58 +08:00
Râu Cao 84dded2f10 Do not automatically push new release versions 2023-01-11 12:44:56 +08:00
Râu Cao f86190030f Make full data sync optional, start via button 2022-11-14 22:58:15 +01:00
79 changed files with 1643 additions and 798 deletions
+30
View File
@@ -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
+6
View File
@@ -48,6 +48,12 @@ module.exports = {
"no-action"
]
},
{
"moduleId": "app/components/expense-list/template",
"only": [
"no-invalid-role"
]
},
{
"moduleId": "app/components/topbar-account-panel/template",
"only": [
-21
View File
@@ -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
+14 -8
View File
@@ -1,9 +1,15 @@
[![Build Status](https://travis-ci.org/67P/kredits-web.svg?branch=master)](https://travis-ci.org/67P/kredits-web)
[![Build Status](https://github.com/67P/kredits-web/actions/workflows/ci.yml/badge.svg)](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.
A short introduction of this app could easily go here.
This is the main Web UI for Kosmos [Kredits](https://wiki.kosmos.org/Kredits).
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
@@ -24,9 +30,9 @@ You will need the following things properly installed on your computer.
### Building/running for development
* `ember serve` - by default kredits-web connects to the Ethreum Kovan network
* Visit your app at [http://localhost:4200](http://localhost:4200).
* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).
* `npm start` - by default Kredits Web connects to the Rootstock testnet network
* Visit the app at [http://localhost:4200](http://localhost:4200).
* 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.
@@ -48,7 +54,7 @@ Make use of the many generators for code, try `ember help generate` for more det
### Building
* `ember build` (development)
* `ember build --environment production` (production)
* `npm run build` (production)
### Deploying
+1 -1
View File
@@ -1,6 +1,6 @@
<form onsubmit={{action "submit"}}>
<p>
<label for="c-account">Ethereum account</label>
<label for="c-account">Rootstock account</label>
<Input @type="text"
@value={{this.account}}
name="account" id="c-account"
@@ -27,6 +27,10 @@ export default class AddReimbursementComponent extends Component {
this.exchangeRates.fetchRates();
}
get contributorId () {
return this.recipientId || this.kredits.currentUser?.id;
}
get isValidTotal () {
return isValidAmount(this.total);
}
@@ -70,7 +74,7 @@ export default class AddReimbursementComponent extends Component {
}
updateTotalAmountFromFiat() {
let btcAmount = parseFloat(this.total);
let btcAmount = 0;
if (this.exchangeRates.btceur > 0 && this.totalEUR > 0) {
btcAmount += (this.totalEUR / this.exchangeRates.btceur);
@@ -104,7 +108,7 @@ export default class AddReimbursementComponent extends Component {
@action
updateContributor (event) {
this.recipientId = event.target.value;
this.recipientId = parseInt(event.target.value);
}
@action
@@ -136,12 +140,12 @@ export default class AddReimbursementComponent extends Component {
if (!this.kredits.currentUser) { window.alert('You need to connect your RSK account first.'); return false }
if (!this.kredits.currentUserIsCore) { window.alert('Only core contributors can submit reimbursements.'); return false }
const contributor = this.contributors.findBy('id', parseInt(this.recipientId));
const contributor = this.contributors.findBy('id', this.contributorId);
const attributes = {
amount: parseInt(parseFloat(this.total) * 100000000), // convert to sats
token: config.tokens['BTC'],
recipientId: parseInt(this.recipientId),
recipientId: this.contributorId,
title: `Expenses covered by ${contributor.name}`,
description: this.description,
url: this.url,
+9 -23
View File
@@ -2,8 +2,7 @@
<label>
<p class="label">Contributor:</p>
<p>
<select required {{on "change" this.updateContributor}}>
<option value="" selected disabled hidden></option>
<select id="contributor" required {{on "change" this.updateContributor}}>
{{#each this.contributors as |contributor|}}
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
{{/each}}
@@ -15,6 +14,7 @@
<p class="label">Total amount (BTC):</p>
<p>
<Input @type="text"
@name="total-btc"
@placeholder="0.0015"
@value={{this.total}}
@required={{true}}
@@ -44,29 +44,15 @@
<h3>Expense items</h3>
{{#if this.expenses}}
<ul class="expense-list">
{{#each this.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>
<div class="actions">
<button {{on "click" (fn this.removeExpenseItem expense)}}
class="danger small" type="button">delete</button>
</div>
</li>
{{/each}}
</ul>
<ExpenseList @expenses={{this.expenses}}
@removeExpenseItem={{this.removeExpenseItem}}
@deletable={{true}} />
<p class="actions">
<button {{on "click" this.showExpenseForm}}
class="green small" type="button">+ Add another item</button>
id="add-another-item" class="green small" type="button">
+ Add another item
</button>
</p>
{{else}}
<p>No line items yet.</p>
+5 -2
View File
@@ -1,10 +1,13 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
export default class BudgetBalancesComponent extends Component {
@service communityFunds;
@alias('communityFunds.balances') balances;
get balancesSorted () {
return this.communityFunds.balances
.sortBy('confirmed_balance').reverse();
}
get loading () {
return !this.communityFunds.balancesLoaded;
+12 -4
View File
@@ -7,11 +7,19 @@
</tr>
</thead>
<tbody>
{{#each this.balances as |balance|}}
{{#each this.balancesSorted as |balance|}}
<tr>
<th>{{balance.token.symbol}}</th>
<td class="amount">{{balance.confirmed_balance}}</td>
<td class="fiat-amount">~{{balance.balanceUSD}} USD</td>
<th>
<img src={{balance.token.icon}}
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>
{{/each}}
</tbody>
+24
View File
@@ -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;
}
}
+5
View File
@@ -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}}
+9
View File
@@ -0,0 +1,9 @@
import Component from '@glimmer/component';
export default class ExpenseListComponent extends Component {
get showDeleteButton () {
return !!this.args.deletable;
}
}
+29
View File
@@ -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>&#8239;<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,23 +1,10 @@
import Component from '@glimmer/component';
import { sort } from '@ember/object/computed';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import config from 'kredits-web/config/environment';
export default class ReimbursementListComponent extends Component {
@service kredits;
itemSorting = Object.freeze(['pendingStatus:asc', 'id:desc']);
@sort('args.items', 'itemSorting') itemsSorted;
get ipfsGatewayUrl () {
return config.ipfs.gatewayUrl;
}
@action
veto (id) {
this.kredits.vetoReimbursement(id).then(transaction => {
console.debug('[controllers:budget] Veto submitted to chain: '+transaction.hash);
});
}
}
+3 -44
View File
@@ -1,46 +1,5 @@
<ul class="item-list spaced reimbursement-list {{if @loading 'loading'}}">
{{#each this.itemsSorted as |reimbursement|}}
<li data-reimbursement-id={{reimbursement.id}}
class="{{item-status reimbursement}}">
<p class="meta">
<span class="title">
Expenses covered by {{reimbursement.contributor.name}}
</span>
<span class="recipient">
<UserAvatar @contributor={{reimbursement.contributor}} />
</span>
</p>
<p class="token-amount">
<span class="amount">
{{sats-to-btc reimbursement.amount}}</span>&#8239;<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>
{{#if this.kredits.currentUserIsCore}}
<p>
<a href="{{this.ipfsGatewayUrl}}/{{reimbursement.ipfsHash}}"
class="button small" target="_blank" rel="noopener noreferrer">
Inspect IPFS data
</a>
<button {{on "click" (fn this.veto reimbursement.id)}}
disabled={{reimbursement.vetoed}}
class="button small danger" type="button">veto</button>
</p>
{{/if}}
</li>
<ul class="item-list collapsible spaced reimbursement-list {{if @loading 'loading'}}">
{{#each this.itemsSorted as |item|}}
<ReimbursementItem @reimbursement={{item}} />
{{/each}}
</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 { computed } from '@ember/object';
import { action } from '@ember/object';
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(),
router: service(),
setupInProgress: false,
userHasEthereumWallet: computed(function() {
get userHasWallet () {
return isPresent(window.ethereum);
}),
showConnectButton: computed('userHasEthereumWallet',
'kredits.hasAccounts', function() {
return this.userHasEthereumWallet &&
!this.kredits.hasAccounts;
}),
actions: {
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);
}
}
}
});
get walletConnected () {
return this.userHasWallet && this.kredits.hasAccounts;
}
get walletDisconnected () {
return this.userHasWallet && !this.kredits.hasAccounts;
}
@action
signup () {
this.router.transitionTo('signup');
}
@action
async connectWallet () {
this.setupInProgress = true;
await this.kredits.connectWallet();
this.setupInProgress = false;
}
}
@@ -2,16 +2,15 @@
{{#if this.setupInProgress}}
Connecting account...
{{else}}
{{#if (and this.kredits.hasAccounts this.kredits.currentUser)}}
{{#if (and this.walletConnected this.kredits.currentUser)}}
{{this.kredits.currentUser.name}}
{{#if this.kredits.currentUserIsCore}}
<span class="core-flag">(core)</span>
{{/if}}
{{else}}
<span class="anonymous">Anonymous</span>
<button onclick={{action "signup"}} class="small" type="button">Sign up</button>
{{#if this.showConnectButton}}
<button onclick={{action "connectAccount"}} class="small green" type="button">Connect account</button>
<button onclick={{action "signup"}} id="signup"
class="small" type="button">Sign up</button>
{{#if this.walletDisconnected}}
<button onclick={{action "connectWallet"}} id="connect"
class="small green" type="button">Connect wallet</button>
{{/if}}
{{/if}}
{{/if}}
+3 -3
View File
@@ -1,6 +1,6 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
kredits: service(),
});
export default class ApplicationController extends Controller {
@service kredits;
}
+16
View File
@@ -1,10 +1,26 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import { action } from '@ember/object';
export default class BudgetController extends Controller {
@service kredits;
@service router;
@alias('kredits.reimbursementsUnconfirmed') reimbursementsUnconfirmed;
@alias('kredits.reimbursementsConfirmed') reimbursementsConfirmed;
@alias('kredits.currentUserIsCore') currentUserIsCore;
@action
addReimbursement () {
if (!this.kredits.currentUser) {
window.alert('You need to connect your RSK account first.');
return false;
}
if (!this.kredits.currentUserIsCore) {
window.alert('Only core contributors can submit reimbursements.');
return false;
}
this.router.transitionTo('reimbursements.new');
}
}
+2 -4
View File
@@ -1,6 +1,6 @@
import Controller from '@ember/controller';
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';
export default Controller.extend({
@@ -33,9 +33,7 @@ export default Controller.extend({
showQuickFilterUnconfirmed: false,
showQuickFilterConfirmed: false,
showIntroText: computed('kredits.{hasAccounts,currentUser}', function(){
return (!this.kredits.hasAccounts || !this.kredits.currentUser);
}),
showFullContributionSync: gt('kredits.missingHistoricContributionsCount', 0),
actions: {
+50
View File
@@ -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');
}
})
}
}
-49
View File
@@ -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');
});
}
}
});
+6 -8
View File
@@ -1,14 +1,12 @@
import Controller from '@ember/controller';
import { action } from '@ember/object';
import config from 'kredits-web/config/environment';
export default Controller.extend({
actions: {
connectGithub () {
window.location = config.githubConnectUrl;
}
export default class IndexController extends Controller {
@action
connectGithub () {
window.location = config.githubConnectUrl;
}
});
}
+8
View File
@@ -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);
});
+6
View File
@@ -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
View File
@@ -11,7 +11,6 @@ Router.map(function() {
this.route('contributors', function() {
this.route('show', { path: ':id' });
});
this.route('contributions', function() {
this.route('show', { path: ':id' });
});
@@ -26,16 +25,15 @@ Router.map(function() {
});
this.route('signup', function() {
this.route('github');
this.route('eth-account');
this.route('account');
this.route('complete');
});
this.route('budget', function() {
this.route('expenses');
this.route('reimbursements', function() {});
});
this.route('reimbursements', function() {
this.route('new');
});
this.route('about');
});
+4
View File
@@ -0,0 +1,4 @@
import Route from '@ember/routing/route';
export default class AboutRoute extends Route {
}
+5 -2
View File
@@ -11,8 +11,11 @@ export default class DashboardRoute extends Route {
schedule('afterRender', this.kredits.syncContributions,
this.kredits.syncContributions.perform);
}
schedule('afterRender', this.kredits.fetchMissingContributions,
this.kredits.fetchMissingContributions.perform);
// TODO fetch automatically under a certain threshold
// 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);
}
}
+7 -1
View File
@@ -8,7 +8,13 @@ export default Route.extend({
contributors: alias('kredits.contributors'),
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) {
+23
View File
@@ -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');
}
}
}
-17
View File
@@ -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');
}
}
});
+1 -1
View File
@@ -22,7 +22,7 @@ export default Route.extend({
this.kredits.set('githubAccessToken', accessToken);
this.transitionTo('signup.eth-account');
this.transitionTo('signup.account');
}
});
+2 -2
View File
@@ -4,8 +4,8 @@ import config from 'kredits-web/config/environment';
function createStore(name) {
let networkName;
if (config.web3RequiredNetworkName) {
networkName = config.web3RequiredNetworkName.toLocaleLowerCase().replace(' ', '-');
if (config.web3NetworkName) {
networkName = config.web3NetworkName.toLocaleLowerCase().replace(' ', '-');
} else {
networkName = 'custom';
}
+24 -10
View File
@@ -1,7 +1,7 @@
import Service, { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { A } from '@ember/array';
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';
export default class CommunityFundsService extends Service {
@@ -12,9 +12,20 @@ export default class CommunityFundsService extends Service {
@task
*fetchBalances () {
yield fetch(config.btcBalanceAPI).then(res => res.json())
.then(res => {
return this.processBalances(res);
const promises = [];
const balances = config.communityFundsAPI.balances;
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 => {
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();
// Format and round the approximate USD value
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);
this.balances.pushObject({
...res,
...{ token: { name: 'BTC', symbol: 'BTC'} }
...{ token: { icon: `/img/${config.icon}`, symbol: config.symbol, description: config.description } }
});
this.balancesLoaded = true;
}
}
+6
View File
@@ -19,7 +19,13 @@ export default class ExchangeRatesService extends Service {
@tracked btceur = 0;
@tracked btcusd = 0;
get exchangeRatesLoaded () {
return (this.btceur !== 0) && (this.btcusd !== 0);
}
async fetchRates (source='bitstamp') {
if (this.exchangeRatesLoaded) return;
switch(source) {
case 'bitstamp':
this.btceur = await fetchFromBitstamp('btceur');
+73 -31
View File
@@ -5,7 +5,7 @@ import Service from '@ember/service';
import EmberObject from '@ember/object';
import { computed } from '@ember/object';
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 { 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 processReimbursementData from 'kredits-web/utils/process-reimbursement-data';
import formatKredits from 'kredits-web/utils/format-kredits';
import switchNetwork from 'kredits-web/utils/switch-network';
import config from 'kredits-web/config/environment';
import Contributor from 'kredits-web/models/contributor';
@@ -28,7 +29,7 @@ export default Service.extend({
browserCache: service(),
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,
contributors: null,
contributions: null,
@@ -48,11 +49,28 @@ export default Service.extend({
contributionsNeedSync: false,
reimbursementsNeedSync: false,
missingHistoricContributionsCount: 0,
init () {
this._super(...arguments);
this.set('contributors', []);
this.set('contributions', []);
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
@@ -62,7 +80,7 @@ export default Service.extend({
let ethProvider;
return new Promise(resolve => {
function instantiateWithoutAccount () {
function instantiateWithoutWallet () {
console.debug('[kredits] Creating new instance from npm module class');
console.debug(`[kredits] providerURL: ${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');
ethProvider = new ethers.providers.Web3Provider(web3Provider);
ethProvider = new ethers.providers.Web3Provider(web3Provider);
const network = await ethProvider.getNetwork();
const accounts = await ethProvider.listAccounts();
const chainId = config.web3ChainId;
const network = await ethProvider.getNetwork();
if (isPresent(config.web3RequiredChainId) &&
network.chainId !== config.web3RequiredChainId) {
return instantiateWithoutAccount();
}
if (isEmpty(accounts)) return instantiateWithoutWallet();
ethProvider.listAccounts().then(accounts => {
if (network.chainId !== chainId) {
return switchNetwork();
} else {
context.set('currentUserAccounts', accounts);
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
resolve({
ethProvider,
ethSigner
});
});
}
}
if (window.ethereum) {
if (window.ethereum.isConnected()) {
instantiateWithAccount(window.ethereum, this);
} else {
instantiateWithoutAccount();
}
}
// Legacy dapp browsers...
else if (window.web3) {
instantiateWithAccount(window.web3.currentProvider, this);
}
// Non-dapp browsers...
else {
instantiateWithoutAccount();
instantiateWithWallet(window.ethereum, this);
} else {
instantiateWithoutWallet();
}
});
},
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 () {
const kredits = await this.getEthProvider().then(providerAndSigner => {
return new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
@@ -118,9 +142,12 @@ export default Service.extend({
});
await kredits.init();
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) {
this.getCurrentUser.then(contributorData => {
@@ -227,12 +254,20 @@ export default Service.extend({
await this.loadObjectsFromCache('Contribution');
this.set('contributionsNeedSync', true);
} else {
await this.fetchContributions({ page: { size: 30 } });
await this.fetchContributions({ page: { size: 40 } });
}
await this.updateMissingHistoricContributionsCount();
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) {
if (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 () {
console.debug(`[kredits] Fetching all contributors from the network`);
return this.kredits.Contributor.all()
@@ -368,11 +409,12 @@ export default Service.extend({
syncContributions: task(function * () {
yield this.fetchNewContributions.perform();
yield this.syncUnconfirmedContributions.perform();
yield this.updateMissingHistoricContributionsCount();
this.set('contributionsNeedSync', false);
}).group('contributionTasks'),
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 toFetch = count - lastKnownContributionId;
@@ -389,7 +431,7 @@ export default Service.extend({
}),
fetchMissingContributions: task(function * () {
const count = yield this.kredits.Contribution.functions.contributionsCount();
const count = yield this.kredits.Contribution.count;
const allIds = [...Array(count+1).keys()];
allIds.shift(); // remove first item, which is 0
const loadedContributions = new Set(this.contributions.mapBy('id'));
@@ -738,5 +780,5 @@ export default Service.extend({
this.contributors
.findBy('address', to)
.incrementProperty('balance', value);
},
}
});
+34 -13
View File
@@ -31,32 +31,53 @@ button, input[type=submit], .button {
&.small {
font-size: 0.86rem;
padding: 0.2rem 0.8rem;
svg {
width: 1em;
height: 1em;
vertical-align: middle;
margin-right: 0.4rem;
}
}
&.danger:not(:disabled) {
color: $red;
background-color: rgba(40, 21, 21, 0.6);
border-color: rgba(40, 21, 21, 1);
&:hover {
background-color: rgba(40, 21, 21, 0.8);
}
&:focus, &:active, &.active {
border-color: $red;
}
&:hover { background-color: rgba(40, 21, 21, 0.8); }
&:focus, &:active, &.active { border-color: $red; }
}
&.green:not(:disabled) {
color: $green;
background-color: rgba(21, 40, 21, 0.6);
border-color: rgba(21, 40, 21, 1);
&:hover { background-color: rgba(21, 40, 21, 0.8); }
&:focus, &:active, &.active { border-color: $green; }
}
&:hover {
background-color: rgba(21, 40, 21, 0.8);
}
&:focus, &:active, &.active {
border-color: $green;
}
&.pink:not(:disabled) {
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; }
}
&.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 {
-4
View File
@@ -14,10 +14,6 @@ section#signup {
p {
font-size: 1.2rem;
&.mg-bottom-md {
margin-bottom: 2rem;
}
&.label {
margin-bottom: 1.5rem;
font-size: 1rem;
+10
View File
@@ -16,6 +16,16 @@ ul.item-list {
}
}
&.collapsible {
> li {
border-left: 1px solid transparent;
&:hover {
border-left: 1px solid $blue;
}
}
}
&.loading {
@include loading-border-top;
+26 -38
View File
@@ -1,3 +1,7 @@
body {
margin-bottom: 10rem;
}
main {
padding: 1rem 2rem;
@@ -14,7 +18,7 @@ main {
"contributions";
}
&#budget {
&#budget, &#about {
width: 100%;
display: grid;
grid-row-gap: 2rem;
@@ -56,9 +60,9 @@ main {
&.text-lg {
p {
font-size: 1.2rem;
margin-bottom: 1em;
line-height: 1.5em;
font-size: 1.35rem;
margin-bottom: 2rem;
line-height: 150%;
}
}
@@ -67,50 +71,29 @@ main {
}
p {
&.mg-bottom-md {
margin-bottom: 2rem;
}
&.actions {
text-align: center;
padding-top: 2rem;
}
}
}
}
}
#intro {
padding: 2rem;
background-color: rgba(0,0,0,.2);
font-size: 1.6rem;
&.text {
h2 {
margin: 4rem 0 2rem 0;
@include media-max(small) {
padding: 2rem 1rem;
font-size: 1.5rem;
}
&:first-of-type {
margin-top: 0;
}
}
h2 {
font-size: inherit;
margin-bottom: 1em;
}
p {
margin-bottom: 1em;
line-height: 1.5em;
font-size: 1.2rem;
&:last-child {
margin-bottom: 0;
p {
font-size: 1.35rem;
margin-bottom: 2rem;
line-height: 150%;
}
}
}
a {
text-decoration: none;
&:hover, &:active {
text-decoration: underline;
}
}
}
@@ -131,7 +114,7 @@ main {
}
}
&#budget {
&#budget, &#about {
grid-column-gap: 3rem;
grid-template-columns: 2fr 4fr 2fr;
grid-template-areas:
@@ -177,4 +160,9 @@ main section {
margin-bottom: 2rem;
}
}
// TODO Remove after switch to Tailwind CSS
.mb-4 { margin-bottom: 1rem; }
.mb-8 { margin-bottom: 2rem; }
}
+1
View File
@@ -98,6 +98,7 @@ section {
@import "components/contributor-profile";
@import "components/expense-list";
@import "components/external-account-link";
@import "components/main-navigation-menu";
@import "components/loading-spinner";
@import "components/reimbursement-list";
@import "components/topbar";
+10 -1
View File
@@ -16,8 +16,12 @@ section#funds {
}
th, td {
font-size: 1.2rem;
vertical-align: text-bottom;
img {
max-height: 1.5rem;
max-width: 1.5rem;
}
}
th {
@@ -35,8 +39,13 @@ section#funds {
}
&.fiat-amount {
font-size: 1.2rem;
color: rgba(255,255,255,0.8);
}
span.unit {
font-size: 1.5rem;
}
}
}
}
+17 -19
View File
@@ -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 {
font-size: 1.2rem;
font-weight: normal;
line-height: 2rem;
}
p {
line-height: 2rem;
.amount {
justify-self: end;
}
&.description {
font-size: 1rem;
opacity: 0.7;
.description {
font-size: 1rem;
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;
}
}
}
}
@@ -31,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;
}
}
}
}
+1
View File
@@ -13,6 +13,7 @@
h1 {
display: inline-block;
text-transform: uppercase;
font-weight: bold;
a {
color: inherit;
+78
View File
@@ -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>
+3 -1
View File
@@ -1,6 +1,8 @@
<header id="topbar">
<h1><LinkTo @route="dashboard">Kosmos Kredits</LinkTo></h1>
<h1><LinkTo @route="dashboard">Kredits</LinkTo></h1>
<TopbarAccountPanel />
</header>
<MainNavigationMenu />
{{outlet}}
+11 -19
View File
@@ -1,22 +1,6 @@
<main id="budget">
<div id="aside">
<!-- <section id="main&#45;nav"> -->
<!-- <header> -->
<!-- <h2>Budget</h2> -->
<!-- </header> -->
<!-- <div class="content"> -->
<!-- <ul> -->
<!-- <li> -->
<!-- <LinkTo @route="budget.expenses">Expenses</LinkTo> -->
<!-- </li> -->
<!-- <li> -->
<!-- <a href="#">Rewards</a> -->
<!-- </li> -->
<!-- </ul> -->
<!-- </div> -->
<!-- </section> -->
<section id="funds">
<header>
<h2>Community funds</h2>
@@ -33,7 +17,11 @@
<header class="with-nav">
<h2>Proposed Reimbursements</h2>
<nav>
<LinkTo @route="reimbursements.new" @title="Submit a reimbursement" class="button small green">add</LinkTo>
<button type="button" class="button small green"
title="Submit a reimbursement"
{{on "click" this.addReimbursement}}>
add
</button>
</nav>
</header>
<div class="content">
@@ -45,9 +33,13 @@
{{#if this.reimbursementsConfirmed}}
<section id="expenses-confirmed">
<header class="with-nav">
<h2>Confirmed Expenses</h2>
<h2>Confirmed Reimbursements</h2>
<nav>
<LinkTo @route="reimbursements.new" @title="Submit a reimbursement" class="button small green">add</LinkTo>
<button type="button" class="button small green"
title="Submit a reimbursement"
{{on "click" this.addReimbursement}}>
add
</button>
</nav>
</header>
<div class="content">
+1
View File
@@ -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

+54 -40
View File
@@ -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"}}>
<div id="stats">
@@ -45,14 +29,16 @@
</div>
</section>
<section id="contributions-by-type">
<header>
<h2>Contributions by type</h2>
</header>
<div class="content">
<ChartContributionsByType @contributions={{this.contributions}} />
</div>
</section>
{{#if this.contributionsConfirmed}}
<section id="contributions-by-type">
<header>
<h2>Contributions by type</h2>
</header>
<div class="content">
<ChartContributionsByType @contributions={{this.contributions}} />
</div>
</section>
{{/if}}
</div>
<div id="contributions">
@@ -87,24 +73,52 @@
</section>
{{/if}}
<section id="contributions-confirmed">
<header class="with-nav">
<h2>Confirmed Contributions</h2>
<nav>
<button type="button"
onclick={{action "toggleQuickFilterConfirmed"}}
class="small {{if this.showQuickFilterConfirmed "active"}}">
filter
{{#if this.contributionsConfirmed}}
<section id="contributions-confirmed">
<header class="with-nav">
<h2>Confirmed Contributions</h2>
<nav>
<button type="button"
onclick={{action "toggleQuickFilterConfirmed"}}
class="small {{if this.showQuickFilterConfirmed "active"}}">
filter
</button>
</nav>
</header>
<div class="content">
<ContributionList @contributions={{this.contributionsConfirmedSorted}}
@vetoContribution={{action "vetoContribution"}}
@selectedContributionId={{this.selectedContributionId}}
@showQuickFilter={{this.showQuickFilterConfirmed}} />
</div>
</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>
</nav>
</header>
<div class="content">
<ContributionList @contributions={{this.contributionsConfirmedSorted}}
@vetoContribution={{action "vetoContribution"}}
@selectedContributionId={{this.selectedContributionId}}
@showQuickFilter={{this.showQuickFilterConfirmed}} />
</div>
</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 id="details">
@@ -31,7 +31,7 @@
<div class="actions">
<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}}
<a href="{{this.ipfsGatewayUrl}}/{{this.model.ipfsHash}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect IPFS profile</a>
{{/if}}
+30
View File
@@ -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>
+2 -1
View File
@@ -7,7 +7,8 @@
</p>
<p>
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 class="actions">
<LinkTo @route="dashboard" class="button small">Return to dashboard</LinkTo>
-24
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
export default function() {
return (navigator.languages && navigator.languages.length) ?
navigator.languages[0] :
navigator.language;
}
+29
View 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
View File
@@ -26,11 +26,11 @@ module.exports = function(environment) {
},
web3ProviderUrl: 'https://rsk-testnet.kosmos.org',
web3RequiredChainId: 31,
web3RequiredNetworkName: 'RSK Testnet',
web3ChainId: 31,
web3NetworkName: 'RSK Testnet',
githubConnectUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/connect/github',
githubSignupUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/github',
githubConnectUrl: 'https://hal8000.kosmos.chat/kredits/signup/connect/github',
githubSignupUrl: 'https://hal8000.kosmos.chat/kredits/signup/github',
ipfs: {
host: 'ipfs.kosmos.org',
@@ -45,7 +45,22 @@ module.exports = function(environment) {
'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='
};
@@ -66,6 +81,10 @@ module.exports = function(environment) {
protocol: 'http',
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') {
@@ -82,7 +101,12 @@ module.exports = function(environment) {
if (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;
+3
View File
@@ -29,6 +29,9 @@ module.exports = function(defaults) {
path: "empty" // needed for kosmos-schemas dependency
}
}
},
fingerprint: {
exclude: [ 'img/icon-btc' ]
}
});
+20 -13
View File
@@ -1,12 +1,12 @@
{
"name": "kredits-web",
"version": "2.0.0-beta.2",
"version": "2.2.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "kredits-web",
"version": "2.0.0-beta.2",
"version": "2.2.1",
"license": "MIT",
"devDependencies": {
"@babel/eslint-parser": "^7.19.1",
@@ -14,7 +14,7 @@
"@ember/optional-features": "^1.3.0",
"@glimmer/component": "^1.0.0",
"@glimmer/tracking": "^1.0.0",
"@kredits/contracts": "^7.0.1",
"@kredits/contracts": "^7.3.0",
"babel-preset-es2015": "^6.22.0",
"broccoli-asset-rev": "^3.0.0",
"ember-auto-import": "^1.12.1",
@@ -3067,9 +3067,9 @@
"license": "MIT"
},
"node_modules/@kredits/contracts": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.0.1.tgz",
"integrity": "sha512-4IhNQoPc1BCT63NKyDOmV+ryb82FKNbIqfDyTdant6WTI9lfkIAnI8Et+ynQmqxx3Efez5hqoDZtEhCijcuIng==",
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.3.0.tgz",
"integrity": "sha512-7Bo2LFEqGIK39G54AStas1RWouBOp+r7Vcv2vP1uLbY01Usea8SHvd2OjLP6nFgsQa0n/rto9oremHv5IAylrQ==",
"dev": true,
"dependencies": {
"@kosmos/schemas": "^3.1.0",
@@ -6816,7 +6816,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001332",
"version": "1.0.30001599",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz",
"integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==",
"dev": true,
"funding": [
{
@@ -6826,9 +6828,12 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
]
},
"node_modules/capture-exit": {
"version": "2.0.0",
@@ -26066,9 +26071,9 @@
"dev": true
},
"@kredits/contracts": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.0.1.tgz",
"integrity": "sha512-4IhNQoPc1BCT63NKyDOmV+ryb82FKNbIqfDyTdant6WTI9lfkIAnI8Et+ynQmqxx3Efez5hqoDZtEhCijcuIng==",
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.3.0.tgz",
"integrity": "sha512-7Bo2LFEqGIK39G54AStas1RWouBOp+r7Vcv2vP1uLbY01Usea8SHvd2OjLP6nFgsQa0n/rto9oremHv5IAylrQ==",
"dev": true,
"requires": {
"@kosmos/schemas": "^3.1.0",
@@ -29050,7 +29055,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001332",
"version": "1.0.30001599",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz",
"integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==",
"dev": true
},
"capture-exit": {
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "kredits-web",
"version": "2.0.0-beta.2",
"version": "2.2.1",
"private": true,
"description": "Contribution dashboard of the Kosmos project",
"repository": "https://github.com/67P/kredits-web",
@@ -17,12 +17,11 @@
"start": "ember serve",
"test": "npm-run-all lint:* 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-prod": "rm -rf release/* && ember build -prod --output-path release",
"preversion": "npm test",
"version": "npm run build-prod && git add release/",
"postversion": "git push origin master",
"deploy": "git push 5apps master"
},
"devDependencies": {
@@ -31,7 +30,7 @@
"@ember/optional-features": "^1.3.0",
"@glimmer/component": "^1.0.0",
"@glimmer/tracking": "^1.0.0",
"@kredits/contracts": "^7.0.1",
"@kredits/contracts": "^7.3.0",
"babel-preset-es2015": "^6.22.0",
"broccoli-asset-rev": "^3.0.0",
"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
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

+4 -4
View File
@@ -8,10 +8,10 @@
<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-beta.2%2B62b618b6%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.1%2Bb32f805c%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/kredits-web-429511daa3257050f23812f202a534ee.css">
<link integrity="" rel="stylesheet" href="/assets/kredits-web-1ce689f6aada92a7d79a1947300931cf.css">
@@ -24,8 +24,8 @@
<body>
<script src="/assets/vendor-4ee536ded971436a83d35253df25f2cb.js" integrity="sha256-yb+pC9iuZ8WcQwk9VuDNhSlWguL7BEABsEOEhBpwUC0= sha512-50LApx5XqeKHu/FMaljzqhAMPgkqSiy2rDhUcViYBcXWBBYmilFirup/iWvLiNCdfr5ct9wk7+tU9iShdSgDww==" ></script>
<script src="/assets/kredits-web-0a433c90d05ea3903308867e0d178f7f.js" integrity="sha256-kU8qbqT24o1tZj4xdYJzarfPFSfEmZr3JLu0g8pAmqk= sha512-gD6YmV3xDnwHJ2WU6yXNkUEiPScjdfM2S5eYwCrsfcsI4iJUG81z7IjUbwzstuZocKkCS5zt8Y84Hh84SdhSuw==" ></script>
<script src="/assets/vendor-4d87b3e0995c5bb18e46836a089900a4.js" integrity="sha256-5WmWY05Kd+n98Sw0GwuaWodmDVBZ2yVGGtC/2owVfQE= sha512-tiGPA5zvfhnSC9LPTmpvXJmOu30yhsdPqSiZJMPGmE/ez/xJLMaM29vjs4Cs74cDf6fX4uYSGRIu1KfOyGpJYA==" ></script>
<script src="/assets/kredits-web-71e8e218f4178df50b78058520257c0d.js" integrity="sha256-pQsThEeIoXS7h22Jg3XPg+5l268k7S63rhGhhHC10o8= sha512-CV9qAyQL3jdXW4+DmTdaGFtGYZ9Lz+f4+8aUnQyK+wOeDkLpUultyhhF2P1+wHS6Hm2vj7n213Ef89YblxDgUA==" ></script>
</body>
@@ -1,13 +1,58 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { click, fillIn, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import contributors from '../../../fixtures/contributors';
module('Integration | Component | add-reimbursement', function(hooks) {
setupRenderingTest(hooks);
test('it works', async function(assert) {
hooks.beforeEach(function() {
let kredits = this.owner.lookup('service:kredits');
kredits.set('contributors', contributors);
kredits.set('currentUser', contributors.findBy('id', 3));
});
test('Contributor select menu', async function(assert) {
await render(hbs`<AddReimbursement />`);
assert.ok(true);
assert.equal(this.element.querySelectorAll('select#contributor option').length, contributors.length,
'contains correct amount of items');
assert.equal(this.element.querySelector('select#contributor option:checked').value, "3",
'preselects the connected contributor account');
});
test('Adding expense items', async function(assert) {
await render(hbs`<AddReimbursement />`);
assert.dom(this.element).includesText('New expense item');
await fillIn('form input[name="expense-amount"]', '49');
await fillIn('form input[name="expense-title"]', 'Domain kosmos.org (yearly fee)');
await click('form#add-expense-item input[type="submit"]');
assert.equal(this.element.querySelector('input[name="total-eur"]').value, '49',
'updates the total EUR amount');
assert.equal(this.element.querySelector('input[name="total-usd"]').value, '0',
'does not update the total USD amount');
assert.equal(this.element.querySelector('input[name="total-btc"]').value, '0.00534493',
'updates the total BTC amount');
assert.dom(this.element).doesNotIncludeText('New expense item');
await click('button#add-another-item');
await fillIn('form input[name="expense-amount"]', '29');
await fillIn('select[name="expense-currency"]', 'USD');
await fillIn('form input[name="expense-title"]', 'Domain kosmos.social (yearly fee)');
await click('form#add-expense-item input[type="submit"]');
assert.equal(this.element.querySelector('input[name="total-usd"]').value, '29',
'updates the total USD amount');
assert.equal(this.element.querySelector('input[name="total-eur"]').value, '49',
'does not update the total EUR amount');
assert.equal(this.element.querySelector('input[name="total-btc"]').value, '0.00804268',
'updates the total BTC amount');
});
});
@@ -3,37 +3,90 @@ import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
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) {
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 />`);
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');
service.set('currentUserAccounts', [{ foo: 'bar' }]);
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');
service.set('currentUserAccounts', [{ foo: 'bar' }]);
service.set('currentUser', {
name: 'Dorian Nakamoto',
isCore: false
});
service.set('currentUser', { name: 'Dorian Nakamoto', isCore: false });
await render(hbs`<TopbarAccountPanel />`);
assert.dom(this.element).hasText('Dorian Nakamoto');
service.set('currentUser.isCore', true);
await render(hbs`<TopbarAccountPanel />`);
assert.equal(this.element.querySelectorAll('span.core-flag').length, 1);
assert.dom(this.element).hasText(
'Dorian Nakamoto', 'shows current user\'s name'
);
assert.equal(
this.element.querySelectorAll('button#signup').length, 0,
'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);
})
});
@@ -1,11 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | signup/eth-account', function(hooks) {
module('Unit | Route | signup/account', function(hooks) {
setupTest(hooks);
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);
});
});