sllow quick gifting all the way to the server, maybe with hours
This commit is contained in:
@@ -1,17 +1,41 @@
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h1>Received from {{ contact?.name || "nobody in particular" }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
<h1 class="text-lg text-center">
|
||||
Received from {{ contact?.name || "nobody in particular" }}
|
||||
</h1>
|
||||
<p class="py-2">{{ message }}</p>
|
||||
<input
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
placeholder="What you received"
|
||||
v-model="description"
|
||||
/>
|
||||
<button @click="confirm">Confirm</button>
|
||||
|
||||
<button @click="cancel">Cancel</button>
|
||||
<div class="flex flex-row">
|
||||
<span class="py-4">Hours</span>
|
||||
<input
|
||||
type="text"
|
||||
class="block w-8 rounded border border-slate-400 ml-4 text-center"
|
||||
v-model="hours"
|
||||
/>
|
||||
<div class="flex flex-col px-1">
|
||||
<div>
|
||||
<fa icon="square-caret-up" size="2xl" @click="increment()" />
|
||||
</div>
|
||||
<div>
|
||||
<fa icon="square-caret-down" size="2xl" @click="decrement()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button class="rounded border border-slate-400" @click="confirm">
|
||||
<span class="m-2">Confirm</span>
|
||||
</button>
|
||||
|
||||
<button class="rounded border border-slate-400" @click="cancel">
|
||||
<span class="m-2">Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -23,6 +47,7 @@ export default {
|
||||
return {
|
||||
contact: null,
|
||||
description: "",
|
||||
hours: "0",
|
||||
visible: false,
|
||||
};
|
||||
},
|
||||
@@ -34,11 +59,18 @@ export default {
|
||||
close() {
|
||||
this.visible = false;
|
||||
},
|
||||
increment() {
|
||||
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
|
||||
},
|
||||
decrement() {
|
||||
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
|
||||
},
|
||||
confirm() {
|
||||
this.close();
|
||||
this.$emit("dialog-result", {
|
||||
action: "confirm",
|
||||
contact: this.contact,
|
||||
hours: parseFloat(this.hours),
|
||||
description: this.description,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
import * as R from "ramda";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||
import * as didJwt from "did-jwt";
|
||||
import { Axios, AxiosResponse } from "axios";
|
||||
|
||||
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
||||
export const SERVICE_ID = "endorser.ch";
|
||||
|
||||
export interface AgreeVerifiableCredential {
|
||||
"@context": string;
|
||||
"@type": string;
|
||||
// "any" because arbitrary objects can be subject of agreement
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
object: Record<any, any>;
|
||||
}
|
||||
|
||||
export interface ClaimResult {
|
||||
success: { claimId: string; handleId: string };
|
||||
error: { code: string; message: string };
|
||||
}
|
||||
|
||||
export interface GenericClaim {
|
||||
"@context": string;
|
||||
"@type": string;
|
||||
@@ -12,14 +29,6 @@ export interface GenericClaim {
|
||||
claim: Record<any, any>;
|
||||
}
|
||||
|
||||
export interface AgreeVerifiableCredential {
|
||||
"@context": string;
|
||||
"@type": string;
|
||||
// "any" because arbitrary objects can be subject of agreement
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
object: Record<any, any>;
|
||||
}
|
||||
|
||||
export interface GiveServerRecord {
|
||||
agentDid: string;
|
||||
amount: number;
|
||||
@@ -35,10 +44,10 @@ export interface GiveServerRecord {
|
||||
export interface GiveVerifiableCredential {
|
||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||
"@type": string;
|
||||
agent: { identifier: string };
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
identifier?: string;
|
||||
object: { amountOfThisGood: number; unitCode: string };
|
||||
object?: { amountOfThisGood: number; unitCode: string };
|
||||
recipient: { identifier: string };
|
||||
}
|
||||
|
||||
@@ -50,6 +59,11 @@ export interface RegisterVerifiableCredential {
|
||||
recipient: { identifier: string };
|
||||
}
|
||||
|
||||
export interface InternalError {
|
||||
error: string; // for system logging
|
||||
userMessage?: string; // for user display
|
||||
}
|
||||
|
||||
// This is used to check for hidden info.
|
||||
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
||||
const HIDDEN_DID = "did:none:HIDDEN";
|
||||
@@ -69,6 +83,8 @@ export function didInfo(did, identifiers, contacts) {
|
||||
const contact = R.find((c) => c.did === did, contacts);
|
||||
if (contact) {
|
||||
return contact.name || "Someone Unnamed in Contacts";
|
||||
} else if (!did) {
|
||||
return "Unpecified Person";
|
||||
} else if (isHiddenDid(did)) {
|
||||
return "Someone Not In Network";
|
||||
} else {
|
||||
@@ -76,3 +92,91 @@ export function didInfo(did, identifiers, contacts) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For result, see https://endorser.ch:3000/api-docs/#/claims/post_api_v2_claim
|
||||
|
||||
* @param identity
|
||||
* @param fromDid may be null
|
||||
* @param toDid
|
||||
* @param description may be null; should have this or hours
|
||||
* @param hours may be null; should have this or description
|
||||
*/
|
||||
export async function createAndSubmitGive(
|
||||
axios: Axios,
|
||||
apiServer: string,
|
||||
identity: IIdentifier,
|
||||
fromDid: string,
|
||||
toDid: string,
|
||||
description: string,
|
||||
hours: number
|
||||
): Promise<AxiosResponse<ClaimResult> | InternalError> {
|
||||
// Make a claim
|
||||
const vcClaim: GiveVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "GiveAction",
|
||||
recipient: { identifier: toDid },
|
||||
};
|
||||
if (fromDid) {
|
||||
vcClaim.agent = { identifier: fromDid };
|
||||
}
|
||||
if (description) {
|
||||
vcClaim.description = description;
|
||||
}
|
||||
if (hours) {
|
||||
vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
|
||||
}
|
||||
// Make a payload for the claim
|
||||
const vcPayload = {
|
||||
vc: {
|
||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||
type: ["VerifiableCredential"],
|
||||
credentialSubject: vcClaim,
|
||||
},
|
||||
};
|
||||
// Create a signature using private key of identity
|
||||
if (identity.keys[0].privateKeyHex == null) {
|
||||
return new Promise<InternalError>((resolve, reject) => {
|
||||
reject({
|
||||
error: "No private key",
|
||||
message:
|
||||
"Your identifier " +
|
||||
identity.did +
|
||||
" is not configured correctly. Use a different identifier.",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
|
||||
const signer = await SimpleSigner(privateKeyHex);
|
||||
const alg = undefined;
|
||||
// Create a JWT for the request
|
||||
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
||||
alg: alg,
|
||||
issuer: identity.did,
|
||||
signer: signer,
|
||||
});
|
||||
|
||||
// Make the xhr request payload
|
||||
|
||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||
const url = apiServer + "/api/v2/claim";
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + token,
|
||||
};
|
||||
|
||||
return axios.post(url, payload, { headers });
|
||||
}
|
||||
|
||||
// from https://stackoverflow.com/a/175787/845494
|
||||
//
|
||||
export function isNumeric(str: string): boolean {
|
||||
return !isNaN(+str);
|
||||
}
|
||||
|
||||
export function numberOrZero(str: string): number {
|
||||
return isNumeric(str) ? +str : 0;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
faRotate,
|
||||
faShareNodes,
|
||||
faSpinner,
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faTrashCan,
|
||||
faUser,
|
||||
faUsers,
|
||||
@@ -69,6 +71,8 @@ library.add(
|
||||
faRotate,
|
||||
faShareNodes,
|
||||
faSpinner,
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faTrashCan,
|
||||
faUser,
|
||||
faUsers,
|
||||
|
||||
@@ -57,10 +57,14 @@
|
||||
v-for="contact in allContacts"
|
||||
:key="contact.did"
|
||||
@click="openDialog(contact)"
|
||||
style="color: blue"
|
||||
>
|
||||
{{ contact.name }},
|
||||
{{ contact.name }},
|
||||
</button>
|
||||
or
|
||||
<button @click="openDialog()" style="color: blue">
|
||||
nobody in particular
|
||||
</button>
|
||||
<button @click="openDialog()">or nobody in particular</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +75,7 @@
|
||||
>
|
||||
</GiftedDialog>
|
||||
|
||||
<div>
|
||||
<div class="py-4">
|
||||
<h1 class="text-2xl">Latest Activity</h1>
|
||||
<span :class="{ hidden: isHiddenSpinner }">
|
||||
<fa icon="spinner" class="fa-fw"></fa>
|
||||
@@ -110,7 +114,7 @@ import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||
import { db, accountsDB } from "@/db";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { didInfo } from "@/libs/endorserServer";
|
||||
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
|
||||
@@ -185,10 +189,12 @@ export default class HomeView extends Vue {
|
||||
(acc) => acc.did === this.activeDid,
|
||||
this.allAccounts
|
||||
);
|
||||
console.log("about to parse from", this.activeDid, account?.identity);
|
||||
//console.log("about to parse from", this.activeDid, account?.identity);
|
||||
const identity = JSON.parse(account?.identity || "undefined");
|
||||
const token = await accessToken(identity);
|
||||
headers["Authorization"] = "Bearer " + token;
|
||||
} else {
|
||||
// it's OK without auth... we just won't get any identifiers
|
||||
}
|
||||
return fetch(this.apiServer + "/api/v2/report/gives?" + beforeQuery, {
|
||||
method: "GET",
|
||||
@@ -240,24 +246,68 @@ export default class HomeView extends Vue {
|
||||
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
|
||||
}
|
||||
|
||||
recordGive(contact) {
|
||||
console.log("recordGive", contact);
|
||||
}
|
||||
|
||||
openDialog(contact) {
|
||||
this.$refs.customDialog.open(contact);
|
||||
}
|
||||
handleDialogResult(result) {
|
||||
if (result.action === "confirm") {
|
||||
return new Promise((resolve) => {
|
||||
console.log("Dialog confirmed: ", result.description);
|
||||
this.recordGive(result.contact, result.description, result.hours);
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
// action was "cancel"
|
||||
// action was "cancel" so do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param contact may be null
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
*/
|
||||
recordGive(contact, description, hours) {
|
||||
if (this.activeDid == null) {
|
||||
this.alertTitle = "Error";
|
||||
this.alertMessage =
|
||||
"You must select an identity before you can record a give.";
|
||||
return;
|
||||
}
|
||||
const account = R.find(
|
||||
(acc) => acc.did === this.activeDid,
|
||||
this.allAccounts
|
||||
);
|
||||
//console.log("about to parse from", this.activeDid, account?.identity);
|
||||
const identity = JSON.parse(account?.identity || "undefined");
|
||||
createAndSubmitGive(
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
identity,
|
||||
contact?.did,
|
||||
this.activeDid,
|
||||
description,
|
||||
hours
|
||||
)
|
||||
.then((result) => {
|
||||
if (result.status != 201 || result.data?.error) {
|
||||
console.log("Error with give result:", result);
|
||||
this.alertTitle = "Error";
|
||||
this.alertMessage =
|
||||
result.data?.message || "There was an error recording the give.";
|
||||
} else {
|
||||
this.alertTitle = "Success";
|
||||
this.alertMessage = "That gift was recorded.";
|
||||
//this.updateAllFeed(); // full update is overkill but we should show something
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log("Error with give caught:", e);
|
||||
this.alertTitle = "Error";
|
||||
this.alertMessage =
|
||||
e.userMessage || "There was an error recording the give.";
|
||||
});
|
||||
}
|
||||
|
||||
// This same popup code is in many files.
|
||||
alertMessage = "";
|
||||
alertTitle = "";
|
||||
|
||||
Reference in New Issue
Block a user