Compare commits

...

20 Commits

Author SHA1 Message Date
Jose Olarte III
a810d531b7 In-progress: PWA install test 2024-08-22 21:24:05 +08:00
Jose Olarte III
91445cc240 Merge branch 'master' into test-playwright 2024-08-22 18:05:35 +08:00
85b9aa8e2b fix linting 2024-08-21 19:16:00 -06:00
7309ba1436 add tests for importing multiple records, fix other confirmation tests 2024-08-21 18:43:28 -06:00
Jose Olarte III
07efab3782 Playwright: Record 10 gives 2024-08-21 19:47:48 +08:00
Jose Olarte III
375cda1082 Merge branch 'master' into test-playwright 2024-08-21 15:22:22 +08:00
67b0122d5a fix tests 2024-08-20 20:05:04 -06:00
6aef08d7e8 copy a list of contacts and then import 2024-08-20 19:39:29 -06:00
a5248af4a3 move contact actions into the details page (prepping for checkboxes) 2024-08-19 20:18:06 -06:00
d9f45d52f9 bump verison and add "-beta" 2024-08-18 17:51:31 -06:00
dc80b686ce bump to version 0.3.20 2024-08-18 17:04:14 -06:00
892cf4c595 update bad verbiage on offer page, fix offer test 2024-08-18 17:02:15 -06:00
e2b641736d bump verison and add "-beta" 2024-08-18 14:58:01 -06:00
bb1fc7519f bump to version 0.3.19 2024-08-18 14:14:53 -06:00
014d4081e6 fix error editing an offer, tweak tests to fix red in IntelliJ 2024-08-18 14:13:42 -06:00
877678b745 bump to version 0.3.18 2024-08-18 13:52:55 -06:00
a3da157ae3 Merge pull request 'offer editing' (#123) from offer-edit into master
Reviewed-on: #123
2024-08-18 15:48:53 -04:00
Jose Olarte III
713faebf51 DONE: create 10 projects 2024-08-12 19:54:25 +08:00
Jose Olarte III
93a230298d Merge branch 'master' into test-playwright 2024-08-12 16:40:05 +08:00
Jose Olarte III
799c8d66c0 IN-PROGRESS: create 10 projects 2024-08-09 23:04:25 +08:00
25 changed files with 1040 additions and 489 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "TimeSafari",
"version": "0.3.18-beta",
"version": "0.3.21-beta",
"scripts": {
"dev": "vite",
"serve": "vite preview",

View File

@@ -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 */
/**

View File

@@ -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">

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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()

View File

@@ -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"

View File

@@ -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>

View File

@@ -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",

View File

@@ -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 -->

View File

@@ -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,
);
});
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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;
}

View 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
});

View File

@@ -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;

View 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]);
}
});

View File

@@ -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');

View 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();
}
});

View File

@@ -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);
});

View File

@@ -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());
});

View File

@@ -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