Browse Source

add ability to give to fulfill an offer; adjust visibility of claim actions

kb/add-usage-guide
Trent Larson 11 months ago
parent
commit
acaaf8776d
  1. 6
      README.md
  2. 13
      project.task.yaml
  3. 32
      src/components/GiftedDialog.vue
  4. 15
      src/libs/endorserServer.ts
  5. 162
      src/views/ClaimView.vue
  6. 6
      src/views/ContactsView.vue
  7. 20
      src/views/ProjectViewView.vue

6
README.md

@ -20,8 +20,6 @@ npm run lint
### Compiles and minifies for production ### Compiles and minifies for production
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
* Update the project.task.yaml & CHANGELOG.md & the version in package.json, run `npm install`, and commit. * Update the project.task.yaml & CHANGELOG.md & the version in package.json, run `npm install`, and commit.
* [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) * [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
@ -44,7 +42,7 @@ If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js,
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari` * `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
* Revert src/constants/app.ts and package.json (if that was prod), edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production. * Revert src/constants/app.ts and package.json (if that was prod), edit package.json to increment version & add "-beta", `npm install`, and commit. Tag if you didn't before. Also record what version is on production.
@ -135,6 +133,8 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
* [Customize Vue configuration](https://cli.vuejs.org/config/). * [Customize Vue configuration](https://cli.vuejs.org/config/).
* If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
### Kudos ### Kudos

13
project.task.yaml

@ -1,13 +1,14 @@
tasks: tasks:
- update dependencies, especially Veramo
- record donations vs gives - record donations vs gives
- deploy & migrate - deploy & migrate (prod)
- in mobile - change give & fulfills to array of objects?
- update docs - update docs
- check that 'show more contacts' from the contact-give-list on the project screen includes project ID
- on ClaimView, the "ask someone" should refer to "visible" IDs, or to confirmations only if confirmations are visible
- "send them to this page" on ClaimView should be a link for installed app
- show VC details... somehow: - show VC details... somehow:
- 01 show my VCs - most interesting, or via search - 01 show my VCs - most interesting, or via search
- 04 allow user to download & prove chains of VCs, mine + ones I can see about me from others - 04 allow user to download & prove chains of VCs, mine + ones I can see about me from others
@ -15,7 +16,9 @@ tasks:
- on gives feed - link to project - on gives feed - link to project
- show feed of offers, new projects, etc -- maybe limited to my search area - show feed of offers, new projects, etc -- maybe limited to my search area
- revenue - update Veramo library
- revenue to support server operation
- copy button for seed - copy button for seed
- .5 If notifications are not enabled, add message to front page with link/button to enable - .5 If notifications are not enabled, add message to front page with link/button to enable

32
src/components/GiftedDialog.vue

@ -67,10 +67,15 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { createAndSubmitGive, GiverInputInfo } from "@/libs/endorserServer"; import {
createAndSubmitGive,
didInfo,
GiverInputInfo,
} from "@/libs/endorserServer";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
interface Notification { interface Notification {
group: string; group: string;
@ -85,9 +90,12 @@ export default class GiftedDialog extends Vue {
@Prop message = ""; @Prop message = "";
@Prop projectId = ""; @Prop projectId = "";
@Prop offerId = "";
@Prop showGivenToUser = false; @Prop showGivenToUser = false;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
amountInput = "0"; amountInput = "0";
@ -122,9 +130,16 @@ export default class GiftedDialog extends Vue {
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings from database:", err); console.error("Error retrieving settings from database:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -140,6 +155,14 @@ export default class GiftedDialog extends Vue {
open(giver: GiverInputInfo) { open(giver: GiverInputInfo) {
this.description = ""; this.description = "";
this.giver = giver; this.giver = giver;
if (!this.giver.name) {
this.giver.name = didInfo(
this.giver.did,
this.activeDid,
this.allMyDids,
this.allContacts,
);
}
// if we show "given to user" selection, default checkbox to true // if we show "given to user" selection, default checkbox to true
this.givenToUser = this.showGivenToUser; this.givenToUser = this.showGivenToUser;
this.amountInput = "0"; this.amountInput = "0";
@ -271,6 +294,7 @@ export default class GiftedDialog extends Vue {
amountInput, amountInput,
unitCode, unitCode,
this.projectId, this.projectId,
this.offerId,
this.isTrade, this.isTrade,
); );
@ -279,7 +303,7 @@ export default class GiftedDialog extends Vue {
this.isGiveCreationError(result.response) this.isGiveCreationError(result.response)
) { ) {
const errorMessage = this.getGiveCreationErrorMessage(result); const errorMessage = this.getGiveCreationErrorMessage(result);
console.log("Error with give creation result:", result); console.error("Error with give creation result:", result);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -302,7 +326,7 @@ export default class GiftedDialog extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
console.log("Error with give recordation caught:", error); console.error("Error with give recordation caught:", error);
const message = const message =
error.userMessage || error.userMessage ||
error.response?.data?.error?.message || error.response?.data?.error?.message ||

15
src/libs/endorserServer.ts

@ -52,6 +52,7 @@ export interface GenericServerRecord extends GenericVerifiableCredential {
issuer?: string; issuer?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
claim: Record<any, any>; claim: Record<any, any>;
claimType?: string;
} }
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = { export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
"@context": SCHEMA_ORG_CONTEXT, "@context": SCHEMA_ORG_CONTEXT,
@ -259,8 +260,8 @@ export function removeVisibleToDids(input: any): any {
Similar logic is found in endorser-mobile. Similar logic is found in endorser-mobile.
**/ **/
export function didInfo( export function didInfo(
did: string, did: string | undefined,
activeDid: string, activeDid: string | undefined,
allMyDids: string[], allMyDids: string[],
contacts: Contact[], contacts: Contact[],
): string { ): string {
@ -312,6 +313,7 @@ export async function createAndSubmitGive(
hours?: number, hours?: number,
unitCode?: string, unitCode?: string,
fulfillsProjectHandleId?: string, fulfillsProjectHandleId?: string,
fulfillsOfferHandleId?: string,
isTrade: boolean = false, isTrade: boolean = false,
): Promise<CreateAndSubmitClaimResult> { ): Promise<CreateAndSubmitClaimResult> {
const vcClaim: GiveVerifiableCredential = { const vcClaim: GiveVerifiableCredential = {
@ -332,6 +334,13 @@ export async function createAndSubmitGive(
identifier: fulfillsProjectHandleId, identifier: fulfillsProjectHandleId,
}); });
} }
if (fulfillsOfferHandleId) {
vcClaim.fulfills = vcClaim.fulfills || []; // weird that it won't typecheck without this
vcClaim.fulfills.push({
"@type": "Offer",
identifier: fulfillsOfferHandleId,
});
}
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericServerRecord, vcClaim as GenericServerRecord,
identity, identity,
@ -437,7 +446,7 @@ export async function createAndSubmitClaim(
return { type: "success", response }; return { type: "success", response };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
console.log("Error creating claim:", error); console.error("Error creating claim:", error);
const errorMessage: string = const errorMessage: string =
error.response?.data?.error?.message || error.message || "Unknown error"; error.response?.data?.error?.message || error.message || "Unknown error";

162
src/views/ClaimView.vue

@ -17,34 +17,55 @@
</div> </div>
<!-- Details --> <!-- Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="h-32 bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<div> <div class="block flex gap-4 overflow-hidden">
<div class="block flex gap-4 overflow-hidden"> <div class="overflow-hidden">
<div class="overflow-hidden"> <h2 class="text-md font-bold">{{ veriClaim.id }}</h2>
<h2 class="text-md font-bold">{{ veriClaim.id }}</h2> <div class="text-sm">
<div class="text-sm"> <div>
<div> {{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
{{ veriClaim.claimType }} </div>
</div> <div>
<div> <fa icon="message" class="fa-fw text-slate-400"></fa>
<fa icon="message" class="fa-fw text-slate-400"></fa> {{ veriClaim.claim?.description }}
{{ veriClaim.claim?.description }} </div>
</div> <div>
<div> <fa icon="user" class="fa-fw text-slate-400"></fa>
<fa icon="user" class="fa-fw text-slate-400"></fa> {{ veriClaim.issuer }}
{{ veriClaim.issuer }} </div>
</div> <div>
<div> <fa icon="calendar" class="fa-fw text-slate-400"></fa>
<fa icon="calendar" class="fa-fw text-slate-400"></fa> {{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div> <div class="h-6 columns-3">
<button
class="col-span-1 bg-blue-600 text-white px-4 py-2 rounded-md"
v-if="userCanConfirm()"
@click="confirmClaim(veriClaim.id)"
>
Confirm
</button>
<button
v-if="canFulfillOffer()"
@click="openGiftDialog()"
class="col-span-1 block w-fit text-center text-md bg-blue-600 text-white px-1.5 py-2 rounded-md"
>
Record Some Delivered
</button>
</div>
<GiftedDialog
ref="customGiveDialog"
message="Offer fulfilled by"
:offerId="veriClaim.handleId"
/>
<div v-if="isConfirmable()">
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Confirmations</h2> <h2 class="font-bold uppercase text-xl mt-8 mb-2">Confirmations</h2>
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span> <span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
@ -71,7 +92,7 @@
</div> </div>
<div v-if="confirmerIdList.length > 0"> <div v-if="confirmerIdList.length > 0">
The following people have issued or confirmed this claim. The following people have issued or confirmed this claim.
<ul> <ul class="ml-4">
<li <li
v-for="confirmerId in confirmerIdList" v-for="confirmerId in confirmerIdList"
:key="confirmerId" :key="confirmerId"
@ -98,7 +119,7 @@
<div v-if="confsVisibleToIdList.length > 0"> <div v-if="confsVisibleToIdList.length > 0">
The following people can connect you with people who have issued or The following people can connect you with people who have issued or
confirmed this claim. confirmed this claim.
<ul> <ul class="ml-4">
<li <li
v-for="confsVisibleTo in confsVisibleToIdList" v-for="confsVisibleTo in confsVisibleToIdList"
:key="confsVisibleTo" :key="confsVisibleTo"
@ -116,27 +137,22 @@
</div> </div>
</div> </div>
<div class="mt-4"> <!-- explain if user cannot confirm -->
<div v-if="confirmerIdList.includes(activeDid)"> <!-- Note that these conditions are mirrored in userCanConfirm(). -->
You have confirmed this claim. <div v-if="confirmerIdList.includes(activeDid)">
</div> You have confirmed this claim.
<div v-else-if="containsHiddenDid(veriClaim.claim)"> </div>
You cannot confirm this claim because it contains data that is hidden <div v-else-if="veriClaim.issuer == activeDid">
from you. You cannot confirm this because you issued this claim, so you already
</div> count as confirming it.
<div v-else> </div>
<button <div v-else-if="containsHiddenDid(veriClaim.claim)">
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4" You cannot confirm this because it contains hidden identifiers.
@click="confirmClaim(veriClaim.id)"
>
Confirm Claim
</button>
</div>
</div> </div>
</div> </div>
<div> <div>
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Claim</h2> <h2 class="font-bold uppercase text-xl mt-8 mb-2">Visible Details</h2>
<pre <pre
class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md" class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"
>{{ veriClaimDump }}</pre >{{ veriClaimDump }}</pre
@ -192,6 +208,7 @@ import * as serverUtil from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { GiverInputInfo } from "@/libs/endorserServer";
interface Notification { interface Notification {
group: string; group: string;
@ -210,9 +227,9 @@ export default class ClaimView extends Vue {
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
apiServer = ""; apiServer = "";
confirmerIdList = []; // list of DIDs that have confirmed this claim excluding the issuer confirmerIdList: string[] = []; // list of DIDs that have confirmed this claim excluding the issuer
confsVisibleErrorMessage = ""; confsVisibleErrorMessage = "";
confsVisibleToIdList = []; // list of DIDs that can see any confirmer confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
fullClaim = null; fullClaim = null;
fullClaimDump = ""; fullClaimDump = "";
fullClaimMessage = ""; fullClaimMessage = "";
@ -242,7 +259,7 @@ export default class ClaimView extends Vue {
let claimId; let claimId;
if (pathParam) { if (pathParam) {
claimId = decodeURIComponent(pathParam); claimId = decodeURIComponent(pathParam);
this.loadClaim(claimId, identity); await this.loadClaim(claimId, identity);
} else { } else {
this.$notify( this.$notify(
{ {
@ -256,6 +273,50 @@ export default class ClaimView extends Vue {
} }
} }
// insert a space before any capital letters except the initial letter
// (and capitalize initial letter, just in case)
capitalizeAndInsertSpacesBeforeCaps(text: string) {
return !text
? ""
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
}
isConfirmable() {
return this.veriClaim.claimType === "GiveAction";
}
userCanConfirm() {
// Note that this logic is mirrored in the template. Look for "userCanConfirm"
return (
this.isConfirmable() &&
!this.confirmerIdList.includes(this.activeDid) &&
this.veriClaim.issuer !== this.activeDid &&
!this.containsHiddenDid(this.veriClaim.claim)
);
}
offerGiverDid(): string | undefined {
let giver;
if (
this.veriClaim.claim.offeredBy?.identifier &&
!serverUtil.isHiddenDid(
this.veriClaim.claim.offeredBy.identifier as string,
)
) {
giver = this.veriClaim.claim.offeredBy.identifier;
} else if (
this.veriClaim.issuer &&
!serverUtil.isHiddenDid(this.veriClaim.issuer)
) {
giver = this.veriClaim.issuer;
}
return giver;
}
canFulfillOffer() {
return this.veriClaim.claimType === "Offer" && this.offerGiverDid();
}
totalConfirmers() { totalConfirmers() {
return ( return (
this.numConfsNotVisible + this.numConfsNotVisible +
@ -313,7 +374,7 @@ export default class ClaimView extends Vue {
this.veriClaimDump = yaml.dump(this.veriClaim); this.veriClaimDump = yaml.dump(this.veriClaim);
} else { } else {
// actually, axios typically throws an error so we never get here // actually, axios typically throws an error so we never get here
console.log("Error getting claim:", resp); console.error("Error getting claim:", resp);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -393,7 +454,7 @@ export default class ClaimView extends Vue {
this.fullClaimDump = yaml.dump(this.fullClaim); this.fullClaimDump = yaml.dump(this.fullClaim);
} else { } else {
// actually, axios typically throws an error so we never get here // actually, axios typically throws an error so we never get here
console.log("Error getting full claim:", resp); console.error("Error getting full claim:", resp);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -466,7 +527,7 @@ export default class ClaimView extends Vue {
5000, 5000,
); );
} else { } else {
console.log("Got error submitting the confirmation:", result); console.error("Got error submitting the confirmation:", result);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -479,5 +540,12 @@ export default class ClaimView extends Vue {
} }
} }
} }
openGiftDialog() {
const giver: GiverInputInfo = {
did: this.offerGiverDid(),
};
(this.$refs.customGiveDialog as GiftedDialog).open(giver);
}
} }
</script> </script>

6
src/views/ContactsView.vue

@ -992,7 +992,7 @@ export default class ContactsView extends Vue {
"?", "?",
) )
) { ) {
this.createAndSubmitGive( this.createAndSubmitContactGive(
identity, identity,
fromDid, fromDid,
toDid, toDid,
@ -1004,7 +1004,7 @@ export default class ContactsView extends Vue {
} }
// similar function is in endorserServer.ts // similar function is in endorserServer.ts
private async createAndSubmitGive( private async createAndSubmitContactGive(
identity: IIdentifier, identity: IIdentifier,
fromDid: string, fromDid: string,
toDid: string, toDid: string,
@ -1073,7 +1073,7 @@ export default class ContactsView extends Vue {
} }
} }
} catch (error) { } catch (error) {
console.log("Error in createAndSubmitGive: ", error); console.log("Error in createAndSubmitContactGive: ", error);
let userMessage = "There was an error. See logs for more info."; let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError; const serverError = error as AxiosError;
if (serverError) { if (serverError) {

20
src/views/ProjectViewView.vue

@ -94,13 +94,14 @@
<div v-if="activeDid" class="mb-4"> <div v-if="activeDid" class="mb-4">
<div class="text-center"> <div class="text-center">
<button <button
@click="openOfferDialog({ name: 'you', did: activeDid })" @click="openOfferDialog()"
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md" class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
> >
I offer&hellip; I offer&hellip;
</button> </button>
</div> </div>
</div> </div>
<OfferDialog ref="customOfferDialog" :projectId="this.projectId" />
<div v-if="activeDid"> <div v-if="activeDid">
<div class="text-center"> <div class="text-center">
@ -112,6 +113,12 @@
</button> </button>
<p class="mt-2 mb-4 text-center">Or, record a contribution from:</p> <p class="mt-2 mb-4 text-center">Or, record a contribution from:</p>
</div> </div>
<GiftedDialog
ref="customGiveDialog"
message="Received from"
:projectId="this.projectId"
>
</GiftedDialog>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5"> <ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openGiftDialog()"> <li @click="openGiftDialog()">
@ -267,15 +274,6 @@
</div> </div>
</div> </div>
</div> </div>
<GiftedDialog
ref="customGiveDialog"
message="Received from"
:projectId="this.projectId"
>
</GiftedDialog>
<OfferDialog ref="customOfferDialog" :projectId="this.projectId">
</OfferDialog>
</section> </section>
</template> </template>
@ -434,7 +432,7 @@ export default class ProjectViewView extends Vue {
this.url = resp.data.claim?.url || ""; this.url = resp.data.claim?.url || "";
} else { } else {
// actually, axios throws an error on 404 so we probably never get here // actually, axios throws an error on 404 so we probably never get here
console.log("Error getting project:", resp); console.error("Error getting project:", resp);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

Loading…
Cancel
Save