Compare commits
20 Commits
offer-edit
...
playwright
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a810d531b7 | ||
|
|
91445cc240 | ||
| 85b9aa8e2b | |||
| 7309ba1436 | |||
|
|
07efab3782 | ||
|
|
375cda1082 | ||
| 67b0122d5a | |||
| 6aef08d7e8 | |||
| a5248af4a3 | |||
| d9f45d52f9 | |||
| dc80b686ce | |||
| 892cf4c595 | |||
| e2b641736d | |||
| bb1fc7519f | |||
| 014d4081e6 | |||
| 877678b745 | |||
| a3da157ae3 | |||
|
|
713faebf51 | ||
|
|
93a230298d | ||
|
|
799c8d66c0 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -7,8 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
|
||||
## ?
|
||||
### Added
|
||||
- Send list of contacts to someone
|
||||
### Changed
|
||||
- Moved contact actions from list onto detail page
|
||||
|
||||
|
||||
## [0.3.20] - 2024.08.18 - 4064eb75a9743ca268bf00016fa0a5fc5dec4e30
|
||||
### Fixed
|
||||
- Bad "give" verbiage on offer page
|
||||
- Failing offer test
|
||||
|
||||
|
||||
## [0.3.19] - 2024.08.18 - ee9c14942ceba993bf21a11249601f205158ec71
|
||||
### Added
|
||||
- Update of an offer
|
||||
- Recipient description in offer list
|
||||
### Fixed
|
||||
- List of offers wasn't showing.
|
||||
- Destination page after sharing photo was wrong.
|
||||
|
||||
|
||||
## [0.3.17] - 2024.07.11 - cefa384ff1a2d922848c370640c096c529920fab
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "TimeSafari",
|
||||
"version": "0.3.18-beta",
|
||||
"version": "0.3.21-beta",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "TimeSafari",
|
||||
"version": "0.3.18-beta",
|
||||
"version": "0.3.21-beta",
|
||||
"dependencies": {
|
||||
"@dicebear/collection": "^5.4.1",
|
||||
"@dicebear/core": "^5.4.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TimeSafari",
|
||||
"version": "0.3.18-beta",
|
||||
"version": "0.3.21-beta",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"serve": "vite preview",
|
||||
|
||||
@@ -74,7 +74,7 @@ export default defineConfig({
|
||||
|
||||
/* Configure global timeout; default is 30000 milliseconds */
|
||||
// the image upload will often not succeed at 5 seconds
|
||||
//timeout: 10000,
|
||||
timeout: 20000,
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
<template>
|
||||
<div class="text-center text-red-500">{{ message }}</div>
|
||||
<div class="absolute right-5 top-3">
|
||||
<span class="align-center text-red-500 mr-2">{{ message }}</span>
|
||||
<span class="ml-2">
|
||||
<router-link
|
||||
:to="{ name: 'help' }"
|
||||
class="text-xs uppercase 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-1.5 py-1 rounded-md ml-1"
|
||||
>
|
||||
Help
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -4,8 +4,8 @@ export interface Contact {
|
||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
||||
profileImageUrl?: string;
|
||||
publicKeyBase64?: string;
|
||||
seesMe?: boolean;
|
||||
registered?: boolean;
|
||||
seesMe?: boolean; // cached value of the server setting
|
||||
registered?: boolean; // cached value of the server setting
|
||||
}
|
||||
|
||||
export const ContactSchema = {
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface GiveSummaryRecord {
|
||||
fulfillsPlanHandleId: string;
|
||||
handleId: string;
|
||||
issuedAt: string;
|
||||
issuerDid: string;
|
||||
jwtId: string;
|
||||
recipientDid: string;
|
||||
unit: string;
|
||||
@@ -98,6 +99,7 @@ export interface OfferSummaryRecord {
|
||||
fullClaim: OfferVerifiableCredential;
|
||||
fulfillsPlanHandleId: string;
|
||||
handleId: string;
|
||||
issuerDid: string;
|
||||
jwtId: string;
|
||||
nonAmountGivenConfirmed: number;
|
||||
objectDescription: string;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// many of these are also found in endorser-mobile utility.ts
|
||||
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { Buffer } from "buffer";
|
||||
import * as R from "ramda";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import {
|
||||
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
||||
MASTER_SETTINGS_KEY,
|
||||
@@ -18,11 +21,9 @@ import {
|
||||
OfferVerifiableCredential,
|
||||
} from "@/libs/endorserServer";
|
||||
import * as serverUtil from "@/libs/endorserServer";
|
||||
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";
|
||||
|
||||
import { Buffer } from "buffer";
|
||||
import { KeyMeta } from "@/libs/crypto/vc";
|
||||
import { createPeerDid } from "@/libs/crypto/vc/didPeer";
|
||||
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";
|
||||
|
||||
export const PRIVACY_MESSAGE =
|
||||
"The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
|
||||
@@ -91,6 +92,28 @@ export const isGiveAction = (
|
||||
return veriClaim.claimType === "GiveAction";
|
||||
};
|
||||
|
||||
export const nameForDid = (
|
||||
activeDid: string,
|
||||
contacts: Array<Contact>,
|
||||
did: string,
|
||||
): string => {
|
||||
if (did === activeDid) {
|
||||
return "you";
|
||||
}
|
||||
const contact = R.find((con) => con.did == did, contacts);
|
||||
return nameForContact(contact);
|
||||
};
|
||||
|
||||
export const nameForContact = (
|
||||
contact?: Contact,
|
||||
capitalize?: boolean,
|
||||
): string => {
|
||||
return (
|
||||
(contact?.name as string) ||
|
||||
(capitalize ? "This" : "this") + " unnamed user"
|
||||
);
|
||||
};
|
||||
|
||||
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
|
||||
fn();
|
||||
useClipboard()
|
||||
|
||||
@@ -22,18 +22,6 @@
|
||||
<span />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between py-2">
|
||||
<span />
|
||||
<span>
|
||||
<router-link
|
||||
:to="{ name: 'help' }"
|
||||
class="text-xs uppercase 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-1.5 py-1 rounded-md ml-1"
|
||||
>
|
||||
Help
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- ID notice -->
|
||||
<div
|
||||
v-if="!activeDid"
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
v-if="libsUtil.isGiveAction(veriClaim)"
|
||||
:to="'/confirm-gift/' + encodeURIComponent(veriClaim.id)"
|
||||
class="col-span-1 text-blue-500"
|
||||
data-testId="confirmGiftLink"
|
||||
>
|
||||
Details...
|
||||
</router-link>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<QuickNav />
|
||||
<TopMessage />
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<!-- Breadcrumb -->
|
||||
@@ -148,11 +149,9 @@
|
||||
|
||||
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
|
||||
<span v-else-if="totalConfirmers() === 1">
|
||||
One person has confirmed this.
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ totalConfirmers() }} people have confirmed this.
|
||||
One person confirmed this.
|
||||
</span>
|
||||
<span v-else> {{ totalConfirmers() }} people confirmed this. </span>
|
||||
|
||||
<div v-if="totalConfirmers() > 0">
|
||||
<div
|
||||
@@ -170,10 +169,10 @@
|
||||
"
|
||||
>
|
||||
<!-- Only show if this person has links to confirmers (below). -->
|
||||
Nobody that you know has issued or confirmed this claim.
|
||||
Nobody that you know issued or confirmed this claim.
|
||||
</div>
|
||||
<div v-if="confirmerIdList.length > 0">
|
||||
The following people have issued or confirmed this claim.
|
||||
The following people issued or confirmed this claim.
|
||||
<ul class="ml-4">
|
||||
<li
|
||||
v-for="confirmerId in confirmerIdList"
|
||||
@@ -205,7 +204,7 @@
|
||||
|
||||
<!--
|
||||
Never need to show this message:
|
||||
"Nobody that you know can see someone who has confirmed this claim."
|
||||
"Nobody that you know can see someone who confirmed this claim."
|
||||
|
||||
If there is nobody in the confirmerIdList then we'll show the combined "nobody" message.
|
||||
If there is somebody in the confirmerIdList then that's all they need to show.
|
||||
@@ -213,7 +212,7 @@
|
||||
|
||||
<!-- Now show anyone linked to confirmers. -->
|
||||
<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 issued or
|
||||
confirmed this claim.
|
||||
<ul class="ml-4">
|
||||
<li
|
||||
@@ -249,10 +248,11 @@
|
||||
|
||||
<!-- 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 v-if="!isRegistered">
|
||||
You cannot confirm this because you are not registered. Find someone
|
||||
to register you, maybe on the Help page.
|
||||
</div>
|
||||
<div v-else-if="giveDetails.agentDid == activeDid">
|
||||
<div v-else-if="giveDetails.issuerDid == activeDid">
|
||||
You cannot confirm this because you issued this claim, so you already
|
||||
count as confirming it.
|
||||
</div>
|
||||
@@ -410,13 +410,14 @@ import { Account } from "@/db/tables/accounts";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import * as serverUtil from "@/libs/endorserServer";
|
||||
import { displayAmount } from "@/libs/endorserServer";
|
||||
import { displayAmount, GiveSummaryRecord } from "@/libs/endorserServer";
|
||||
import * as libsUtil from "@/libs/util";
|
||||
import { isGiveAction } from "@/libs/util";
|
||||
import TopMessage from "@/components/TopMessage.vue";
|
||||
|
||||
@Component({
|
||||
methods: { displayAmount },
|
||||
components: { QuickNav },
|
||||
components: { TopMessage, QuickNav },
|
||||
})
|
||||
export default class ClaimView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
@@ -430,7 +431,7 @@ export default class ClaimView extends Vue {
|
||||
confirmerIdList: string[] = []; // list of DIDs that have confirmed this claim excluding the issuer
|
||||
confsVisibleErrorMessage = "";
|
||||
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
|
||||
giveDetails = null;
|
||||
giveDetails?: GiveSummaryRecord;
|
||||
giverName = "";
|
||||
issuerName = "";
|
||||
isLoading = false;
|
||||
@@ -453,7 +454,7 @@ export default class ClaimView extends Vue {
|
||||
this.confirmerIdList = [];
|
||||
this.confsVisibleErrorMessage = "";
|
||||
this.confsVisibleToIdList = [];
|
||||
this.giveDetails = null;
|
||||
this.giveDetails = undefined;
|
||||
this.isRegistered = false;
|
||||
this.numConfsNotVisible = 0;
|
||||
this.urlForNewGive = "";
|
||||
@@ -605,6 +606,12 @@ export default class ClaimView extends Vue {
|
||||
},
|
||||
3000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// the logic already stops earlier if the claim doesn't exist but this helps with typechecking
|
||||
if (!this.giveDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.urlForNewGive = "/gifted-details?";
|
||||
@@ -645,7 +652,8 @@ export default class ClaimView extends Vue {
|
||||
this.giveDetails.fulfillsHandleId
|
||||
) {
|
||||
this.urlForNewGive +=
|
||||
"&offerId=" + encodeURIComponent(this.giveDetails.fulfillsHandleId);
|
||||
"&offerId=" +
|
||||
encodeURIComponent(this.giveDetails?.fulfillsHandleId as string);
|
||||
}
|
||||
if (this.giveDetails.fulfillsPlanHandleId) {
|
||||
this.urlForNewGive +=
|
||||
@@ -666,9 +674,11 @@ export default class ClaimView extends Vue {
|
||||
const resultList1 = response.data.result || [];
|
||||
//const publicUrls = resultList.publicUrls || [];
|
||||
delete resultList1.publicUrls;
|
||||
// remove any hidden DIDs
|
||||
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1);
|
||||
// remove confirmations by this user
|
||||
const resultList3 = R.reject(
|
||||
(did: string) => did === this.giveDetails.agentDid,
|
||||
(did: string) => did === this.giveDetails?.issuerDid,
|
||||
resultList2,
|
||||
);
|
||||
this.confirmerIdList = resultList3;
|
||||
@@ -814,11 +824,11 @@ export default class ClaimView extends Vue {
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Already Confirmed",
|
||||
text: "You have already confirmed this claim.",
|
||||
text: "You already confirmed this claim.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
} else if (this.giveDetails.agentDid == this.activeDid) {
|
||||
} else if (this.giveDetails?.issuerDid == this.activeDid) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -828,7 +838,7 @@ export default class ClaimView extends Vue {
|
||||
},
|
||||
3000,
|
||||
);
|
||||
} else if (serverUtil.containsHiddenDid(this.giveDetails.fullClaim)) {
|
||||
} else if (serverUtil.containsHiddenDid(this.giveDetails?.fullClaim)) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -21,8 +21,12 @@
|
||||
Contacts.
|
||||
</span>
|
||||
<div v-if="sameCount > 0">
|
||||
{{ sameCount }} contact{{ sameCount == 1 ? "" : "s" }} are the same as
|
||||
existing contacts.
|
||||
<span v-if="sameCount == 1"
|
||||
>One contact is the same as an existing contact</span
|
||||
>
|
||||
<span v-else
|
||||
>{{ sameCount }} contacts are the same as existing contacts</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Results List -->
|
||||
|
||||
@@ -458,9 +458,9 @@ export default class ContactQRScanShow extends Vue {
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Copied",
|
||||
text: "Your DID was copied to the clipboard. Have them paste it on their 'People' screen to add you.",
|
||||
text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.",
|
||||
},
|
||||
10000,
|
||||
5000,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<QuickNav selected="Contacts"></QuickNav>
|
||||
<QuickNav selected="Contacts" />
|
||||
<TopMessage />
|
||||
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<!-- Heading -->
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||
@@ -37,18 +39,49 @@
|
||||
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
||||
@click="onClickNewContact()"
|
||||
>
|
||||
<fa icon="plus" class="fa-fw"></fa>
|
||||
<fa icon="plus" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="w-full text-right">
|
||||
<button
|
||||
href=""
|
||||
class="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-1 py-1 rounded-md"
|
||||
@click="toggleShowContactAmounts()"
|
||||
>
|
||||
{{ showGiveNumbers ? "Hide Given Hours" : "Show Given Hours" }}
|
||||
</button>
|
||||
<div class="flex justify-between" v-if="contacts.length > 0">
|
||||
<div class="w-full text-left">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-if="!showGiveNumbers"
|
||||
:checked="contactsSelected.length === contacts.length"
|
||||
@click="
|
||||
contactsSelected.length === contacts.length
|
||||
? (contactsSelected = [])
|
||||
: (contactsSelected = contacts.map((contact) => contact.did))
|
||||
"
|
||||
class="align-middle ml-2 h-6 w-6"
|
||||
data-testId="contactCheckAllTop"
|
||||
/>
|
||||
<button
|
||||
href=""
|
||||
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
|
||||
:style="
|
||||
contactsSelected.length > 0
|
||||
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
||||
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
||||
"
|
||||
@click="copySelectedContacts()"
|
||||
v-if="!showGiveNumbers"
|
||||
data-testId="copySelectedContactsButtonTop"
|
||||
>
|
||||
Copy Selections
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="w-full text-right">
|
||||
<button
|
||||
href=""
|
||||
class="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-1 py-1 rounded-md"
|
||||
@click="toggleShowContactAmounts()"
|
||||
>
|
||||
{{ showGiveNumbers ? "Hide Given Hours" : "Show Given Hours" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-1" v-if="showGiveNumbers">
|
||||
<div class="w-full text-right">
|
||||
@@ -82,147 +115,53 @@
|
||||
<ul
|
||||
id="listContacts"
|
||||
v-if="contacts.length > 0"
|
||||
class="border-t border-slate-300"
|
||||
class="border-t border-slate-300 mt-1"
|
||||
>
|
||||
<li
|
||||
class="border-b border-slate-300 pt-2.5 pb-4"
|
||||
v-for="contact in contacts"
|
||||
class="border-b border-slate-300 pt-1 pb-1"
|
||||
v-for="contact in filteredContacts()"
|
||||
:key="contact.did"
|
||||
data-testId="contactListItem"
|
||||
>
|
||||
<div class="grow overflow-hidden">
|
||||
<h2 class="text-base font-semibold">
|
||||
<div class="flex items-center">
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:iconSize="24"
|
||||
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
|
||||
@click="showLargeIdenticon = contact"
|
||||
/>
|
||||
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
||||
<button
|
||||
|
||||
<input
|
||||
type="checkbox"
|
||||
v-if="!showGiveNumbers"
|
||||
:checked="contactsSelected.includes(contact.did)"
|
||||
@click="
|
||||
contactEdit = contact;
|
||||
contactNewName = contact.name || '';
|
||||
contactsSelected.includes(contact.did)
|
||||
? contactsSelected.splice(
|
||||
contactsSelected.indexOf(contact.did),
|
||||
1,
|
||||
)
|
||||
: contactsSelected.push(contact.did)
|
||||
"
|
||||
title="Edit"
|
||||
>
|
||||
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1"></fa>
|
||||
</button>
|
||||
class="ml-2 h-6 w-6"
|
||||
data-testId="contactCheckOne"
|
||||
/>
|
||||
|
||||
<h2 class="text-base font-semibold ml-2">
|
||||
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
||||
</h2>
|
||||
|
||||
<router-link
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(contact.did),
|
||||
}"
|
||||
title="See more about this DID"
|
||||
title="See more about this person"
|
||||
>
|
||||
<fa icon="circle-info" class="text-blue-500 ml-4" />
|
||||
</router-link>
|
||||
</h2>
|
||||
<div class="text-sm truncate">
|
||||
Identifier:
|
||||
<button
|
||||
@click="
|
||||
libsUtil.doCopyTwoSecRedo(
|
||||
contact.did,
|
||||
() => (showDidCopy = !showDidCopy),
|
||||
)
|
||||
"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showDidCopy" class="text-green-500">Copied DID</span>
|
||||
{{ contact.did }}
|
||||
</div>
|
||||
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
||||
Public Key (base 64):
|
||||
<button
|
||||
@click="
|
||||
libsUtil.doCopyTwoSecRedo(
|
||||
contact.publicKeyBase64,
|
||||
() => (showPubKeyCopy = !showPubKeyCopy),
|
||||
)
|
||||
"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showPubKeyCopy" class="text-green-500"
|
||||
>Copied Key</span
|
||||
>
|
||||
{{ contact.publicKeyBase64 }}
|
||||
</div>
|
||||
<div class="text-sm truncate" v-if="contact.nextPubKeyHashB64">
|
||||
Next Public Key Hash (base 64):
|
||||
<button
|
||||
@click="
|
||||
libsUtil.doCopyTwoSecRedo(
|
||||
contact.nextPubKeyHashB64,
|
||||
() => (showPubKeyHashCopy = !showPubKeyHashCopy),
|
||||
)
|
||||
"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showPubKeyHashCopy" class="text-green-500"
|
||||
>Copied Hash</span
|
||||
>
|
||||
{{ contact.nextPubKeyHashB64 }}
|
||||
</div>
|
||||
|
||||
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
||||
<div v-if="activeDid">
|
||||
<button
|
||||
v-if="contact.seesMe && contact.did !== activeDid"
|
||||
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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
@click="confirmSetVisibility(contact, false)"
|
||||
title="They can see you"
|
||||
>
|
||||
<fa icon="eye" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!contact.seesMe && contact.did !== activeDid"
|
||||
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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
@click="confirmSetVisibility(contact, true)"
|
||||
title="They cannot see you"
|
||||
>
|
||||
<fa icon="eye-slash" class="fa-fw" />
|
||||
</button>
|
||||
<!-- otherwise it's this user so hide it -->
|
||||
<fa v-else icon="eye" class="text-white mx-2.5" />
|
||||
|
||||
<button
|
||||
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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
@click="checkVisibility(contact)"
|
||||
title="Check Visibility"
|
||||
v-if="contact.did !== activeDid"
|
||||
>
|
||||
<fa icon="rotate" class="fa-fw" />
|
||||
</button>
|
||||
<!-- otherwise it's this user so hide it -->
|
||||
<fa v-else icon="rotate" class="text-white mx-2.5" />
|
||||
|
||||
<button
|
||||
@click="confirmRegister(contact)"
|
||||
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 ml-6 px-2 py-1.5 rounded-md"
|
||||
v-if="contact.did !== activeDid"
|
||||
title="Registration"
|
||||
>
|
||||
<fa
|
||||
v-if="contact.registered"
|
||||
icon="person-circle-check"
|
||||
class="fa-fw"
|
||||
/>
|
||||
<fa v-else icon="person-circle-question" class="fa-fw" />
|
||||
</button>
|
||||
<!-- otherwise it's this user so hide it -->
|
||||
<fa v-else icon="rotate" class="text-white ml-6 px-2.5" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="confirmDeleteContact(contact)"
|
||||
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 px-2 py-1.5 rounded-md"
|
||||
title="Delete"
|
||||
>
|
||||
<fa icon="trash-can" class="fa-fw" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="showGiveNumbers && contact.did != activeDid"
|
||||
class="ml-auto flex gap-1.5"
|
||||
@@ -293,6 +232,34 @@
|
||||
</ul>
|
||||
<p v-else>There are no contacts.</p>
|
||||
|
||||
<div class="mt-2 w-full text-left" v-if="contacts.length > 0">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-if="!showGiveNumbers"
|
||||
:checked="contactsSelected.length === contacts.length"
|
||||
@click="
|
||||
contactsSelected.length === contacts.length
|
||||
? (contactsSelected = [])
|
||||
: (contactsSelected = contacts.map((contact) => contact.did))
|
||||
"
|
||||
class="align-middle ml-2 h-6 w-6"
|
||||
data-testId="contactCheckAllBottom"
|
||||
/>
|
||||
<button
|
||||
href=""
|
||||
class="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 ml-2 px-1 py-1 rounded-md"
|
||||
:style="
|
||||
contactsSelected.length > 0
|
||||
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
||||
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
||||
"
|
||||
@click="copySelectedContacts()"
|
||||
v-if="!showGiveNumbers"
|
||||
>
|
||||
Copy Selections
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<GiftedDialog ref="customGivenDialog" />
|
||||
<OfferDialog ref="customOfferDialog" />
|
||||
|
||||
@@ -308,33 +275,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="contactEdit !== null" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
|
||||
<input
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
placeholder="Name"
|
||||
v-model="contactNewName"
|
||||
/>
|
||||
<div class="flex justify-between">
|
||||
<button
|
||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
||||
@click="onClickSaveName(contactEdit, contactNewName)"
|
||||
>
|
||||
<fa icon="save" />
|
||||
</button>
|
||||
<span class="inline-block w-2" />
|
||||
<button
|
||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
||||
@click="onClickCancelName()"
|
||||
>
|
||||
<fa icon="ban" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -345,6 +285,7 @@ import { IndexableType } from "dexie";
|
||||
import * as R from "ramda";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import { AppString, NotificationIface } from "@/constants/app";
|
||||
import { db } from "@/db/index";
|
||||
@@ -366,9 +307,10 @@ import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||
import OfferDialog from "@/components/OfferDialog.vue";
|
||||
import TopMessage from "@/components/TopMessage.vue";
|
||||
|
||||
@Component({
|
||||
components: { GiftedDialog, EntityIcon, OfferDialog, QuickNav },
|
||||
components: { TopMessage, GiftedDialog, EntityIcon, OfferDialog, QuickNav },
|
||||
})
|
||||
export default class ContactsView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
@@ -379,6 +321,7 @@ export default class ContactsView extends Vue {
|
||||
contactInput = "";
|
||||
contactEdit: Contact | null = null;
|
||||
contactNewName = "";
|
||||
contactsSelected: Array<string> = [];
|
||||
// { "did:...": concatenated-descriptions } entry for each contact
|
||||
givenByMeDescriptions: Record<string, string> = {};
|
||||
// { "did:...": amount } entry for each contact
|
||||
@@ -404,7 +347,7 @@ export default class ContactsView extends Vue {
|
||||
AppString = AppString;
|
||||
libsUtil = libsUtil;
|
||||
|
||||
async created() {
|
||||
public async created() {
|
||||
await db.open();
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
@@ -427,7 +370,7 @@ export default class ContactsView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
danger(message: string, title: string = "Error", timeout = 5000) {
|
||||
private danger(message: string, title: string = "Error", timeout = 5000) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -439,7 +382,17 @@ export default class ContactsView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
async loadGives() {
|
||||
private filteredContacts() {
|
||||
return this.showGiveNumbers
|
||||
? this.contactsSelected.length === 0
|
||||
? this.contacts
|
||||
: this.contacts.filter((contact) =>
|
||||
this.contactsSelected.includes(contact.did),
|
||||
)
|
||||
: this.contacts;
|
||||
}
|
||||
|
||||
private async loadGives() {
|
||||
if (!this.activeDid) {
|
||||
return;
|
||||
}
|
||||
@@ -546,19 +499,20 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async onClickNewContact(): Promise<void> {
|
||||
if (!this.contactInput) {
|
||||
private async onClickNewContact(): Promise<void> {
|
||||
const contactInput = this.contactInput.trim();
|
||||
if (!contactInput) {
|
||||
this.danger("There was no contact info to add.", "No Contact");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
|
||||
await this.addContactFromScan(this.contactInput);
|
||||
if (contactInput.startsWith(CONTACT_URL_PREFIX)) {
|
||||
await this.addContactFromScan(contactInput);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.contactInput.startsWith(CONTACT_CSV_HEADER)) {
|
||||
const lines = this.contactInput.split(/\n/);
|
||||
if (contactInput.startsWith(CONTACT_CSV_HEADER)) {
|
||||
const lines = contactInput.split(/\n/);
|
||||
const lineAdded = [];
|
||||
for (const line of lines) {
|
||||
if (!line.trim() || line.startsWith(CONTACT_CSV_HEADER)) {
|
||||
@@ -590,44 +544,71 @@ export default class ContactsView extends Vue {
|
||||
return;
|
||||
}
|
||||
|
||||
let did = this.contactInput;
|
||||
let name, publicKeyInput, nextPublicKeyHashInput;
|
||||
const commaPos1 = this.contactInput.indexOf(",");
|
||||
if (commaPos1 > -1) {
|
||||
did = this.contactInput.substring(0, commaPos1).trim();
|
||||
name = this.contactInput.substring(commaPos1 + 1).trim();
|
||||
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
|
||||
if (commaPos2 > -1) {
|
||||
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
|
||||
publicKeyInput = this.contactInput.substring(commaPos2 + 1).trim();
|
||||
const commaPos3 = this.contactInput.indexOf(",", commaPos2 + 1);
|
||||
if (commaPos3 > -1) {
|
||||
publicKeyInput = this.contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier
|
||||
nextPublicKeyHashInput = this.contactInput.substring(commaPos3 + 1).trim(); // eslint-disable-line prettier/prettier
|
||||
if (contactInput.startsWith("did:")) {
|
||||
let did = contactInput;
|
||||
let name, publicKeyInput, nextPublicKeyHashInput;
|
||||
const commaPos1 = contactInput.indexOf(",");
|
||||
if (commaPos1 > -1) {
|
||||
did = contactInput.substring(0, commaPos1).trim();
|
||||
name = contactInput.substring(commaPos1 + 1).trim();
|
||||
const commaPos2 = contactInput.indexOf(",", commaPos1 + 1);
|
||||
if (commaPos2 > -1) {
|
||||
name = contactInput.substring(commaPos1 + 1, commaPos2).trim();
|
||||
publicKeyInput = contactInput.substring(commaPos2 + 1).trim();
|
||||
const commaPos3 = contactInput.indexOf(",", commaPos2 + 1);
|
||||
if (commaPos3 > -1) {
|
||||
publicKeyInput = contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier
|
||||
nextPublicKeyHashInput = contactInput.substring(commaPos3 + 1).trim(); // eslint-disable-line prettier/prettier
|
||||
}
|
||||
}
|
||||
}
|
||||
// help with potential mistakes while this sharing requires copy-and-paste
|
||||
let publicKeyBase64 = publicKeyInput;
|
||||
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
||||
// it must be all hex (compressed public key), so convert
|
||||
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString(
|
||||
"base64",
|
||||
);
|
||||
}
|
||||
let nextPubKeyHashB64 = nextPublicKeyHashInput;
|
||||
if (nextPubKeyHashB64 && /^[0-9A-Fa-f]{66}$/i.test(nextPubKeyHashB64)) {
|
||||
// it must be all hex (compressed public key), so convert
|
||||
nextPubKeyHashB64 = Buffer.from(nextPubKeyHashB64, "hex").toString("base64"); // eslint-disable-line prettier/prettier
|
||||
}
|
||||
const newContact = {
|
||||
did,
|
||||
name,
|
||||
publicKeyBase64,
|
||||
nextPubKeyHashB64: nextPubKeyHashB64,
|
||||
};
|
||||
await this.addContact(newContact);
|
||||
return;
|
||||
}
|
||||
// help with potential mistakes while this sharing requires copy-and-paste
|
||||
let publicKeyBase64 = publicKeyInput;
|
||||
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
||||
// it must be all hex (compressed public key), so convert
|
||||
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
||||
|
||||
if (contactInput.includes("[")) {
|
||||
// assume there's a JSON array of contacts in the input
|
||||
const jsonContactInput = contactInput.substring(
|
||||
contactInput.indexOf("["),
|
||||
contactInput.lastIndexOf("]") + 1,
|
||||
);
|
||||
try {
|
||||
const contacts = JSON.parse(jsonContactInput);
|
||||
(this.$router as Router).push({
|
||||
name: "contact-import",
|
||||
query: { contacts: JSON.stringify(contacts) },
|
||||
});
|
||||
} catch (e) {
|
||||
this.danger("The input could not be parsed.", "Invalid Contact List");
|
||||
}
|
||||
return;
|
||||
}
|
||||
let nextPubKeyHashB64 = nextPublicKeyHashInput;
|
||||
if (nextPubKeyHashB64 && /^[0-9A-Fa-f]{66}$/i.test(nextPubKeyHashB64)) {
|
||||
// it must be all hex (compressed public key), so convert
|
||||
nextPubKeyHashB64 = Buffer.from(nextPubKeyHashB64, "hex").toString("base64"); // eslint-disable-line prettier/prettier
|
||||
}
|
||||
const newContact = {
|
||||
did,
|
||||
name,
|
||||
publicKeyBase64,
|
||||
nextPubKeyHashB64: nextPubKeyHashB64,
|
||||
};
|
||||
await this.addContact(newContact);
|
||||
|
||||
this.danger("No contact info was found in that input.", "No Contact Info");
|
||||
}
|
||||
|
||||
async addContactFromEndorserMobileLine(line: string): Promise<IndexableType> {
|
||||
private async addContactFromEndorserMobileLine(
|
||||
line: string,
|
||||
): Promise<IndexableType> {
|
||||
// Note that Endorser Mobile puts name first, then did, etc.
|
||||
let name = line;
|
||||
let did = "";
|
||||
@@ -668,7 +649,7 @@ export default class ContactsView extends Vue {
|
||||
return db.contacts.add(newContact);
|
||||
}
|
||||
|
||||
async addContactFromScan(url: string): Promise<void> {
|
||||
private async addContactFromScan(url: string): Promise<void> {
|
||||
const payload = getContactPayloadFromJwtUrl(url);
|
||||
if (!payload) {
|
||||
this.$notify(
|
||||
@@ -693,7 +674,7 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async addContact(newContact: Contact) {
|
||||
private async addContact(newContact: Contact) {
|
||||
if (!newContact.did) {
|
||||
this.danger("Cannot add a contact without a DID.", "Incomplete Contact");
|
||||
return;
|
||||
@@ -782,56 +763,30 @@ export default class ContactsView extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
// prompt with confirmation if they want to delete a contact
|
||||
confirmDeleteContact(contact: Contact) {
|
||||
// note that this is also in DIDView.vue
|
||||
private async confirmSetVisibility(contact: Contact, visibility: boolean) {
|
||||
const visibilityPrompt = visibility
|
||||
? "Are you sure you want to make your activity visible to them?"
|
||||
: "Are you sure you want to hide all your activity from them?";
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Delete",
|
||||
text:
|
||||
"Are you sure you want to remove " +
|
||||
this.nameForDid(this.contacts, contact.did) +
|
||||
" with DID " +
|
||||
contact.did +
|
||||
" from your contact list?",
|
||||
title: "Set Visibility",
|
||||
text: visibilityPrompt,
|
||||
onYes: async () => {
|
||||
await this.deleteContact(contact);
|
||||
const success = await this.setVisibility(contact, visibility, true);
|
||||
if (success) {
|
||||
contact.seesMe = visibility; // didn't work inside setVisibility
|
||||
}
|
||||
},
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
async deleteContact(contact: Contact) {
|
||||
await db.open();
|
||||
await db.contacts.delete(contact.did);
|
||||
this.contacts = R.without([contact], this.contacts);
|
||||
}
|
||||
|
||||
// confirm to register a new contact
|
||||
async confirmRegister(contact: Contact) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Register",
|
||||
text:
|
||||
"Are you sure you want to register " +
|
||||
this.nameForDid(this.contacts, contact.did) +
|
||||
(contact.registered
|
||||
? " -- especially since they are already marked as registered"
|
||||
: "") +
|
||||
"?",
|
||||
onYes: async () => {
|
||||
await this.register(contact);
|
||||
},
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
async register(contact: Contact) {
|
||||
// note that this is also in DIDView.vue
|
||||
private async register(contact: Contact) {
|
||||
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
|
||||
|
||||
try {
|
||||
@@ -896,28 +851,8 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async confirmSetVisibility(contact: Contact, visibility: boolean) {
|
||||
const visibilityPrompt = visibility
|
||||
? "Are you sure you want to make your activity visible to them?"
|
||||
: "Are you sure you want to hide all your activity from them?";
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Set Visibility",
|
||||
text: visibilityPrompt,
|
||||
onYes: async () => {
|
||||
const success = await this.setVisibility(contact, visibility, true);
|
||||
if (success) {
|
||||
contact.seesMe = visibility; // didn't work inside setVisibility
|
||||
}
|
||||
},
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
async setVisibility(
|
||||
// note that this is also in DIDView.vue
|
||||
private async setVisibility(
|
||||
contact: Contact,
|
||||
visibility: boolean,
|
||||
showSuccessAlert: boolean,
|
||||
@@ -966,7 +901,8 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async checkVisibility(contact: Contact) {
|
||||
// note that this is also in DIDView.vue
|
||||
private async checkVisibility(contact: Contact) {
|
||||
const url =
|
||||
this.apiServer +
|
||||
"/api/report/canDidExplicitlySeeMe?did=" +
|
||||
@@ -999,7 +935,7 @@ export default class ContactsView extends Vue {
|
||||
type: "info",
|
||||
title: "Visibility Refreshed",
|
||||
text:
|
||||
this.nameForContact(contact, true) +
|
||||
libsUtil.nameForContact(contact, true) +
|
||||
" can " +
|
||||
(visibility ? "" : "not ") +
|
||||
"see your activity.",
|
||||
@@ -1033,22 +969,7 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
private nameForDid(contacts: Array<Contact>, did: string): string {
|
||||
if (did === this.activeDid) {
|
||||
return "you";
|
||||
}
|
||||
const contact = R.find((con) => con.did == did, contacts);
|
||||
return this.nameForContact(contact);
|
||||
}
|
||||
|
||||
private nameForContact(contact?: Contact, capitalize?: boolean): string {
|
||||
return (
|
||||
(contact?.name as string) ||
|
||||
(capitalize ? "This" : "this") + " unnamed user"
|
||||
);
|
||||
}
|
||||
|
||||
confirmShowGiftedDialog(giverDid: string, recipientDid: string) {
|
||||
private confirmShowGiftedDialog(giverDid: string, recipientDid: string) {
|
||||
// if they have unconfirmed amounts, ask to confirm those
|
||||
if (
|
||||
recipientDid === this.activeDid &&
|
||||
@@ -1093,13 +1014,13 @@ export default class ContactsView extends Vue {
|
||||
if (giverDid) {
|
||||
giver = {
|
||||
did: giverDid,
|
||||
name: this.nameForDid(this.contacts, giverDid),
|
||||
name: libsUtil.nameForDid(this.activeDid, this.contacts, giverDid),
|
||||
};
|
||||
}
|
||||
if (recipientDid) {
|
||||
receiver = {
|
||||
did: recipientDid,
|
||||
name: this.nameForDid(this.contacts, recipientDid),
|
||||
name: libsUtil.nameForDid(this.activeDid, this.contacts, recipientDid),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1131,26 +1052,14 @@ export default class ContactsView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
openOfferDialog(recipientDid: string, recipientName: string) {
|
||||
openOfferDialog(recipientDid: string, recipientName?: string) {
|
||||
(this.$refs.customOfferDialog as OfferDialog).open(
|
||||
recipientDid,
|
||||
recipientName,
|
||||
);
|
||||
}
|
||||
|
||||
private async onClickCancelName() {
|
||||
this.contactEdit = null;
|
||||
this.contactNewName = "";
|
||||
}
|
||||
|
||||
private async onClickSaveName(contact: Contact, newName: string) {
|
||||
contact.name = newName;
|
||||
return db.contacts
|
||||
.update(contact.did, { name: newName })
|
||||
.then(() => (this.contactEdit = null));
|
||||
}
|
||||
|
||||
public async toggleShowContactAmounts() {
|
||||
private async toggleShowContactAmounts() {
|
||||
const newShowValue = !this.showGiveNumbers;
|
||||
try {
|
||||
await db.open();
|
||||
@@ -1186,7 +1095,7 @@ export default class ContactsView extends Vue {
|
||||
this.loadGives();
|
||||
}
|
||||
}
|
||||
public toggleShowGiveTotals() {
|
||||
private toggleShowGiveTotals() {
|
||||
if (this.showGiveTotals) {
|
||||
this.showGiveTotals = false;
|
||||
this.showGiveConfirmed = true;
|
||||
@@ -1199,7 +1108,7 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
public showGiveAmountsClassNames() {
|
||||
private showGiveAmountsClassNames() {
|
||||
return {
|
||||
"from-slate-400": this.showGiveTotals,
|
||||
"to-slate-700": this.showGiveTotals,
|
||||
@@ -1209,76 +1118,31 @@ export default class ContactsView extends Vue {
|
||||
"to-yellow-700": !this.showGiveTotals && !this.showGiveConfirmed,
|
||||
};
|
||||
}
|
||||
|
||||
private copySelectedContacts() {
|
||||
if (this.contactsSelected.length === 0) {
|
||||
this.danger("You must select contacts to copy.");
|
||||
return;
|
||||
}
|
||||
const selectedContacts = this.contacts.filter((c) =>
|
||||
this.contactsSelected.includes(c.did),
|
||||
);
|
||||
const message =
|
||||
"To add contacts, paste this into the box on the 'People' screen.\n\n" +
|
||||
JSON.stringify(selectedContacts, null, 2);
|
||||
useClipboard()
|
||||
.copy(message)
|
||||
.then(() => {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Copied",
|
||||
text: "Those contacts were copied to the clipboard. Have them paste it in the box on their 'People' screen.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/*
|
||||
Tooltip, generated on "title" attributes on "fa" icons
|
||||
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
||||
*/
|
||||
/* Tooltip container */
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
|
||||
}
|
||||
/* Tooltip text */
|
||||
.tooltip .tooltiptext {
|
||||
visibility: hidden;
|
||||
width: 200px;
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
border-radius: 6px;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
/* How do we share with the above so code isn't duplicated? */
|
||||
.tooltip .tooltiptext-left {
|
||||
visibility: hidden;
|
||||
width: 200px;
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
border-radius: 6px;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
bottom: 0%;
|
||||
right: 105%;
|
||||
margin-left: -60px;
|
||||
}
|
||||
|
||||
/* Show the tooltip text when you mouse over the tooltip container */
|
||||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
.tooltip:hover .tooltiptext-left {
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,12 +22,21 @@
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">
|
||||
{{
|
||||
didInfoForContact(viewingDid, activeDid, contact, allMyDids)
|
||||
.displayName
|
||||
}}
|
||||
{{ contact?.name || "(no name)" }}
|
||||
<button
|
||||
@click="
|
||||
contactEdit = true;
|
||||
contactNewName = contact.name || '';
|
||||
"
|
||||
title="Edit"
|
||||
>
|
||||
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||
</button>
|
||||
</h2>
|
||||
<button @click="showDidDetails = !showDidDetails" class="ml-2 mr-2">
|
||||
<button
|
||||
@click="showDidDetails = !showDidDetails"
|
||||
class="ml-2 mr-2 mt-4"
|
||||
>
|
||||
Details
|
||||
<fa v-if="showDidDetails" icon="chevron-up" class="text-blue-400" />
|
||||
<fa v-else icon="chevron-down" class="text-blue-400" />
|
||||
@@ -49,15 +58,76 @@
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="flex justify-center">Auto-Generated Icon:</div>
|
||||
<div class="flex justify-center">
|
||||
<EntityIcon
|
||||
:entityId="viewingDid"
|
||||
:iconSize="64"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
@click="showLargeIdenticonId = viewingDid"
|
||||
/>
|
||||
<div class="flex justify-between mt-4">
|
||||
<div class="flex items-center">
|
||||
<div v-if="activeDid" class="flex justify-between">
|
||||
<div>
|
||||
<button
|
||||
v-if="contact?.seesMe && contact.did !== activeDid"
|
||||
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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
@click="confirmSetVisibility(contact, false)"
|
||||
title="They can see you"
|
||||
>
|
||||
<fa icon="eye" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!contact?.seesMe && contact?.did !== activeDid"
|
||||
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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
@click="confirmSetVisibility(contact, true)"
|
||||
title="They cannot see you"
|
||||
>
|
||||
<fa icon="eye-slash" class="fa-fw" />
|
||||
</button>
|
||||
<!-- otherwise it's this user so hide it -->
|
||||
<fa v-else icon="eye" class="text-white mx-2.5" />
|
||||
|
||||
<button
|
||||
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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
@click="checkVisibility(contact)"
|
||||
title="Check Visibility"
|
||||
v-if="contact?.did !== activeDid"
|
||||
>
|
||||
<fa icon="rotate" class="fa-fw" />
|
||||
</button>
|
||||
<!-- otherwise it's this user so hide it -->
|
||||
<fa v-else icon="rotate" class="text-white mx-2.5" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="confirmRegister(contact)"
|
||||
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 ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
v-if="contact?.did !== activeDid"
|
||||
title="Registration"
|
||||
>
|
||||
<fa
|
||||
v-if="contact?.registered"
|
||||
icon="person-circle-check"
|
||||
class="fa-fw"
|
||||
/>
|
||||
<fa v-else icon="person-circle-question" class="fa-fw" />
|
||||
</button>
|
||||
<!-- otherwise it's this user so hide it -->
|
||||
<fa v-else icon="rotate" class="text-white ml-6 px-2.5" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="confirmDeleteContact(contact)"
|
||||
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="Delete"
|
||||
>
|
||||
<fa icon="trash-can" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!contact?.profileImageUrl">
|
||||
<div>Auto-Generated Icon</div>
|
||||
<div class="flex justify-center">
|
||||
<EntityIcon
|
||||
:entityId="viewingDid"
|
||||
:iconSize="64"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
@click="showLargeIdenticonId = viewingDid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -80,6 +150,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="contactEdit" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
|
||||
<input
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
placeholder="Name"
|
||||
v-model="contactNewName"
|
||||
/>
|
||||
<div class="flex justify-between">
|
||||
<button
|
||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
||||
@click="onClickSaveName(contactNewName)"
|
||||
>
|
||||
<fa icon="save" />
|
||||
</button>
|
||||
<span class="inline-block w-2" />
|
||||
<button
|
||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
||||
@click="onClickCancelName()"
|
||||
>
|
||||
<fa icon="ban" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Animation -->
|
||||
<div
|
||||
@@ -126,15 +222,16 @@
|
||||
v-if="!isLoading && claims.length === 0"
|
||||
class="flex justify-center mt-4"
|
||||
>
|
||||
<span>They Are in No Claims Visible to You</span>
|
||||
<span>They are in no claims visible to you.</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { AxiosError } from "axios";
|
||||
import * as yaml from "js-yaml";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||
@@ -152,6 +249,8 @@ import {
|
||||
GenericVerifiableCredential,
|
||||
GiveVerifiableCredential,
|
||||
OfferVerifiableCredential,
|
||||
register,
|
||||
setVisibilityUtil,
|
||||
} from "@/libs/endorserServer";
|
||||
import * as libsUtil from "@/libs/util";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
@@ -174,7 +273,9 @@ export default class DIDView extends Vue {
|
||||
allMyDids: Array<string> = [];
|
||||
apiServer = "";
|
||||
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
|
||||
contact?: Contact;
|
||||
contact: Contact;
|
||||
contactEdit = false;
|
||||
contactNewName?: string;
|
||||
contactYaml = "";
|
||||
hitEnd = false;
|
||||
isLoading = false;
|
||||
@@ -195,23 +296,29 @@ export default class DIDView extends Vue {
|
||||
this.apiServer = (settings?.apiServer as string) || "";
|
||||
|
||||
const pathParam = window.location.pathname.substring("/did/".length);
|
||||
let theContact: Contact | undefined;
|
||||
if (pathParam) {
|
||||
this.viewingDid = decodeURIComponent(pathParam);
|
||||
this.contact = await db.contacts.get(this.viewingDid);
|
||||
this.contactYaml = yaml.dump(this.contact);
|
||||
await this.loadClaimsAbout();
|
||||
theContact = await db.contacts.get(this.viewingDid);
|
||||
}
|
||||
if (theContact) {
|
||||
this.contact = theContact;
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "No claim ID was provided.",
|
||||
text: "No valid claim ID was provided.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.contactYaml = yaml.dump(this.contact);
|
||||
await this.loadClaimsAbout();
|
||||
|
||||
await accountsDB.open();
|
||||
const allAccounts = await accountsDB.accounts.toArray();
|
||||
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||
@@ -227,6 +334,128 @@ export default class DIDView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
// prompt with confirmation if they want to delete a contact
|
||||
confirmDeleteContact(contact: Contact) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Delete",
|
||||
text:
|
||||
"Are you sure you want to remove " +
|
||||
libsUtil.nameForContact(contact, false) +
|
||||
" from your contact list?",
|
||||
onYes: async () => {
|
||||
await this.deleteContact(contact);
|
||||
},
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
async deleteContact(contact: Contact) {
|
||||
await db.open();
|
||||
await db.contacts.delete(contact.did);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Deleted",
|
||||
text: "Contact has been removed.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
(this.$router as Router).push({ name: "contacts" });
|
||||
}
|
||||
|
||||
// confirm to register a new contact
|
||||
async confirmRegister(contact: Contact) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Register",
|
||||
text:
|
||||
"Are you sure you want to register " +
|
||||
libsUtil.nameForContact(this.contact, false) +
|
||||
(contact.registered
|
||||
? " -- especially since they are already marked as registered"
|
||||
: "") +
|
||||
"?",
|
||||
onYes: async () => {
|
||||
await this.register(contact);
|
||||
},
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
// note that this is also in ContactView.vue
|
||||
async register(contact: Contact) {
|
||||
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
|
||||
|
||||
try {
|
||||
const regResult = await register(
|
||||
this.activeDid,
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
contact,
|
||||
);
|
||||
if (regResult.success) {
|
||||
contact.registered = true;
|
||||
await db.contacts.update(contact.did, { registered: true });
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Registration Success",
|
||||
text:
|
||||
(contact.name || "That unnamed person") + " has been registered.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Registration Error",
|
||||
text:
|
||||
(regResult.error as string) ||
|
||||
"Something went wrong during registration.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error when registering:", error);
|
||||
let userMessage = "There was an error. See logs for more info.";
|
||||
const serverError = error as AxiosError;
|
||||
if (serverError) {
|
||||
if (serverError.response?.data?.error?.message) {
|
||||
userMessage = serverError.response.data.error.message;
|
||||
} else 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: "Registration Error",
|
||||
text: userMessage,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async loadClaimsAbout() {
|
||||
if (!this.viewingDid) {
|
||||
console.error("This should never be called without a DID.");
|
||||
@@ -323,5 +552,178 @@ export default class DIDView extends Vue {
|
||||
claimDescription(claim: GenericVerifiableCredential) {
|
||||
return claim.claim.name || claim.claim.description || "";
|
||||
}
|
||||
|
||||
private async onClickCancelName() {
|
||||
this.contactEdit = false;
|
||||
}
|
||||
|
||||
private async onClickSaveName(newName: string) {
|
||||
this.contact.name = newName;
|
||||
return db.contacts
|
||||
.update(this.contact.did, { name: newName })
|
||||
.then(() => (this.contactEdit = false));
|
||||
}
|
||||
|
||||
// note that this is also in ContactView.vue
|
||||
async confirmSetVisibility(contact: Contact, visibility: boolean) {
|
||||
const visibilityPrompt = visibility
|
||||
? "Are you sure you want to make your activity visible to them?"
|
||||
: "Are you sure you want to hide all your activity from them?";
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Set Visibility",
|
||||
text: visibilityPrompt,
|
||||
onYes: async () => {
|
||||
const success = await this.setVisibility(contact, visibility, true);
|
||||
if (success) {
|
||||
contact.seesMe = visibility; // didn't work inside setVisibility
|
||||
}
|
||||
},
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
// note that this is also in ContactView.vue
|
||||
async setVisibility(
|
||||
contact: Contact,
|
||||
visibility: boolean,
|
||||
showSuccessAlert: boolean,
|
||||
) {
|
||||
const result = await setVisibilityUtil(
|
||||
this.activeDid,
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
db,
|
||||
contact,
|
||||
visibility,
|
||||
);
|
||||
if (result.success) {
|
||||
//contact.seesMe = visibility; // why doesn't it affect the UI from here?
|
||||
//console.log("Set result & seesMe", result, contact.seesMe, contact.did);
|
||||
if (showSuccessAlert) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Visibility Set",
|
||||
text:
|
||||
(contact.name || "That user") +
|
||||
" can " +
|
||||
(visibility ? "" : "not ") +
|
||||
"see your activity.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.error("Got strange result from setting visibility:", result);
|
||||
const message =
|
||||
(result.error as string) || "Could not set visibility on the server.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Setting Visibility",
|
||||
text: message,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// note that this is also in ContactView.vue
|
||||
async checkVisibility(contact: Contact) {
|
||||
const url =
|
||||
this.apiServer +
|
||||
"/api/report/canDidExplicitlySeeMe?did=" +
|
||||
encodeURIComponent(contact.did);
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
if (!headers["Authorization"]) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "No Identity",
|
||||
text: "There is no identity to use to check visibility.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
if (resp.status === 200) {
|
||||
const visibility = resp.data;
|
||||
contact.seesMe = visibility;
|
||||
//console.log("Visi check:", visibility, contact.seesMe, contact.did);
|
||||
await db.contacts.update(contact.did, { seesMe: visibility });
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Visibility Refreshed",
|
||||
text:
|
||||
libsUtil.nameForContact(contact, true) +
|
||||
" can " +
|
||||
(visibility ? "" : "not ") +
|
||||
"see your activity.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
} else {
|
||||
console.error("Got bad server response checking visibility:", resp);
|
||||
const message = resp.data.error?.message || "Got bad server response.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Checking Visibility",
|
||||
text: message,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Caught error from request to check visibility:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Checking Visibility",
|
||||
text: "Check connectivity and try again.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -54,8 +54,9 @@
|
||||
<h2 class="text-xl font-semibold">How do I get started?</h2>
|
||||
<p>
|
||||
You need someone to register you, like the person who told you
|
||||
about this app, on the Contacts
|
||||
<fa icon="users" class="fa-fw" /> page. After they register you, you can
|
||||
about this app, on the Contacts <fa icon="users" class="fa-fw" /> page.
|
||||
If you heard about this from our outreach, feel free to contact us (below) for a chat.
|
||||
After someone registers you, you can
|
||||
select any contact on the home page (or "anonymous") and record your
|
||||
appreciation for... whatever. The main goal is to record what people
|
||||
have given you, to grow giving economies. You can also record your own
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Heading -->
|
||||
<h1 class="text-4xl text-center font-light px-4 mb-4">What Was Offered</h1>
|
||||
<h1 class="text-4xl text-center font-light px-4 mb-4">What Is Offered</h1>
|
||||
|
||||
<h1 class="text-xl font-bold text-center mb-4">
|
||||
<span>
|
||||
@@ -34,7 +34,7 @@
|
||||
</h1>
|
||||
<textarea
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
placeholder="What was offered"
|
||||
placeholder="What is offered"
|
||||
v-model="itemDescription"
|
||||
data-testId="itemDescription"
|
||||
/>
|
||||
@@ -107,7 +107,7 @@
|
||||
<label class="text-sm mt-1">
|
||||
{{
|
||||
projectId
|
||||
? "This was given to " + projectName
|
||||
? "This is offered to " + projectName
|
||||
: "No project was chosen"
|
||||
}}
|
||||
</label>
|
||||
@@ -129,7 +129,7 @@
|
||||
<label class="text-sm mt-1">
|
||||
{{
|
||||
recipientDid
|
||||
? "This was given to " + recipientName
|
||||
? "This is offered to " + recipientName
|
||||
: "No recipient was chosen."
|
||||
}}
|
||||
</label>
|
||||
@@ -275,7 +275,7 @@ export default class OfferDetailsView extends Vue {
|
||||
// find any project ID
|
||||
let project;
|
||||
if (
|
||||
this.prevCredToEdit?.claim?.itemOffered?.isPartOf["@type"] ===
|
||||
this.prevCredToEdit?.claim?.itemOffered?.isPartOf?.["@type"] ===
|
||||
"PlanAction"
|
||||
) {
|
||||
project = this.prevCredToEdit?.claim?.itemOffered?.isPartOf;
|
||||
@@ -421,7 +421,7 @@ export default class OfferDetailsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
text: "Recording the give...",
|
||||
text: "Recording the offer...",
|
||||
title: "",
|
||||
},
|
||||
1000,
|
||||
@@ -527,13 +527,13 @@ export default class OfferDetailsView extends Vue {
|
||||
|
||||
if (result.type === "error" || this.isCreationError(result.response)) {
|
||||
const errorMessage = this.getCreationErrorMessage(result);
|
||||
console.error("Error with give creation result:", result);
|
||||
console.error("Error with offer creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: errorMessage || "There was an error creating the give.",
|
||||
text: errorMessage || "There was an error creating the offer.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -556,11 +556,11 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Error with give recordation caught:", error);
|
||||
console.error("Error with offer recordation caught:", error);
|
||||
const errorMessage =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
"There was an error recording the give.";
|
||||
"There was an error recording the offer.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -578,7 +578,7 @@ export default class OfferDetailsView extends Vue {
|
||||
? this.recipientDid
|
||||
: undefined;
|
||||
const projectId = this.offeredToProject ? this.projectId : undefined;
|
||||
const giveClaim = hydrateOffer(
|
||||
const offerClaim = hydrateOffer(
|
||||
this.prevCredToEdit?.claim as OfferVerifiableCredential,
|
||||
this.activeDid,
|
||||
recipientDid,
|
||||
@@ -590,7 +590,7 @@ export default class OfferDetailsView extends Vue {
|
||||
this.validThroughDateInput,
|
||||
this.prevCredToEdit?.id as string,
|
||||
);
|
||||
const claimStr = JSON.stringify(giveClaim);
|
||||
const claimStr = JSON.stringify(offerClaim);
|
||||
return claimStr;
|
||||
}
|
||||
|
||||
|
||||
27
test-playwright/05-install-pwa.spec.ts
Normal file
27
test-playwright/05-install-pwa.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test('Install PWA', async ({ page, context }) => {
|
||||
await page.goto('./');
|
||||
|
||||
// Wait for the service worker to register
|
||||
await page.waitForSelector('service-worker-registered-indicator', {
|
||||
timeout: 10000, // Adjust timeout according to your needs
|
||||
});
|
||||
|
||||
// Trigger the install prompt manually
|
||||
const [installPrompt] = await Promise.all([
|
||||
page.waitForEvent('beforeinstallprompt'),
|
||||
page.evaluate(() => {
|
||||
window.dispatchEvent(new Event('beforeinstallprompt'));
|
||||
}),
|
||||
]);
|
||||
|
||||
// Accept the install prompt
|
||||
await installPrompt.prompt();
|
||||
|
||||
// Check if the PWA was installed successfully
|
||||
const result = await installPrompt.userChoice;
|
||||
expect(result.outcome).toBe('accepted');
|
||||
|
||||
// Additional checks go here
|
||||
});
|
||||
@@ -12,8 +12,8 @@ test('Create new project, then search for it', async ({ page }) => {
|
||||
const finalRandomString = randomString.substring(0, 16);
|
||||
|
||||
// Standard texts
|
||||
const standardTitle = "Idea ";
|
||||
const standardDescription = "Description of Idea ";
|
||||
const standardTitle = 'Idea ';
|
||||
const standardDescription = 'Description of Idea ';
|
||||
|
||||
// Combine texts with the random string
|
||||
const finalTitle = standardTitle + finalRandomString;
|
||||
|
||||
68
test-playwright/25-create-project-x10.spec.ts
Normal file
68
test-playwright/25-create-project-x10.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { importUser } from './testUtils';
|
||||
|
||||
// Function to generate a random string of specified length
|
||||
function generateRandomString(length) {
|
||||
return Math.random().toString(36).substring(2, 2 + length);
|
||||
}
|
||||
|
||||
// Function to create an array of unique strings
|
||||
function createUniqueStringsArray(count) {
|
||||
const stringsArray = [];
|
||||
const stringLength = 16;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let randomString = generateRandomString(stringLength);
|
||||
stringsArray.push(randomString);
|
||||
}
|
||||
|
||||
return stringsArray;
|
||||
}
|
||||
|
||||
test('Create 10 new projects', async ({ page }) => {
|
||||
test.slow(); // Extend the test timeout
|
||||
const projectCount = 10;
|
||||
|
||||
// Standard texts
|
||||
const standardTitle = "Idea ";
|
||||
const standardDescription = "Description of Idea ";
|
||||
|
||||
// Title and description arrays
|
||||
const finalTitles = [];
|
||||
const finalDescriptions = [];
|
||||
|
||||
// Create an array of unique strings
|
||||
const uniqueStrings = createUniqueStringsArray(projectCount);
|
||||
|
||||
// Populate arrays with titles and descriptions
|
||||
for (let i = 0; i < projectCount; i++) {
|
||||
let loopTitle = standardTitle + uniqueStrings[i];
|
||||
finalTitles.push(loopTitle);
|
||||
let loopDescription = standardDescription + uniqueStrings[i];
|
||||
finalDescriptions.push(loopDescription);
|
||||
}
|
||||
|
||||
// Import user 00
|
||||
await importUser(page, '00');
|
||||
|
||||
// Pause a bit
|
||||
await page.waitForTimeout(3000); // I have to wait, otherwise the (+) button to add a new project doesn't appear
|
||||
|
||||
// Create new projects
|
||||
for (let i = 0; i < projectCount; i++) {
|
||||
await page.goto('./projects');
|
||||
await page.getByRole('link', { name: 'Projects', exact: true }).click();
|
||||
await page.getByRole('button').click();
|
||||
await page.getByPlaceholder('Idea Name').fill(finalTitles[i]); // Add random suffix
|
||||
await page.getByPlaceholder('Description').fill(finalDescriptions[i]);
|
||||
await page.getByPlaceholder('Website').fill('https://example.com');
|
||||
await page.getByPlaceholder('Start Date').fill('2025-12-01');
|
||||
await page.getByPlaceholder('Start Time').fill('12:00');
|
||||
await page.getByRole('button', { name: 'Save Project' }).click();
|
||||
await page.waitForTimeout(1000); // Compensate for delay in loading Idea Name heading
|
||||
|
||||
// Check texts
|
||||
await expect(page.locator('h2')).toContainText(finalTitles[i]);
|
||||
await expect(page.locator('#Content')).toContainText(finalDescriptions[i]);
|
||||
}
|
||||
});
|
||||
@@ -2,23 +2,17 @@ import { test, expect } from '@playwright/test';
|
||||
import { importUser } from './testUtils';
|
||||
|
||||
test('Record something given', async ({ page }) => {
|
||||
// Generate a random string of 16 characters
|
||||
let randomString = Math.random().toString(36).substring(2, 18);
|
||||
|
||||
// In case the string is shorter than 16 characters, generate more characters until it is 16 characters long
|
||||
while (randomString.length < 16) {
|
||||
randomString += Math.random().toString(36).substring(2, 18);
|
||||
}
|
||||
const finalRandomString = randomString.substring(0, 16);
|
||||
// Generate a random string of a few characters
|
||||
const randomString = Math.random().toString(36).substring(2, 6);
|
||||
|
||||
// Generate a random non-zero single-digit number
|
||||
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;
|
||||
|
||||
// Standard title prefix
|
||||
const standardTitle = "Gift ";
|
||||
const standardTitle = 'Gift ';
|
||||
|
||||
// Combine title prefix with the random string
|
||||
const finalTitle = standardTitle + finalRandomString;
|
||||
const finalTitle = standardTitle + randomString;
|
||||
|
||||
// Import user 00
|
||||
await importUser(page, '00');
|
||||
|
||||
77
test-playwright/33-record-gift-x10.spec.ts
Normal file
77
test-playwright/33-record-gift-x10.spec.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { importUser } from './testUtils';
|
||||
|
||||
// Function to generate a random string of specified length
|
||||
function generateRandomString(length) {
|
||||
return Math.random().toString(36).substring(2, 2 + length);
|
||||
}
|
||||
|
||||
// Function to create an array of unique strings
|
||||
function createUniqueStringsArray(count) {
|
||||
const stringsArray = [];
|
||||
const stringLength = 16;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let randomString = generateRandomString(stringLength);
|
||||
stringsArray.push(randomString);
|
||||
}
|
||||
|
||||
return stringsArray;
|
||||
}
|
||||
|
||||
// Function to create an array of two-digit non-zero numbers
|
||||
function createRandomNumbersArray(count) {
|
||||
const numbersArray = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let randomNumber = Math.floor(Math.random() * 99) + 1;
|
||||
numbersArray.push(randomNumber);
|
||||
}
|
||||
|
||||
return numbersArray;
|
||||
}
|
||||
|
||||
test('Record 10 new gifts', async ({ page }) => {
|
||||
test.slow(); // Extend the test timeout
|
||||
const giftCount = 10;
|
||||
|
||||
// Standard text
|
||||
const standardTitle = "Gift ";
|
||||
|
||||
// Field value arrays
|
||||
const finalTitles = [];
|
||||
const finalNumbers = [];
|
||||
|
||||
// Create arrays for field input
|
||||
const uniqueStrings = createUniqueStringsArray(giftCount);
|
||||
const randomNumbers = createRandomNumbersArray(giftCount);
|
||||
|
||||
// Populate array with titles
|
||||
for (let i = 0; i < giftCount; i++) {
|
||||
let loopTitle = standardTitle + uniqueStrings[i];
|
||||
finalTitles.push(loopTitle);
|
||||
let loopNumber = randomNumbers[i];
|
||||
finalNumbers.push(loopNumber);
|
||||
}
|
||||
|
||||
// Import user 00
|
||||
await importUser(page, '00');
|
||||
|
||||
// Pause a bit
|
||||
await page.waitForTimeout(3000); // I have to wait, otherwise the (+) button to add a new project doesn't appear
|
||||
|
||||
// Record new gifts
|
||||
for (let i = 0; i < giftCount; i++) {
|
||||
// Record something given
|
||||
await page.goto('./');
|
||||
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
|
||||
await page.getByPlaceholder('What was given').fill(finalTitles[i]);
|
||||
await page.getByRole('spinbutton').fill(finalNumbers[i].toString());
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
||||
|
||||
// Refresh home view and check gift
|
||||
await page.goto('./');
|
||||
await expect(page.locator('li').filter({ hasText: finalTitles[i] })).toBeVisible();
|
||||
}
|
||||
});
|
||||
@@ -15,31 +15,31 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
||||
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;
|
||||
|
||||
// Standard title prefix
|
||||
const standardTitle = "Gift ";
|
||||
const standardTitle = 'Gift ';
|
||||
|
||||
// Combine title prefix with the random string
|
||||
const finalTitle = standardTitle + finalRandomString;
|
||||
|
||||
// Contact name
|
||||
const contactName = 'Contact 00';
|
||||
const contactName = 'Contact #111';
|
||||
|
||||
// Import user 01
|
||||
await importUser(page, '01');
|
||||
|
||||
// Add new contact 00
|
||||
// Add new contact
|
||||
await page.goto('./contacts');
|
||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F');
|
||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F, User #000');
|
||||
await page.locator('button > svg.fa-plus').click();
|
||||
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
||||
|
||||
// Why doesn't the alert box come up every time?
|
||||
// await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
||||
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
||||
|
||||
// Verify added contact
|
||||
await expect(page.locator('li.border-b')).toContainText('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F');
|
||||
await expect(page.locator('li.border-b')).toContainText('User #000');
|
||||
|
||||
// Rename contact
|
||||
await page.locator('li.border-b h2 > button[title="Edit"]').click();
|
||||
await page.locator('li.border-b div div > a[title="See more about this person"]').click();
|
||||
await page.locator('h2 > button > svg.fa-pen').click();
|
||||
await expect(page.locator('div.dialog-overlay > div.dialog').filter({ hasText: 'Edit Name' })).toBeVisible();
|
||||
await page.getByPlaceholder('Name', { exact: true }).fill(contactName);
|
||||
await page.locator('.dialog > .flex > button').first().click();
|
||||
@@ -51,7 +51,7 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
||||
// Record something given by new contact
|
||||
await page.getByRole('heading', { name: contactName }).click();
|
||||
await page.getByPlaceholder('What was given').fill(finalTitle);
|
||||
await page.getByRole('spinbutton', { id: 'inputGivenAmount' }).fill(randomNonZeroNumber.toString());
|
||||
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
||||
|
||||
@@ -75,11 +75,73 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
||||
await page.locator('li').filter({ hasText: finalTitle }).locator('a').click();
|
||||
|
||||
// Confirm gift as user 00
|
||||
await page.getByTestId('confirmGiftLink').click();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await page.getByRole('button', { name: 'Yes' }).click();
|
||||
await expect(page.getByText('Confirmation submitted.')).toBeVisible();
|
||||
|
||||
// Refresh claim page, Confirm button should be hidden
|
||||
// Refresh claim page, Confirm button should throw an alert because they already confirmed
|
||||
await page.reload();
|
||||
await expect(page.getByRole('button', { name: 'Confirm' })).toBeHidden();
|
||||
});
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Add contact, copy details, delete, and import various ways', async ({ page, context }) => {
|
||||
await importUser(page, '00');
|
||||
|
||||
// Add new contact
|
||||
await page.goto('./contacts');
|
||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39, User #111');
|
||||
await page.locator('button > svg.fa-plus').click();
|
||||
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
||||
await page.locator('div[role="alert"] button:has-text("No")').click();
|
||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click();
|
||||
// wait for the alert to disappear
|
||||
await expect(page.locator('div[role="alert"]')).toBeHidden();
|
||||
|
||||
// Add another new contact
|
||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b, User #222, asdf1234');
|
||||
await page.locator('button > svg.fa-plus').click();
|
||||
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
||||
await page.locator('div[role="alert"] button:has-text("No")').click();
|
||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click();
|
||||
await expect(page.locator('div[role="alert"]')).toBeHidden();
|
||||
|
||||
await expect(page.getByTestId('contactListItem')).toHaveCount(2);
|
||||
|
||||
//// Copy contact details, export them, remove them, and paste to add them
|
||||
|
||||
// Copy contact details
|
||||
await page.getByTestId('contactCheckAllTop').click();
|
||||
await page.getByTestId('copySelectedContactsButtonTop').click();
|
||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
|
||||
// I would prefer to copy from the clipboard, but the recommended approaches don't work.
|
||||
// this seems to fail in non-chromium browsers
|
||||
//await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||
// this seems to fail in chromium (at least) where clipboard is undefined
|
||||
//const contactData = await navigator.clipboard.readText();
|
||||
|
||||
// see contact details on the second contact
|
||||
await page.getByTestId('contactListItem').nth(1).locator('a').click();
|
||||
// remove contact
|
||||
await page.locator('button > svg.fa-trash-can').click();
|
||||
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
||||
// for some reason, .isHidden() (without expect) doesn't work
|
||||
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
|
||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
|
||||
|
||||
// go to the contacts page and paste the copied contact details
|
||||
await page.goto('./contacts');
|
||||
// check that there are fewer contacts
|
||||
await expect(page.getByTestId('contactListItem')).toHaveCount(1);
|
||||
|
||||
const contactData = 'Paste this: [{ "did": "did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39", "name": "User #111" }, { "did": "did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b", "name": "User #222", "publicKeyBase64": "asdf1234"}] '
|
||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(contactData);
|
||||
await page.locator('button > svg.fa-plus').click();
|
||||
// we're on the contact-import page
|
||||
await expect(page.locator('li', { hasText: 'New' })).toHaveCount(1);
|
||||
await expect(page.locator('span').filter({ hasText: 'the same as' })).toBeVisible();
|
||||
await page.locator('button', { hasText: 'Import' }).click();
|
||||
// check that there are more contacts
|
||||
await expect(page.getByTestId('contactListItem')).toHaveCount(2);
|
||||
});
|
||||
|
||||
@@ -2,10 +2,11 @@ import { test, expect } from '@playwright/test';
|
||||
import { importUser } from './testUtils';
|
||||
|
||||
test('Record an offer', async ({ page }) => {
|
||||
// Generate a random string of 6 characters, skipping the "0." at the beginning
|
||||
const randomString = Math.random().toString(36).substring(2, 8);
|
||||
// Generate a random string of 3 characters, skipping the "0." at the beginning
|
||||
const randomString = Math.random().toString(36).substring(2, 5);
|
||||
// Standard title prefix
|
||||
const finalTitle = `Offering of ${randomString}`;
|
||||
const description = `Offering of ${randomString}`;
|
||||
const updatedDescription = `Updated ${description}`;
|
||||
const randomNonZeroNumber = Math.floor(Math.random() * 998) + 1;
|
||||
|
||||
// Create new ID for default user
|
||||
@@ -17,46 +18,46 @@ test('Record an offer', async ({ page }) => {
|
||||
|
||||
// Record an offer
|
||||
await page.getByTestId('offerButton').click();
|
||||
await page.getByTestId('inputDescription').fill(finalTitle);
|
||||
await page.getByTestId('inputDescription').fill(description);
|
||||
await page.getByTestId('inputOfferAmount').fill(randomNonZeroNumber.toString());
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
await expect(page.getByText('That offer was recorded.')).toBeVisible();
|
||||
|
||||
// go to the offer and check the values
|
||||
await page.goto('./projects');
|
||||
await page.locator('li').filter({ hasText: finalTitle }).locator('a').first().click();
|
||||
await page.locator('li').filter({ hasText: description }).locator('a').first().click();
|
||||
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
||||
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
||||
await expect(page.getByText(description, { exact: true })).toBeVisible();
|
||||
const serverPagePromise = page.waitForEvent('popup');
|
||||
await page.getByRole('link', { name: 'View on the Public Server' }).click();
|
||||
const serverPage = await serverPagePromise;
|
||||
await serverPage.getByText(finalTitle);
|
||||
await serverPage.getByText(description);
|
||||
await serverPage.getByText('did:none:HIDDEN');
|
||||
|
||||
// Now update that offer
|
||||
|
||||
// find the edit page and check the old values again
|
||||
await page.goto('./projects');
|
||||
await page.locator('li').filter({ hasText: finalTitle }).locator('a').first().click();
|
||||
await page.locator('li').filter({ hasText: description }).locator('a').first().click();
|
||||
await page.getByTestId('editClaimButton').click();
|
||||
await page.locator('heading', { hasText: 'What was offered' }).isVisible();
|
||||
await page.locator('heading', { hasText: 'What is offered' }).isVisible();
|
||||
const itemDesc = await page.getByTestId('itemDescription');
|
||||
await expect(itemDesc).toHaveValue(finalTitle);
|
||||
await expect(itemDesc).toHaveValue(description);
|
||||
const amount = await page.getByTestId('inputOfferAmount');
|
||||
await expect(amount).toHaveValue(randomNonZeroNumber.toString());
|
||||
// update the values
|
||||
await itemDesc.fill('Updated ' + finalTitle);
|
||||
await itemDesc.fill(updatedDescription);
|
||||
await amount.fill(String(randomNonZeroNumber + 1));
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
|
||||
// go to the offer claim again and check the updated values
|
||||
await page.goto('./projects');
|
||||
await page.locator('li').filter({ hasText: finalTitle }).locator('a').first().click();
|
||||
await page.locator('li').filter({ hasText: description }).locator('a').first().click();
|
||||
const newItemDesc = await page.getByTestId('description');
|
||||
await expect(newItemDesc).toHaveText(finalTitle);
|
||||
await expect(newItemDesc).toHaveText(updatedDescription);
|
||||
|
||||
// go to edit page
|
||||
await page.getByTestId('editClaimButton').click();
|
||||
const newAmount = await page.getByTestId('inputOfferAmount');
|
||||
await expect(newAmount).toHaveValue(randomNonZeroNumber.toString());
|
||||
await expect(newAmount).toHaveValue((randomNonZeroNumber + 1).toString());
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { expect, Page } from '@playwright/test';
|
||||
|
||||
export async function importUser(page, id) {
|
||||
export async function importUser(page: Page, id?: string): Promise<void> {
|
||||
let seedPhrase, userName, did;
|
||||
|
||||
// Set seed phrase and DID based on user ID
|
||||
Reference in New Issue
Block a user