Browse Source

change the "give" action on contact page to use dialog box

Trent Larson 7 months ago
parent
commit
d1a285d659
  1. 7
      CHANGELOG.md
  2. 4
      src/components/EntityIcon.vue
  3. 49
      src/components/GiftedDialog.vue
  4. 9
      src/components/OfferDialog.vue
  5. 2
      src/components/QuickNav.vue
  6. 8
      src/libs/endorserServer.ts
  7. 5
      src/views/AccountViewView.vue
  8. 5
      src/views/ClaimView.vue
  9. 9
      src/views/ContactGiftingView.vue
  10. 1
      src/views/ContactQRScanShowView.vue
  11. 264
      src/views/ContactsView.vue
  12. 39
      src/views/GiftedDetails.vue
  13. 21
      src/views/HomeView.vue
  14. 12
      src/views/ProjectViewView.vue
  15. 4
      src/views/ProjectsView.vue
  16. 1
      src/views/QuickActionBvcBeginView.vue
  17. 2
      src/views/QuickActionBvcEndView.vue

7
CHANGELOG.md

@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Nothing - Nothing
## [0.3.9] - 2024.04-21
### Added
- Offer on contacts page
### Changed in DB or environment
- Nothing
## [0.3.8] - 2024.04-20 - 15c026c80ce03a26cae3ff80b0888934c101c7e2 ## [0.3.8] - 2024.04-20 - 15c026c80ce03a26cae3ff80b0888934c101c7e2
### Added ### Added
- Profile image for user - Profile image for user

4
src/components/EntityIcon.vue

@ -23,6 +23,10 @@ export default class EntityIcon extends Vue {
if (!identifier) { if (!identifier) {
return `<img src="../src/assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`; return `<img src="../src/assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
} }
// https://api.dicebear.com/8.x/avataaars/svg?seed=
// ... does not render things with the same seed as this library.
// "did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b" yields a girl with flowers in her hair and a lightning earring
// ... which looks similar to '' at the dicebear site but which is different.
const options: StyleOptions<object> = { const options: StyleOptions<object> = {
seed: (identifier as string) || "", seed: (identifier as string) || "",
size: this.iconSize, size: this.iconSize,

49
src/components/GiftedDialog.vue

@ -2,12 +2,12 @@
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4"> <h1 class="text-xl font-bold text-center mb-4">
{{ message }} {{ giver?.name || "somebody not named" }} {{ customTitle || message + " " + giver?.name || "somebody not named" }}
</h1> </h1>
<input <input
type="text" type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received" placeholder="What was given"
v-model="description" v-model="description"
/> />
<div class="flex flex-row justify-center"> <div class="flex flex-row justify-center">
@ -42,12 +42,15 @@
name: 'gifted-details', name: 'gifted-details',
query: { query: {
amountInput, amountInput,
customTitle,
description, description,
giverDid: giver?.did, giverDid: giver?.did,
giverName: giver?.name, giverName: giver?.name,
message, message,
offerId, offerId,
projectId, projectId,
recipientDid: receiver?.did,
recipientName: receiver?.name,
unitCode, unitCode,
}, },
}" }"
@ -90,7 +93,7 @@ import { NotificationIface } from "@/constants/app";
import { import {
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
GiverInputInfo, GiverReceiverInputInfo,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
@ -103,7 +106,6 @@ export default class GiftedDialog extends Vue {
@Prop message = ""; @Prop message = "";
@Prop projectId = ""; @Prop projectId = "";
@Prop showGivenToUser = false;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@ -111,22 +113,32 @@ export default class GiftedDialog extends Vue {
apiServer = ""; apiServer = "";
amountInput = "0"; amountInput = "0";
callbackOnSuccess?: (amount: number) => void = () => {};
customTitle?: string;
description = ""; description = "";
givenToUser = false; giver?: GiverReceiverInputInfo; // undefined means no identified giver agent
giver?: GiverInputInfo; // undefined means no identified giver agent
isTrade = false; isTrade = false;
offerId = ""; offerId = "";
receiver?: GiverReceiverInputInfo;
unitCode = "HUR"; unitCode = "HUR";
visible = false; visible = false;
libsUtil = libsUtil; libsUtil = libsUtil;
async open(giver?: GiverInputInfo, offerId?: string) { async open(
giver?: GiverReceiverInputInfo,
receiver?: GiverReceiverInputInfo,
offerId?: string,
customTitle?: string,
callbackOnSuccess?: (amount: number) => void,
) {
this.customTitle = customTitle;
this.description = ""; this.description = "";
this.giver = giver || {}; this.giver = giver;
this.receiver = receiver;
// 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.amountInput = "0"; this.amountInput = "0";
this.callbackOnSuccess = callbackOnSuccess;
this.offerId = offerId || ""; this.offerId = offerId || "";
try { try {
@ -141,7 +153,7 @@ export default class GiftedDialog extends Vue {
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
if (!this.giver.name) { if (this.giver && !this.giver.name) {
this.giver.name = didInfo( this.giver.name = didInfo(
this.giver.did, this.giver.did,
this.activeDid, this.activeDid,
@ -196,7 +208,6 @@ export default class GiftedDialog extends Vue {
eraseValues() { eraseValues() {
this.description = ""; this.description = "";
this.giver = undefined; this.giver = undefined;
this.givenToUser = this.showGivenToUser;
this.amountInput = "0"; this.amountInput = "0";
this.unitCode = "HUR"; this.unitCode = "HUR";
} }
@ -254,6 +265,7 @@ export default class GiftedDialog extends Vue {
// this is asynchronous, but we don't need to wait for it to complete // this is asynchronous, but we don't need to wait for it to complete
await this.recordGive( await this.recordGive(
(this.giver?.did as string) || null, (this.giver?.did as string) || null,
(this.receiver?.did as string) || null,
this.description, this.description,
parseFloat(this.amountInput), parseFloat(this.amountInput),
this.unitCode, this.unitCode,
@ -265,14 +277,16 @@ export default class GiftedDialog extends Vue {
/** /**
* *
* @param giverDid may be null * @param giverDid may be null
* @param recipientDid may be null
* @param description may be an empty string * @param description may be an empty string
* @param amountInput may be 0 * @param amount may be 0
* @param unitCode may be omitted, defaults to "HUR" * @param unitCode may be omitted, defaults to "HUR"
*/ */
public async recordGive( async recordGive(
giverDid: string | null, giverDid: string | null,
recipientDid: string | null,
description: string, description: string,
amountInput: number, amount: number,
unitCode: string = "HUR", unitCode: string = "HUR",
) { ) {
try { try {
@ -282,9 +296,9 @@ export default class GiftedDialog extends Vue {
this.apiServer, this.apiServer,
identity, identity,
giverDid, giverDid,
this.givenToUser ? this.activeDid : undefined, this.receiver?.did as string,
description, description,
amountInput, amount,
unitCode, unitCode,
this.projectId, this.projectId,
this.offerId, this.offerId,
@ -316,6 +330,9 @@ export default class GiftedDialog extends Vue {
}, },
7000, 7000,
); );
if (this.callbackOnSuccess) {
this.callbackOnSuccess(amount);
}
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {

9
src/components/OfferDialog.vue

@ -43,7 +43,7 @@
<input <input
type="text" type="text"
class="w-full border border-slate-400 px-2 py-2 rounded-r" class="w-full border border-slate-400 px-2 py-2 rounded-r"
:placeholder="'Date, eg. ' + new Date().toISOString().slice(0, 10)" :placeholder="datePlaceholder()"
v-model="expirationDateInput" v-model="expirationDateInput"
/> />
</div> </div>
@ -69,6 +69,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { DateTime } from "luxon";
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
@ -143,6 +144,12 @@ export default class OfferDialog extends Vue {
)}`; )}`;
} }
datePlaceholder() {
return (
"Date, eg. " + DateTime.now().plus({ month: 1 }).toISO().slice(0, 10)
);
}
cancel() { cancel() {
this.close(); this.close();
this.eraseValues(); this.eraseValues();

2
src/components/QuickNav.vue

@ -44,7 +44,7 @@
:to="{ name: 'projects' }" :to="{ name: 'projects' }"
class="block text-center py-3 px-1" class="block text-center py-3 px-1"
> >
<fa icon="folder-open" class="fa-fw"></fa> <fa icon="hand" class="fa-fw"></fa>
</router-link> </router-link>
</li> </li>
<!-- Contacts --> <!-- Contacts -->

8
src/libs/endorserServer.ts

@ -27,14 +27,14 @@ export interface AgreeVerifiableCredential {
object: Record<string, any>; object: Record<string, any>;
} }
export interface GiverInputInfo { export interface GiverReceiverInputInfo {
did?: string; did?: string;
name?: string; name?: string;
} }
export interface GiverOutputInfo { export interface GiverOutputInfo {
action: string; action: string;
giver?: GiverInputInfo; giver?: GiverReceiverInputInfo;
description?: string; description?: string;
amount?: number; amount?: number;
unitCode?: string; unitCode?: string;
@ -481,7 +481,7 @@ export async function getPlanFromCache(
cred = resp.data.data[0]; cred = resp.data.data[0];
planCache.set(handleId, cred); planCache.set(handleId, cred);
} else { } else {
console.log( console.error(
"Failed to load plan with handle", "Failed to load plan with handle",
handleId, handleId,
" Got data:", " Got data:",
@ -489,7 +489,7 @@ export async function getPlanFromCache(
); );
} }
} catch (error) { } catch (error) {
console.log( console.error(
"Failed to load plan with handle", "Failed to load plan with handle",
handleId, handleId,
" Got error:", " Got error:",

5
src/views/AccountViewView.vue

@ -104,7 +104,8 @@
</div> </div>
<div class="mt-4"> <div class="mt-4">
<div class="flex justify-center"> <div class="flex justify-center">
... and those without your image see this (if you let them see your activity): ... and those without your image see this (if you let them see your
activity):
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<EntityIcon <EntityIcon
@ -1400,7 +1401,7 @@ export default class AccountViewView extends Vue {
console.error("Error deleting image:", error); console.error("Error deleting image:", error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any).response.status === 404) { if ((error as any).response.status === 404) {
console.log("The image was already deleted:", error); console.error("The image was already deleted:", error);
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {

5
src/views/ClaimView.vue

@ -415,7 +415,7 @@ import * as serverUtil from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { GiverInputInfo } from "@/libs/endorserServer"; import { GiverReceiverInputInfo } from "@/libs/endorserServer";
@Component({ @Component({
components: { GiftedDialog, QuickNav }, components: { GiftedDialog, QuickNav },
@ -775,11 +775,12 @@ export default class ClaimView extends Vue {
} }
openFulfillGiftDialog() { openFulfillGiftDialog() {
const giver: GiverInputInfo = { const giver: GiverReceiverInputInfo = {
did: libsUtil.offerGiverDid(this.veriClaim), did: libsUtil.offerGiverDid(this.veriClaim),
}; };
(this.$refs.customGiveDialog as GiftedDialog).open( (this.$refs.customGiveDialog as GiftedDialog).open(
giver, giver,
undefined,
this.veriClaim.handleId, this.veriClaim.handleId,
); );
} }

9
src/views/ContactGiftingView.vue

@ -87,7 +87,7 @@ import { Account, AccountsSchema } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { GiverInputInfo } from "@/libs/endorserServer"; import { GiverReceiverInputInfo } from "@/libs/endorserServer";
@Component({ @Component({
components: { GiftedDialog, QuickNav, EntityIcon }, components: { GiftedDialog, QuickNav, EntityIcon },
@ -163,8 +163,11 @@ export default class ContactGiftingView extends Vue {
return headers; return headers;
} }
openDialog(giver: GiverInputInfo) { openDialog(giver: GiverReceiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver); const recipient = this.projectId
? undefined
: { did: this.activeDid, name: "you" };
(this.$refs.customDialog as GiftedDialog).open(giver, recipient);
} }
} }
</script> </script>

1
src/views/ContactQRScanShowView.vue

@ -188,7 +188,6 @@ export default class ContactQRScanShow extends Vue {
if (url) { if (url) {
try { try {
const fullData = getContactPayloadFromJwtUrl(url); const fullData = getContactPayloadFromJwtUrl(url);
console.log("fullData", fullData);
localStorage.setItem("contactEndorserUrl", url); localStorage.setItem("contactEndorserUrl", url);
this.$router.push({ name: "contacts" }); this.$router.push({ name: "contacts" });
} catch (e) { } catch (e) {

264
src/views/ContactsView.vue

@ -43,6 +43,7 @@
<div class="flex justify-between" v-if="showGiveNumbers"> <div class="flex justify-between" v-if="showGiveNumbers">
<div class="w-full text-right"> <div class="w-full text-right">
<!--
Hours to Add: Hours to Add:
<input <input
class="border rounded border-slate-400 w-24 text-right" class="border rounded border-slate-400 w-24 text-right"
@ -57,30 +58,29 @@
placeholder="Description" placeholder="Description"
v-model="hourDescriptionInput" v-model="hourDescriptionInput"
/> />
<br /> -->
In the following, only the most recent hours are included. To see more,
click
<span
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
>
<fa icon="file-lines" class="fa-fw" />
</span>
<br /> <br />
<button <button
href="" href=""
class="text-center text-md text-white px-1.5 py-2 rounded-md mb-6" class="text-center text-md text-white px-1.5 py-2 rounded-md mt-1"
v-bind:class="showGiveAmountsClassNames()" v-bind:class="showGiveAmountsClassNames()"
@click="toggleShowGiveTotals()" @click="toggleShowGiveTotals()"
> >
{{ {{
showGiveTotals showGiveTotals
? "Total" ? "Showing Total"
: showGiveConfirmed : showGiveConfirmed
? "Confirmed" ? "Confirmed"
: "Unconfirmed" : "Unconfirmed"
}} }}
</button> </button>
<br />
(Only most recent hours included. To see more, click
<span
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
>
<fa icon="file-lines" class="fa-fw" />
</span>
)
</div> </div>
</div> </div>
@ -189,10 +189,11 @@
> >
<button <button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md" class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md"
@click="onClickAddGive(activeDid, contact.did)" @click="showGiftedDialog(activeDid, contact.did)"
:title="givenByMeDescriptions[contact.did] || ''" :title="givenByMeDescriptions[contact.did] || ''"
> >
To: To:
<br />
{{ {{
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
this.showGiveTotals this.showGiveTotals
@ -203,15 +204,17 @@
: (givenByMeUnconfirmed[contact.did] || 0) : (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */ /* eslint-enable prettier/prettier */
}} }}
<br />
<fa icon="plus" /> <fa icon="plus" />
</button> </button>
<button <button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l" class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
@click="onClickAddGive(contact.did, activeDid)" @click="showGiftedDialog(contact.did, this.activeDid)"
:title="givenToMeDescriptions[contact.did] || ''" :title="givenToMeDescriptions[contact.did] || ''"
> >
From: From:
<br />
{{ {{
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
this.showGiveTotals this.showGiveTotals
@ -222,6 +225,7 @@
: (givenToMeUnconfirmed[contact.did] || 0) : (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */ /* eslint-enable prettier/prettier */
}} }}
<br />
<fa icon="plus" /> <fa icon="plus" />
</button> </button>
@ -237,7 +241,7 @@
name: 'contact-amounts', name: 'contact-amounts',
query: { contactDid: contact.did }, query: { contactDid: contact.did },
}" }"
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md" class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-slate-400"
title="See more given activity" title="See more given activity"
> >
<fa icon="file-lines" class="fa-fw" /> <fa icon="file-lines" class="fa-fw" />
@ -249,6 +253,7 @@
</ul> </ul>
<p v-else>There are no contacts.</p> <p v-else>There are no contacts.</p>
<GiftedDialog ref="customGivenDialog" />
<OfferDialog ref="customOfferDialog" /> <OfferDialog ref="customOfferDialog" />
<div v-if="showLargeIdenticon" class="fixed z-[100] top-0 inset-x-0 w-full"> <div v-if="showLargeIdenticon" class="fixed z-[100] top-0 inset-x-0 w-full">
@ -313,8 +318,8 @@ import {
import { import {
CONTACT_CSV_HEADER, CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
GiveVerifiableCredential,
isDid, isDid,
RegisterVerifiableCredential, RegisterVerifiableCredential,
SERVICE_ID, SERVICE_ID,
@ -322,13 +327,14 @@ import {
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
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 GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue"; import OfferDialog from "@/components/OfferDialog.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Buffer } from "buffer/"; import { Buffer } from "buffer/";
@Component({ @Component({
components: { EntityIcon, OfferDialog, QuickNav }, components: { GiftedDialog, EntityIcon, OfferDialog, QuickNav },
}) })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
@ -352,8 +358,6 @@ export default class ContactsView extends Vue {
givenToMeConfirmed: Record<string, number> = {}; givenToMeConfirmed: Record<string, number> = {};
// { "did:...": amount } entry for each contact // { "did:...": amount } entry for each contact
givenToMeUnconfirmed: Record<string, number> = {}; givenToMeUnconfirmed: Record<string, number> = {};
hourDescriptionInput = "";
hourInput = "0";
isRegistered = false; isRegistered = false;
showDidCopy = false; showDidCopy = false;
showGiveNumbers = false; showGiveNumbers = false;
@ -1041,27 +1045,33 @@ export default class ContactsView extends Vue {
} }
private nameForDid(contacts: Array<Contact>, did: string): string { private nameForDid(contacts: Array<Contact>, did: string): string {
if (did === this.activeDid) {
return "you";
}
const contact = R.find((con) => con.did == did, contacts); const contact = R.find((con) => con.did == did, contacts);
return this.nameForContact(contact); return this.nameForContact(contact);
} }
private nameForContact(contact?: Contact, capitalize?: boolean): string { private nameForContact(contact?: Contact, capitalize?: boolean): string {
return contact?.name || (capitalize ? "T" : "t") + "his unnamed user"; return (
(contact?.name as string) || (capitalize ? "T" : "t") + "his unnamed user"
);
} }
async onClickAddGive(fromDid: string, toDid: string): Promise<void> { private showGiftedDialog(giverDid: string, recipientDid: string) {
const identity = await this.getIdentity(this.activeDid);
// if they have unconfirmed amounts, ask to confirm those first // if they have unconfirmed amounts, ask to confirm those first
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) { if (
const isare = this.givenToMeUnconfirmed[fromDid] == 1 ? "is" : "are"; recipientDid == this.activeDid &&
const hours = this.givenToMeUnconfirmed[fromDid] == 1 ? "hour" : "hours"; this.givenToMeUnconfirmed[giverDid] > 0
) {
const isAre = this.givenToMeUnconfirmed[giverDid] == 1 ? "is" : "are";
const hours = this.givenToMeUnconfirmed[giverDid] == 1 ? "hour" : "hours";
if ( if (
confirm( confirm(
"There " + "There " +
isare + isAre +
" " + " " +
this.givenToMeUnconfirmed[fromDid] + this.givenToMeUnconfirmed[giverDid] +
" unconfirmed " + " unconfirmed " +
hours + hours +
" from them." + " from them." +
@ -1070,178 +1080,58 @@ export default class ContactsView extends Vue {
) { ) {
this.$router.push({ this.$router.push({
name: "contact-amounts", name: "contact-amounts",
query: { contactDid: fromDid }, query: { contactDid: giverDid },
}); });
return; return;
} }
} }
if (!libsUtil.isNumeric(this.hourInput)) {
this.$notify( let giver: GiverReceiverInputInfo, receiver: GiverReceiverInputInfo;
{ if (giverDid) {
group: "alert", giver = {
type: "danger", did: giverDid,
title: "Input Error", name: this.nameForDid(this.contacts, giverDid),
text: "This is not a valid number of hours: " + this.hourInput, };
}, }
3000, if (recipientDid) {
); receiver = {
} else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) { did: recipientDid,
this.$notify( name: this.nameForDid(this.contacts, recipientDid),
{ };
group: "alert", }
type: "danger",
title: "Input Error", let callback: (amount: number) => void;
text: "Giving no hours or description does nothing.", let customTitle = "Given";
}, // choose whether to open dialog to user or from user
3000, if (giverDid == this.activeDid) {
); callback = (amount: number) => {
} else if (!identity) { const newList = R.clone(this.givenByMeUnconfirmed);
this.$notify( newList[recipientDid] = (newList[recipientDid] || 0) + amount;
{ this.givenByMeUnconfirmed = newList;
group: "alert", };
type: "danger", customTitle = "To " + receiver.name;
title: "Status Error",
text: "No identifier is available.",
},
3000,
);
} else { } else {
// ask to confirm amount // must be (recipientDid == this.activeDid)
let toFrom; callback = (amount: number) => {
if (fromDid == identity?.did) { const newList = R.clone(this.givenToMeUnconfirmed);
toFrom = "from you to " + this.nameForDid(this.contacts, toDid); newList[giverDid] = (newList[giverDid] || 0) + amount;
} else { this.givenToMeUnconfirmed = newList;
toFrom = "from " + this.nameForDid(this.contacts, fromDid) + " to you"; };
} customTitle = "From " + giver.name;
let description;
if (this.hourDescriptionInput) {
description = " with description '" + this.hourDescriptionInput + "'";
} else {
description = " with no description";
}
if (
confirm(
"Are you sure you want to record " +
this.hourInput +
" hour" +
(this.hourInput == "1" ? "" : "s") +
" " +
toFrom +
description +
"?",
)
) {
this.createAndSubmitContactGive(
identity,
fromDid,
toDid,
parseFloat(this.hourInput),
this.hourDescriptionInput,
);
}
} }
(this.$refs.customGivenDialog as GiftedDialog).open(
giver,
receiver,
undefined as string,
customTitle,
callback,
);
} }
openOfferDialog(recipientDid: string) { openOfferDialog(recipientDid: string) {
(this.$refs.customOfferDialog as OfferDialog).open(recipientDid); (this.$refs.customOfferDialog as OfferDialog).open(recipientDid);
} }
// similar function is in endorserServer.ts
private async createAndSubmitContactGive(
identity: IIdentifier,
fromDid: string,
toDid: string,
amount: number,
description: string,
): Promise<void> {
// Make a claim
const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org",
"@type": "GiveAction",
agent: { identifier: fromDid },
object: { amountOfThisGood: amount, unitCode: "HUR" },
recipient: { identifier: toDid },
};
if (description) {
vcClaim.description = description;
}
// 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) {
// 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 = this.apiServer + "/api/v2/claim";
const headers = await this.getHeaders(identity);
try {
const resp = await this.axios.post(url, payload, { headers });
if (resp.data?.success?.handleId) {
this.$notify(
{
group: "alert",
type: "success",
title: "Done",
text: "Successfully logged time to the server.",
},
5000,
);
if (fromDid === identity.did) {
const newList = R.clone(this.givenByMeUnconfirmed);
newList[toDid] = (newList[toDid] || 0) + amount;
this.givenByMeUnconfirmed = newList;
} else {
const newList = R.clone(this.givenToMeConfirmed);
newList[fromDid] = (newList[fromDid] || 0) + amount;
this.givenToMeConfirmed = newList;
}
}
} catch (error) {
console.error("Error in createAndSubmitContactGive: ", error);
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
if (serverError.message) {
userMessage = serverError.message; // Info for the user
} else {
userMessage = JSON.stringify(serverError.toJSON());
}
} else {
userMessage = error as string;
}
// Now set that error for the user to see.
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Sending Give",
text: userMessage,
},
5000,
);
}
}
}
private async onClickCancelName() { private async onClickCancelName() {
this.contactEdit = null; this.contactEdit = null;
this.contactNewName = ""; this.contactNewName = "";

39
src/views/GiftedDetails.vue

@ -18,8 +18,12 @@
<h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1> <h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1>
<h1 class="text-xl font-bold text-center mb-4"> <h1 class="text-xl font-bold text-center mb-4">
{{ message }} {{ giverName || "somebody not named" }} {{ customTitle || message + " " + giverName || "somebody not named" }}
</h1> </h1>
<div>
<span>From {{ giverName || "somebody not named" }}</span>
<span> to {{ recipientName || "somebody not named" }}</span>
</div>
<textarea <textarea
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received" placeholder="What was received"
@ -80,7 +84,7 @@
<label class="text-sm">This is given to a project</label> <label class="text-sm">This is given to a project</label>
</div> </div>
<div v-if="!projectId" class="mt-4"> <div v-if="showGivenToUser" class="mt-4">
<input type="checkbox" class="h-6 w-6 mr-2" v-model="givenToUser" /> <input type="checkbox" class="h-6 w-6 mr-2" v-model="givenToUser" />
<label class="text-sm">Given to you</label> <label class="text-sm">Given to you</label>
</div> </div>
@ -126,12 +130,10 @@ import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { createAndSubmitGive } from "@/libs/endorserServer"; import { createAndSubmitGive } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import GiftedDialog from "@/components/GiftedDialog.vue";
import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue"; import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue";
@Component({ @Component({
components: { components: {
GiftedDialog,
GiftedPhotoDialog, GiftedPhotoDialog,
QuickNav, QuickNav,
TopMessage, TopMessage,
@ -144,6 +146,7 @@ export default class GiftedDetails extends Vue {
apiServer = ""; apiServer = "";
amountInput = "0"; amountInput = "0";
customTitle = "";
description = ""; description = "";
givenToUser = false; givenToUser = false;
giverDid: string | undefined; giverDid: string | undefined;
@ -153,30 +156,46 @@ export default class GiftedDetails extends Vue {
message = ""; message = "";
offerId = ""; offerId = "";
projectId = ""; projectId = "";
recipientDid = "";
recipientName = "";
showGivenToUser = false;
unitCode = "HUR"; unitCode = "HUR";
libsUtil = libsUtil; libsUtil = libsUtil;
async mounted() { async mounted() {
this.amountInput = this.$route.query.amountInput as string; this.amountInput = this.$route.query.amountInput as string;
this.customTitle = this.$route.query.customTitle as string;
this.description = this.$route.query.description as string; this.description = this.$route.query.description as string;
this.giverDid = this.$route.query.giverDid as string; this.giverDid = this.$route.query.giverDid as string;
this.giverName = this.$route.query.giverName as string; this.giverName = this.$route.query.giverName as string;
if (this.giverDid && !this.giverName) {
this.giverName =
this.giverDid === this.activeDid ? "you" : "someone not named";
}
this.message = this.$route.query.message as string; this.message = this.$route.query.message as string;
this.offerId = this.$route.query.offerId as string; this.offerId = this.$route.query.offerId as string;
this.projectId = this.$route.query.projectId as string; this.projectId = this.$route.query.projectId as string;
this.recipientDid = this.$route.query.recipientDid as string;
this.recipientName = this.$route.query.recipientName as string;
if (this.recipientDid && !this.recipientName) {
this.recipientName =
this.recipientDid === this.activeDid ? "you" : "someone not named";
}
this.unitCode = this.$route.query.unitCode as string; this.unitCode = this.$route.query.unitCode as string;
this.imageUrl = localStorage.getItem("imageUrl") || ""; this.imageUrl = localStorage.getItem("imageUrl") || "";
this.givenToUser = !this.projectId;
try { try {
await db.open(); await db.open();
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.givenToUser = this.recipientDid === this.activeDid;
this.showGivenToUser =
!this.projectId && this.recipientDid === this.activeDid;
// 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.error("Error retrieving settings from database:", err); console.error("Error retrieving settings from database:", err);
@ -358,12 +377,18 @@ export default class GiftedDetails extends Vue {
public async recordGive() { public async recordGive() {
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const recipientDid =
this.recipientDid === this.activeDid
? this.givenToUser
? this.activeDid
: undefined
: this.recipientDid;
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,
identity, identity,
this.giverDid, this.giverDid,
this.givenToUser ? this.activeDid : undefined, recipientDid,
this.description, this.description,
parseFloat(this.amountInput), parseFloat(this.amountInput),
this.unitCode, this.unitCode,

21
src/views/HomeView.vue

@ -154,13 +154,13 @@
<router-link <router-link
v-if="allContacts.length >= 7" v-if="allContacts.length >= 7"
:to="{ name: 'contact-gives' }" :to="{ name: 'contact-gives' }"
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
> >
Choose From All Contacts Choose From All Contacts
</router-link> </router-link>
<button <button
@click="openGiftedPrompts()" @click="openGiftedPrompts()"
class="block text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" class="block text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
> >
Ideas... Ideas...
</button> </button>
@ -168,11 +168,7 @@
</div> </div>
</div> </div>
<GiftedDialog <GiftedDialog ref="customDialog" message="Received from" />
ref="customDialog"
message="Received from"
showGivenToUser="true"
/>
<GiftedPrompts ref="giftedPrompts" /> <GiftedPrompts ref="giftedPrompts" />
<FeedFilters ref="feedFilters" /> <FeedFilters ref="feedFilters" />
@ -181,7 +177,7 @@
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<h2 class="text-xl font-bold">Latest Activity</h2> <h2 class="text-xl font-bold">Latest Activity</h2>
<button @click="openFeedFilters()" class="block text-center ml-auto"> <button @click="openFeedFilters()" class="block text-center ml-auto">
<span class="text-sm uppercase text-white"> <span class="text-sm text-white">
<span <span
v-if="resultsAreFiltered()" v-if="resultsAreFiltered()"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md" class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md"
@ -328,7 +324,7 @@ import {
containsNonHiddenDid, containsNonHiddenDid,
didInfoForContact, didInfoForContact,
getPlanFromCache, getPlanFromCache,
GiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { generateSaveAndActivateIdentity } from "@/libs/util"; import { generateSaveAndActivateIdentity } from "@/libs/util";
@ -722,8 +718,11 @@ export default class HomeView extends Vue {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
} }
openDialog(giver?: GiverInputInfo) { openDialog(giver?: GiverReceiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver); (this.$refs.customDialog as GiftedDialog).open(giver, {
did: this.activeDid,
name: "you",
});
} }
openGiftedPrompts() { openGiftedPrompts() {

12
src/views/ProjectViewView.vue

@ -363,7 +363,7 @@ import * as libsUtil from "@/libs/util";
import { import {
BLANK_GENERIC_SERVER_RECORD, BLANK_GENERIC_SERVER_RECORD,
GenericCredWrapper, GenericCredWrapper,
GiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
OfferSummaryRecord, OfferSummaryRecord,
PlanSummaryRecord, PlanSummaryRecord,
@ -697,7 +697,7 @@ export default class ProjectViewView extends Vue {
); );
} }
openGiftDialog(contact?: GiverInputInfo) { openGiftDialog(contact?: GiverReceiverInputInfo) {
(this.$refs.customGiveDialog as GiftedDialog).open(contact); (this.$refs.customGiveDialog as GiftedDialog).open(contact);
} }
@ -736,10 +736,14 @@ export default class ProjectViewView extends Vue {
claim: offer.fullClaim, claim: offer.fullClaim,
issuer: offer.offeredByDid, issuer: offer.offeredByDid,
}; };
const giver: GiverInputInfo = { const giver: GiverReceiverInputInfo = {
did: libsUtil.offerGiverDid(offerRecord), did: libsUtil.offerGiverDid(offerRecord),
}; };
(this.$refs.customGiveDialog as GiftedDialog).open(giver, offer.handleId); (this.$refs.customGiveDialog as GiftedDialog).open(
giver,
undefined,
offer.handleId,
);
} }
// return an HTTPS URL if it's not a global URL // return an HTTPS URL if it's not a global URL

4
src/views/ProjectsView.vue

@ -298,7 +298,7 @@ export default class ProjectsView extends Vue {
try { try {
this.isLoading = true; this.isLoading = true;
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
if (resp.status === 200 || !resp.data.data) { if (resp.status === 200 && resp.data.data) {
const plans: PlanData[] = resp.data.data; const plans: PlanData[] = resp.data.data;
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, issuerDid, rowid } = plan; const { name, description, handleId, issuerDid, rowid } = plan;
@ -423,7 +423,7 @@ export default class ProjectsView extends Vue {
try { try {
this.isLoading = true; this.isLoading = true;
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
if (resp.status === 200 || !resp.data.data) { if (resp.status === 200 && resp.data.data) {
this.offers = this.offers.concat(resp.data.data); this.offers = this.offers.concat(resp.data.data);
} else { } else {
console.error( console.error(

1
src/views/QuickActionBvcBeginView.vue

@ -97,6 +97,7 @@ export default class QuickActionBvcBeginView extends Vue {
todayOrPreviousStartDate = ""; todayOrPreviousStartDate = "";
async mounted() { async mounted() {
// use the time zone for Bountiful
let currentOrPreviousSat = DateTime.now().setZone("America/Denver"); let currentOrPreviousSat = DateTime.now().setZone("America/Denver");
if (currentOrPreviousSat.weekday < 6) { if (currentOrPreviousSat.weekday < 6) {
// it's not Saturday or Sunday, // it's not Saturday or Sunday,

2
src/views/QuickActionBvcEndView.vue

@ -223,7 +223,7 @@ export default class QuickActionBvcBeginView extends Vue {
); );
if (!response.ok) { if (!response.ok) {
console.log("Bad response", response); console.error("Bad response", response);
throw new Error("Bad response when retrieving claims."); throw new Error("Bad response when retrieving claims.");
} }
await response.json().then((data) => { await response.json().then((data) => {

Loading…
Cancel
Save