Browse Source

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

starred-projects
Trent Larson 8 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
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.
* [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`
* 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/).
* If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
### Kudos

13
project.task.yaml

@ -1,13 +1,14 @@
tasks:
- update dependencies, especially Veramo
- record donations vs gives
- deploy & migrate
- in mobile - change give & fulfills to array of objects?
- deploy & migrate (prod)
- 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:
- 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
@ -15,7 +16,9 @@ tasks:
- on gives feed - link to project
- 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
- .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">
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 { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
interface Notification {
group: string;
@ -85,9 +90,12 @@ export default class GiftedDialog extends Vue {
@Prop message = "";
@Prop projectId = "";
@Prop offerId = "";
@Prop showGivenToUser = false;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
amountInput = "0";
@ -122,9 +130,16 @@ export default class GiftedDialog extends Vue {
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
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
} catch (err: any) {
console.log("Error retrieving settings from database:", err);
console.error("Error retrieving settings from database:", err);
this.$notify(
{
group: "alert",
@ -140,6 +155,14 @@ export default class GiftedDialog extends Vue {
open(giver: GiverInputInfo) {
this.description = "";
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
this.givenToUser = this.showGivenToUser;
this.amountInput = "0";
@ -271,6 +294,7 @@ export default class GiftedDialog extends Vue {
amountInput,
unitCode,
this.projectId,
this.offerId,
this.isTrade,
);
@ -279,7 +303,7 @@ export default class GiftedDialog extends Vue {
this.isGiveCreationError(result.response)
) {
const errorMessage = this.getGiveCreationErrorMessage(result);
console.log("Error with give creation result:", result);
console.error("Error with give creation result:", result);
this.$notify(
{
group: "alert",
@ -302,7 +326,7 @@ export default class GiftedDialog extends Vue {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log("Error with give recordation caught:", error);
console.error("Error with give recordation caught:", error);
const message =
error.userMessage ||
error.response?.data?.error?.message ||

15
src/libs/endorserServer.ts

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

162
src/views/ClaimView.vue

@ -17,34 +17,55 @@
</div>
<!-- Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<div>
<div class="block flex gap-4 overflow-hidden">
<div class="overflow-hidden">
<h2 class="text-md font-bold">{{ veriClaim.id }}</h2>
<div class="text-sm">
<div>
{{ veriClaim.claimType }}
</div>
<div>
<fa icon="message" class="fa-fw text-slate-400"></fa>
{{ veriClaim.claim?.description }}
</div>
<div>
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{ veriClaim.issuer }}
</div>
<div>
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
</div>
<div class="h-32 bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<div class="block flex gap-4 overflow-hidden">
<div class="overflow-hidden">
<h2 class="text-md font-bold">{{ veriClaim.id }}</h2>
<div class="text-sm">
<div>
{{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
</div>
<div>
<fa icon="message" class="fa-fw text-slate-400"></fa>
{{ veriClaim.claim?.description }}
</div>
<div>
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{ veriClaim.issuer }}
</div>
<div>
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
</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>
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
@ -71,7 +92,7 @@
</div>
<div v-if="confirmerIdList.length > 0">
The following people have issued or confirmed this claim.
<ul>
<ul class="ml-4">
<li
v-for="confirmerId in confirmerIdList"
:key="confirmerId"
@ -98,7 +119,7 @@
<div v-if="confsVisibleToIdList.length > 0">
The following people can connect you with people who have issued or
confirmed this claim.
<ul>
<ul class="ml-4">
<li
v-for="confsVisibleTo in confsVisibleToIdList"
:key="confsVisibleTo"
@ -116,27 +137,22 @@
</div>
</div>
<div class="mt-4">
<div v-if="confirmerIdList.includes(activeDid)">
You have confirmed this claim.
</div>
<div v-else-if="containsHiddenDid(veriClaim.claim)">
You cannot confirm this claim because it contains data that is hidden
from you.
</div>
<div v-else>
<button
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"
@click="confirmClaim(veriClaim.id)"
>
Confirm Claim
</button>
</div>
<!-- explain if user cannot confirm -->
<!-- Note that these conditions are mirrored in userCanConfirm(). -->
<div v-if="confirmerIdList.includes(activeDid)">
You have confirmed this claim.
</div>
<div v-else-if="veriClaim.issuer == activeDid">
You cannot confirm this because you issued this claim, so you already
count as confirming it.
</div>
<div v-else-if="containsHiddenDid(veriClaim.claim)">
You cannot confirm this because it contains hidden identifiers.
</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
class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"
>{{ veriClaimDump }}</pre
@ -192,6 +208,7 @@ import * as serverUtil from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts";
import { GiverInputInfo } from "@/libs/endorserServer";
interface Notification {
group: string;
@ -210,9 +227,9 @@ export default class ClaimView extends Vue {
allMyDids: Array<string> = [];
allContacts: Array<Contact> = [];
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 = "";
confsVisibleToIdList = []; // list of DIDs that can see any confirmer
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
fullClaim = null;
fullClaimDump = "";
fullClaimMessage = "";
@ -242,7 +259,7 @@ export default class ClaimView extends Vue {
let claimId;
if (pathParam) {
claimId = decodeURIComponent(pathParam);
this.loadClaim(claimId, identity);
await this.loadClaim(claimId, identity);
} else {
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() {
return (
this.numConfsNotVisible +
@ -313,7 +374,7 @@ export default class ClaimView extends Vue {
this.veriClaimDump = yaml.dump(this.veriClaim);
} else {
// 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(
{
group: "alert",
@ -393,7 +454,7 @@ export default class ClaimView extends Vue {
this.fullClaimDump = yaml.dump(this.fullClaim);
} else {
// 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(
{
group: "alert",
@ -466,7 +527,7 @@ export default class ClaimView extends Vue {
5000,
);
} else {
console.log("Got error submitting the confirmation:", result);
console.error("Got error submitting the confirmation:", result);
this.$notify(
{
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>

6
src/views/ContactsView.vue

@ -992,7 +992,7 @@ export default class ContactsView extends Vue {
"?",
)
) {
this.createAndSubmitGive(
this.createAndSubmitContactGive(
identity,
fromDid,
toDid,
@ -1004,7 +1004,7 @@ export default class ContactsView extends Vue {
}
// similar function is in endorserServer.ts
private async createAndSubmitGive(
private async createAndSubmitContactGive(
identity: IIdentifier,
fromDid: string,
toDid: string,
@ -1073,7 +1073,7 @@ export default class ContactsView extends Vue {
}
}
} 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.";
const serverError = error as AxiosError;
if (serverError) {

20
src/views/ProjectViewView.vue

@ -94,13 +94,14 @@
<div v-if="activeDid" class="mb-4">
<div class="text-center">
<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"
>
I offer&hellip;
</button>
</div>
</div>
<OfferDialog ref="customOfferDialog" :projectId="this.projectId" />
<div v-if="activeDid">
<div class="text-center">
@ -112,6 +113,12 @@
</button>
<p class="mt-2 mb-4 text-center">Or, record a contribution from:</p>
</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">
<li @click="openGiftDialog()">
@ -267,15 +274,6 @@
</div>
</div>
</div>
<GiftedDialog
ref="customGiveDialog"
message="Received from"
:projectId="this.projectId"
>
</GiftedDialog>
<OfferDialog ref="customOfferDialog" :projectId="this.projectId">
</OfferDialog>
</section>
</template>
@ -434,7 +432,7 @@ export default class ProjectViewView extends Vue {
this.url = resp.data.claim?.url || "";
} else {
// 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(
{
group: "alert",

Loading…
Cancel
Save