forked from jsnbuchanan/crowd-funder-for-time-pwa
Merge branch 'master' into test-playwright
This commit is contained in:
24
CHANGELOG.md
24
CHANGELOG.md
@@ -6,7 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
## [0.3.16] - 2024.07.10
|
## ?
|
||||||
|
### 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
|
||||||
### Added
|
### Added
|
||||||
- Photos on more screens
|
- Photos on more screens
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -2,5 +2,10 @@
|
|||||||
|
|
||||||
Welcome! We are happy to have your help with this project.
|
Welcome! We are happy to have your help with this project.
|
||||||
|
|
||||||
Note that all contributions will be under our
|
We expect contributions to include automated tests and pass linting. Run the `test-all` task.
|
||||||
[license, modeled after SQLite](https://github.com/trentlarson/endorser-ch/blob/master/LICENSE).
|
Note that some previous features don't have tests and adding more will make you friends quick.
|
||||||
|
|
||||||
|
Note that all contributions will be under our [license, modeled after SQLite](https://github.com/trentlarson/endorser-ch/blob/master/LICENSE).
|
||||||
|
|
||||||
|
If you want to see a code of conduct, we're probably not the people you want to hang with.
|
||||||
|
Basically, we'll work together as long as we both enjoy it, and we'll stop when that stops.
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ npm run lint
|
|||||||
|
|
||||||
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
||||||
|
|
||||||
|
* Commit everything (since the commit hash is used the app).
|
||||||
|
|
||||||
* Record what version is currently on production.
|
* Record what version is currently on production.
|
||||||
|
|
||||||
* Run the correct build:
|
* Run the correct build:
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari",
|
||||||
"version": "0.3.17-beta",
|
"version": "0.3.21-beta",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari",
|
||||||
"version": "0.3.17-beta",
|
"version": "0.3.21-beta",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.1",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari",
|
||||||
"version": "0.3.17-beta",
|
"version": "0.3.21-beta",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
/* Configure global timeout */
|
/* Configure global timeout; default is 30000 milliseconds */
|
||||||
// the image upload will often not succeed at 5 seconds
|
// the image upload will often not succeed at 5 seconds
|
||||||
//timeout: 7000,
|
timeout: 20000,
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
/**
|
/**
|
||||||
@@ -86,7 +86,7 @@ export default defineConfig({
|
|||||||
* },
|
* },
|
||||||
*
|
*
|
||||||
* But if we do then the testInfo.config.webServer is null and the API-setting test 00 fails.
|
* But if we do then the testInfo.config.webServer is null and the API-setting test 00 fails.
|
||||||
* It is worth considering a change such that Time Safari's default Endorer API server is NOT set
|
* It is worth considering a change such that Time Safari's default Endorser API server is NOT set
|
||||||
* in the user's settings so that it can be blanked out and the default is used.
|
* in the user's settings so that it can be blanked out and the default is used.
|
||||||
*/
|
*/
|
||||||
webServer: {
|
webServer: {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
}"
|
}"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
Photo & Details ...
|
Photo & more options ...
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<h1 class="text-xl font-bold text-center mb-4">Offer Help</h1>
|
<h1 class="text-xl font-bold text-center mb-4">Offer Help</h1>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
data-testId="inputDescription"
|
||||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||||
placeholder="Description, prerequisites, terms, etc."
|
placeholder="Description, prerequisites, terms, etc."
|
||||||
v-model="description"
|
v-model="description"
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
<fa icon="chevron-left" />
|
<fa icon="chevron-left" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
data-testId="inputOfferAmount"
|
||||||
type="number"
|
type="number"
|
||||||
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
||||||
v-model="amountInput"
|
v-model="amountInput"
|
||||||
@@ -34,18 +36,27 @@
|
|||||||
<fa icon="chevron-right" />
|
<fa icon="chevron-right" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row mt-2">
|
<div class="mt-4 flex justify-center">
|
||||||
<span
|
<span>
|
||||||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'offer-details',
|
||||||
|
query: {
|
||||||
|
amountInput,
|
||||||
|
description,
|
||||||
|
offererDid: activeDid,
|
||||||
|
projectId,
|
||||||
|
projectName,
|
||||||
|
recipientDid,
|
||||||
|
recipientName,
|
||||||
|
unitCode: amountUnitCode,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
Expiration
|
Conditions & more options...
|
||||||
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="w-full border border-slate-400 px-2 py-2 rounded-r"
|
|
||||||
:placeholder="datePlaceholder()"
|
|
||||||
v-model="expirationDateInput"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-center mt-6 mb-2 italic">
|
<p class="text-center mt-6 mb-2 italic">
|
||||||
Sign & Send to publish to the world
|
Sign & Send to publish to the world
|
||||||
@@ -69,7 +80,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DateTime } from "luxon";
|
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface } from "@/constants/app";
|
import { NotificationIface } from "@/constants/app";
|
||||||
@@ -82,7 +92,8 @@ import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
|||||||
export default class OfferDialog extends Vue {
|
export default class OfferDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
@Prop projectId? = "";
|
@Prop projectId?;
|
||||||
|
@Prop projectName?;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
@@ -92,13 +103,15 @@ export default class OfferDialog extends Vue {
|
|||||||
description = "";
|
description = "";
|
||||||
expirationDateInput = "";
|
expirationDateInput = "";
|
||||||
recipientDid? = "";
|
recipientDid? = "";
|
||||||
|
recipientName? = "";
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
|
|
||||||
async open(recipientDid?: string) {
|
async open(recipientDid?: string, recipientName?: string) {
|
||||||
try {
|
try {
|
||||||
this.recipientDid = recipientDid;
|
this.recipientDid = recipientDid;
|
||||||
|
this.recipientName = recipientName;
|
||||||
|
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
@@ -144,12 +157,6 @@ export default class OfferDialog extends Vue {
|
|||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
datePlaceholder() {
|
|
||||||
return (
|
|
||||||
"Date, eg. " + DateTime.now().plus({ month: 1 }).toISO().slice(0, 10)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.close();
|
this.close();
|
||||||
this.eraseValues();
|
this.eraseValues();
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ export interface Contact {
|
|||||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
||||||
profileImageUrl?: string;
|
profileImageUrl?: string;
|
||||||
publicKeyBase64?: string;
|
publicKeyBase64?: string;
|
||||||
seesMe?: boolean;
|
seesMe?: boolean; // cached value of the server setting
|
||||||
registered?: boolean;
|
registered?: boolean; // cached value of the server setting
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContactSchema = {
|
export const ContactSchema = {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export interface ClaimResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericVerifiableCredential {
|
export interface GenericVerifiableCredential {
|
||||||
"@context"?: string;
|
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||||
"@type": string;
|
"@type": string;
|
||||||
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,7 @@ export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
|||||||
id: string;
|
id: string;
|
||||||
issuedAt: string;
|
issuedAt: string;
|
||||||
issuer: string;
|
issuer: string;
|
||||||
|
publicUrls?: Record<string, string>; // only for IDs that want to be public
|
||||||
}
|
}
|
||||||
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
||||||
{
|
{
|
||||||
@@ -139,13 +140,13 @@ export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
|||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||||
// https://endorser.ch/doc/html/transactions.html#id8
|
// https://endorser.ch/doc/html/transactions.html#id8
|
||||||
export interface OfferVerifiableCredential {
|
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
||||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
"@context"?: string; // optional when embedded... though it doesn't make sense to agree to an offer
|
||||||
"@type": "Offer";
|
"@type": "Offer";
|
||||||
description?: string;
|
description?: string; // conditions for the offer
|
||||||
includesObject?: { amountOfThisGood: number; unitCode: string };
|
includesObject?: { amountOfThisGood: number; unitCode: string };
|
||||||
itemOffered?: {
|
itemOffered?: {
|
||||||
description?: string;
|
description?: string; // description of the item
|
||||||
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
|
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
|
||||||
};
|
};
|
||||||
offeredBy?: { identifier: string };
|
offeredBy?: { identifier: string };
|
||||||
@@ -155,7 +156,7 @@ export interface OfferVerifiableCredential {
|
|||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||||
// https://endorser.ch/doc/html/transactions.html#id7
|
// https://endorser.ch/doc/html/transactions.html#id7
|
||||||
export interface PlanVerifiableCredential {
|
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
||||||
"@context": "https://schema.org";
|
"@context": "https://schema.org";
|
||||||
"@type": "PlanAction";
|
"@type": "PlanAction";
|
||||||
name: string;
|
name: string;
|
||||||
@@ -267,10 +268,6 @@ export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
|||||||
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
||||||
const HIDDEN_DID = "did:none:HIDDEN";
|
const HIDDEN_DID = "did:none:HIDDEN";
|
||||||
|
|
||||||
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
|
|
||||||
max: 500,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function isDid(did: string) {
|
export function isDid(did: string) {
|
||||||
return did.startsWith("did:");
|
return did.startsWith("did:");
|
||||||
}
|
}
|
||||||
@@ -507,6 +504,10 @@ export async function getHeaders(did?: string) {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
|
||||||
|
max: 500,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param handleId nullable, in which case "undefined" will be returned
|
* @param handleId nullable, in which case "undefined" will be returned
|
||||||
* @param requesterDid optional, in which case no private info will be returned
|
* @param requesterDid optional, in which case no private info will be returned
|
||||||
@@ -563,6 +564,8 @@ export async function setPlanInCache(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct GiveAction VC for submission to server
|
* Construct GiveAction VC for submission to server
|
||||||
|
*
|
||||||
|
* @param lastClaimId supplied when editing a previous claim
|
||||||
*/
|
*/
|
||||||
export function hydrateGive(
|
export function hydrateGive(
|
||||||
vcClaimOrig?: GiveVerifiableCredential,
|
vcClaimOrig?: GiveVerifiableCredential,
|
||||||
@@ -587,6 +590,7 @@ export function hydrateGive(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (lastClaimId) {
|
if (lastClaimId) {
|
||||||
|
// this is an edit
|
||||||
vcClaim.lastClaimId = lastClaimId;
|
vcClaim.lastClaimId = lastClaimId;
|
||||||
delete vcClaim.identifier;
|
delete vcClaim.identifier;
|
||||||
}
|
}
|
||||||
@@ -594,7 +598,8 @@ export function hydrateGive(
|
|||||||
vcClaim.agent = fromDid ? { identifier: fromDid } : undefined;
|
vcClaim.agent = fromDid ? { identifier: fromDid } : undefined;
|
||||||
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
||||||
vcClaim.description = description || undefined;
|
vcClaim.description = description || undefined;
|
||||||
vcClaim.object = amount
|
vcClaim.object =
|
||||||
|
amount && !isNaN(amount)
|
||||||
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -603,7 +608,7 @@ export function hydrateGive(
|
|||||||
vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : [];
|
vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : [];
|
||||||
}
|
}
|
||||||
// ... and replace or add each element, ending with Trade or Donate
|
// ... and replace or add each element, ending with Trade or Donate
|
||||||
// I realize this doesn't change any elements that are not PlanAction or Offer or Trade/Action.
|
// I realize the following doesn't change any elements that are not PlanAction or Offer or Trade/Action.
|
||||||
vcClaim.fulfills = vcClaim.fulfills.filter(
|
vcClaim.fulfills = vcClaim.fulfills.filter(
|
||||||
(elem) => elem["@type"] !== "PlanAction",
|
(elem) => elem["@type"] !== "PlanAction",
|
||||||
);
|
);
|
||||||
@@ -639,8 +644,8 @@ export function hydrateGive(
|
|||||||
*
|
*
|
||||||
* @param fromDid may be null
|
* @param fromDid may be null
|
||||||
* @param toDid
|
* @param toDid
|
||||||
* @param description may be null; should have this or amount
|
* @param description may be null
|
||||||
* @param amount may be null; should have this or description
|
* @param amount may be null
|
||||||
*/
|
*/
|
||||||
export async function createAndSubmitGive(
|
export async function createAndSubmitGive(
|
||||||
axios: Axios,
|
axios: Axios,
|
||||||
@@ -667,6 +672,7 @@ export async function createAndSubmitGive(
|
|||||||
fulfillsOfferHandleId,
|
fulfillsOfferHandleId,
|
||||||
isTrade,
|
isTrade,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
return createAndSubmitClaim(
|
return createAndSubmitClaim(
|
||||||
vcClaim as GenericVerifiableCredential,
|
vcClaim as GenericVerifiableCredential,
|
||||||
@@ -680,9 +686,9 @@ export async function createAndSubmitGive(
|
|||||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
*
|
*
|
||||||
* @param fromDid may be null
|
* @param fromDid may be null
|
||||||
* @param toDid
|
* @param toDid may be null if project is provided
|
||||||
* @param description may be null; should have this or amount
|
* @param description may be null
|
||||||
* @param amount may be null; should have this or description
|
* @param amount may be null
|
||||||
*/
|
*/
|
||||||
export async function editAndSubmitGive(
|
export async function editAndSubmitGive(
|
||||||
axios: Axios,
|
axios: Axios,
|
||||||
@@ -720,51 +726,128 @@ export async function editAndSubmitGive(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct Offer VC for submission to server
|
||||||
|
*
|
||||||
|
* @param lastClaimId supplied when editing a previous claim
|
||||||
|
*/
|
||||||
|
export function hydrateOffer(
|
||||||
|
vcClaimOrig?: OfferVerifiableCredential,
|
||||||
|
fromDid?: string,
|
||||||
|
toDid?: string,
|
||||||
|
itemDescription?: string,
|
||||||
|
amount?: number,
|
||||||
|
unitCode?: string,
|
||||||
|
conditionDescription?: string,
|
||||||
|
fulfillsProjectHandleId?: string,
|
||||||
|
validThrough?: string,
|
||||||
|
lastClaimId?: string,
|
||||||
|
): OfferVerifiableCredential {
|
||||||
|
// Remember: replace values or erase if it's null
|
||||||
|
|
||||||
|
const vcClaim: OfferVerifiableCredential = vcClaimOrig
|
||||||
|
? R.clone(vcClaimOrig)
|
||||||
|
: {
|
||||||
|
"@context": SCHEMA_ORG_CONTEXT,
|
||||||
|
"@type": "Offer",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastClaimId) {
|
||||||
|
// this is an edit
|
||||||
|
vcClaim.lastClaimId = lastClaimId;
|
||||||
|
delete vcClaim.identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
vcClaim.offeredBy = fromDid ? { identifier: fromDid } : undefined;
|
||||||
|
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
||||||
|
vcClaim.description = conditionDescription || undefined;
|
||||||
|
|
||||||
|
vcClaim.includesObject =
|
||||||
|
amount && !isNaN(amount)
|
||||||
|
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (itemDescription || fulfillsProjectHandleId) {
|
||||||
|
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
||||||
|
vcClaim.itemOffered.description = itemDescription || undefined;
|
||||||
|
if (fulfillsProjectHandleId) {
|
||||||
|
vcClaim.itemOffered.isPartOf = {
|
||||||
|
"@type": "PlanAction",
|
||||||
|
identifier: fulfillsProjectHandleId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vcClaim.validThrough = validThrough || undefined;
|
||||||
|
|
||||||
|
return vcClaim;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
*
|
*
|
||||||
* @param identity
|
* @param identity
|
||||||
* @param description may be null; should have this or amount
|
* @param description may be null
|
||||||
* @param amount may be null; should have this or description
|
* @param amount may be null
|
||||||
* @param expirationDate ISO 8601 date string YYYY-MM-DD (may be null)
|
* @param validThrough ISO 8601 date string YYYY-MM-DD (may be null)
|
||||||
* @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
|
* @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
|
||||||
*/
|
*/
|
||||||
export async function createAndSubmitOffer(
|
export async function createAndSubmitOffer(
|
||||||
axios: Axios,
|
axios: Axios,
|
||||||
apiServer: string,
|
apiServer: string,
|
||||||
issuerDid: string,
|
issuerDid: string,
|
||||||
description?: string,
|
itemDescription: string,
|
||||||
amount?: number,
|
amount?: number,
|
||||||
unitCode?: string,
|
unitCode?: string,
|
||||||
expirationDate?: string,
|
conditionDescription?: string,
|
||||||
|
validThrough?: string,
|
||||||
recipientDid?: string,
|
recipientDid?: string,
|
||||||
fulfillsProjectHandleId?: string,
|
fulfillsProjectHandleId?: string,
|
||||||
): Promise<CreateAndSubmitClaimResult> {
|
): Promise<CreateAndSubmitClaimResult> {
|
||||||
const vcClaim: OfferVerifiableCredential = {
|
const vcClaim = hydrateOffer(
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
undefined,
|
||||||
"@type": "Offer",
|
issuerDid,
|
||||||
offeredBy: { identifier: issuerDid },
|
recipientDid,
|
||||||
validThrough: expirationDate || undefined,
|
itemDescription,
|
||||||
};
|
amount,
|
||||||
if (amount) {
|
unitCode,
|
||||||
vcClaim.includesObject = {
|
conditionDescription,
|
||||||
amountOfThisGood: amount,
|
fulfillsProjectHandleId,
|
||||||
unitCode: unitCode || "HUR",
|
validThrough,
|
||||||
};
|
undefined,
|
||||||
}
|
);
|
||||||
if (description) {
|
return createAndSubmitClaim(
|
||||||
vcClaim.itemOffered = { description };
|
vcClaim as OfferVerifiableCredential,
|
||||||
}
|
issuerDid,
|
||||||
if (recipientDid) {
|
apiServer,
|
||||||
vcClaim.recipient = { identifier: recipientDid };
|
axios,
|
||||||
}
|
);
|
||||||
if (fulfillsProjectHandleId) {
|
|
||||||
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
|
||||||
vcClaim.itemOffered.isPartOf = {
|
|
||||||
"@type": "PlanAction",
|
|
||||||
identifier: fulfillsProjectHandleId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function editAndSubmitOffer(
|
||||||
|
axios: Axios,
|
||||||
|
apiServer: string,
|
||||||
|
fullClaim: GenericCredWrapper<OfferVerifiableCredential>,
|
||||||
|
issuerDid: string,
|
||||||
|
itemDescription: string,
|
||||||
|
amount?: number,
|
||||||
|
unitCode?: string,
|
||||||
|
conditionDescription?: string,
|
||||||
|
validThrough?: string,
|
||||||
|
recipientDid?: string,
|
||||||
|
fulfillsProjectHandleId?: string,
|
||||||
|
): Promise<CreateAndSubmitClaimResult> {
|
||||||
|
const vcClaim = hydrateOffer(
|
||||||
|
fullClaim.claim,
|
||||||
|
issuerDid,
|
||||||
|
recipientDid,
|
||||||
|
itemDescription,
|
||||||
|
amount,
|
||||||
|
unitCode,
|
||||||
|
conditionDescription,
|
||||||
|
fulfillsProjectHandleId,
|
||||||
|
validThrough,
|
||||||
|
fullClaim.id,
|
||||||
|
);
|
||||||
return createAndSubmitClaim(
|
return createAndSubmitClaim(
|
||||||
vcClaim as OfferVerifiableCredential,
|
vcClaim as OfferVerifiableCredential,
|
||||||
issuerDid,
|
issuerDid,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
// many of these are also found in endorser-mobile utility.ts
|
// many of these are also found in endorser-mobile utility.ts
|
||||||
|
|
||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
|
import { Buffer } from "buffer";
|
||||||
|
import * as R from "ramda";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import {
|
import {
|
||||||
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
||||||
MASTER_SETTINGS_KEY,
|
MASTER_SETTINGS_KEY,
|
||||||
@@ -18,11 +21,9 @@ import {
|
|||||||
OfferVerifiableCredential,
|
OfferVerifiableCredential,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import * as serverUtil 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 { KeyMeta } from "@/libs/crypto/vc";
|
||||||
import { createPeerDid } from "@/libs/crypto/vc/didPeer";
|
import { createPeerDid } from "@/libs/crypto/vc/didPeer";
|
||||||
|
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";
|
||||||
|
|
||||||
export const PRIVACY_MESSAGE =
|
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.";
|
"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.";
|
||||||
@@ -30,8 +31,8 @@ export const SHARED_PHOTO_BASE64_KEY = "shared-photo-base64";
|
|||||||
|
|
||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
export const UNIT_SHORT: Record<string, string> = {
|
export const UNIT_SHORT: Record<string, string> = {
|
||||||
"BX": "BX",
|
|
||||||
"BTC": "BTC",
|
"BTC": "BTC",
|
||||||
|
"BX": "BX",
|
||||||
"ETH": "ETH",
|
"ETH": "ETH",
|
||||||
"HUR": "Hours",
|
"HUR": "Hours",
|
||||||
"USD": "US $",
|
"USD": "US $",
|
||||||
@@ -40,8 +41,8 @@ export const UNIT_SHORT: Record<string, string> = {
|
|||||||
|
|
||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
export const UNIT_LONG: Record<string, string> = {
|
export const UNIT_LONG: Record<string, string> = {
|
||||||
"BX": "Buxbe",
|
|
||||||
"BTC": "Bitcoin",
|
"BTC": "Bitcoin",
|
||||||
|
"BX": "Buxbe",
|
||||||
"ETH": "Ethereum",
|
"ETH": "Ethereum",
|
||||||
"HUR": "hours",
|
"HUR": "hours",
|
||||||
"USD": "dollars",
|
"USD": "dollars",
|
||||||
@@ -91,6 +92,28 @@ export const isGiveAction = (
|
|||||||
return veriClaim.claimType === "GiveAction";
|
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) => {
|
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
|
||||||
fn();
|
fn();
|
||||||
useClipboard()
|
useClipboard()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { register } from "register-service-worker";
|
import { register } from "register-service-worker";
|
||||||
|
|
||||||
|
// NODE_ENV is "production" by default with "vite build". See https://vitejs.dev/guide/env-and-mode
|
||||||
if (import.meta.env.NODE_ENV === "production") {
|
if (import.meta.env.NODE_ENV === "production") {
|
||||||
register("/sw_scripts-combined.js", {
|
register("/sw_scripts-combined.js", {
|
||||||
ready() {
|
ready() {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
{
|
{
|
||||||
path: "/gifted-details",
|
path: "/gifted-details",
|
||||||
name: "gifted-details",
|
name: "gifted-details",
|
||||||
component: () => import("../views/GiftedDetails.vue"),
|
component: () => import("@/views/GiftedDetailsView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/help",
|
path: "/help",
|
||||||
@@ -143,6 +143,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "new-identifier",
|
name: "new-identifier",
|
||||||
component: () => import("../views/NewIdentifierView.vue"),
|
component: () => import("../views/NewIdentifierView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/offer-details/:id?",
|
||||||
|
name: "offer-details",
|
||||||
|
component: () => import("../views/OfferDetailsView.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/project/:id?",
|
path: "/project/:id?",
|
||||||
name: "project",
|
name: "project",
|
||||||
@@ -189,6 +194,9 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "shared-photo",
|
name: "shared-photo",
|
||||||
component: () => import("@/views/SharedPhotoView.vue"),
|
component: () => import("@/views/SharedPhotoView.vue"),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /share-target is also an endpoint in the service worker
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/start",
|
path: "/start",
|
||||||
name: "start",
|
name: "start",
|
||||||
|
|||||||
@@ -831,10 +831,16 @@ export default class AccountViewView extends Vue {
|
|||||||
* Beware! I've seen where we never get to this point because "ready" never resolves.
|
* Beware! I've seen where we never get to this point because "ready" never resolves.
|
||||||
*/
|
*/
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// this can happen when running automated tests in dev mode because notifications don't work
|
||||||
console.error(
|
console.error(
|
||||||
"Telling user to clear cache at page create because:",
|
"Telling user to clear cache at page create because:",
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
|
// this sometimes gives different information on the error
|
||||||
|
console.error(
|
||||||
|
"Telling user to clear cache at page create because (error added): " +
|
||||||
|
error,
|
||||||
|
);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -1282,17 +1288,6 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleRateLimitsError(error);
|
this.handleRateLimitsError(error);
|
||||||
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
isRegistered: false,
|
|
||||||
});
|
|
||||||
this.isRegistered = false;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Got an error marking user not registered:", err);
|
|
||||||
// already set an error notification for the user
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadingLimits = false;
|
this.loadingLimits = false;
|
||||||
|
|||||||
@@ -24,13 +24,15 @@
|
|||||||
{{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
|
{{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
|
||||||
<button
|
<button
|
||||||
v-if="
|
v-if="
|
||||||
veriClaim.claimType === 'GiveAction' &&
|
['GiveAction', 'Offer'].includes(
|
||||||
veriClaim.issuer === activeDid
|
veriClaim.claimType as string,
|
||||||
|
) && veriClaim.issuer === activeDid
|
||||||
"
|
"
|
||||||
@click="onClickEditClaim"
|
@click="onClickEditClaim"
|
||||||
title="Edit"
|
title="Edit"
|
||||||
|
data-testId="editClaimButton"
|
||||||
>
|
>
|
||||||
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1"></fa>
|
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
@@ -49,9 +51,12 @@
|
|||||||
</button>
|
</button>
|
||||||
<span v-show="showIdCopy">Copied ID</span>
|
<span v-show="showIdCopy">Copied ID</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div data-testId="description">
|
||||||
<fa icon="message" class="fa-fw text-slate-400" />
|
<fa icon="message" class="fa-fw text-slate-400" />
|
||||||
{{ veriClaim.claim?.description }}
|
{{
|
||||||
|
veriClaim.claim?.itemOffered?.description ||
|
||||||
|
veriClaim.claim?.description
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<fa icon="user" class="fa-fw text-slate-400" />
|
<fa icon="user" class="fa-fw text-slate-400" />
|
||||||
@@ -399,7 +404,7 @@
|
|||||||
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
||||||
<pre
|
<pre
|
||||||
v-if="showVeriClaimDump"
|
v-if="showVeriClaimDump"
|
||||||
class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"
|
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
|
||||||
>{{ veriClaimDump }}</pre
|
>{{ veriClaimDump }}</pre
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -422,7 +427,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<pre>{{ fullClaimDump }}</pre>
|
<pre
|
||||||
|
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
|
||||||
|
>{{ fullClaimDump }}</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@@ -840,6 +848,7 @@ export default class ClaimView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClickEditClaim() {
|
onClickEditClaim() {
|
||||||
|
if (this.veriClaim.claimType === "GiveAction") {
|
||||||
const route = {
|
const route = {
|
||||||
name: "gifted-details",
|
name: "gifted-details",
|
||||||
query: {
|
query: {
|
||||||
@@ -849,6 +858,31 @@ export default class ClaimView extends Vue {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
(this.$router as Router).push(route);
|
(this.$router as Router).push(route);
|
||||||
|
} else if (this.veriClaim.claimType === "Offer") {
|
||||||
|
const route = {
|
||||||
|
name: "offer-details",
|
||||||
|
query: {
|
||||||
|
prevCredToEdit: JSON.stringify(this.veriClaim),
|
||||||
|
destinationPathAfter:
|
||||||
|
"/claim/" + encodeURIComponent(this.veriClaim.handleId),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
(this.$router as Router).push(route);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Unrecognized claim type for edit:",
|
||||||
|
this.veriClaim.claimType,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "This is an unrecognized claim type.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
// Make the xhr request payload
|
// Make the xhr request payload
|
||||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||||
const url = this.apiServer + "/api/v2/claim";
|
const url = this.apiServer + "/api/v2/claim";
|
||||||
const headers = getHeaders(this.activeDid) as AxiosRequestHeaders;
|
const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
|
|||||||
@@ -458,9 +458,9 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Copied",
|
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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,35 @@
|
|||||||
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
||||||
@click="onClickNewContact()"
|
@click="onClickNewContact()"
|
||||||
>
|
>
|
||||||
<fa icon="plus" class="fa-fw"></fa>
|
<fa icon="plus" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Copy Selected Contacts
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -50,6 +78,7 @@
|
|||||||
{{ showGiveNumbers ? "Hide Given Hours" : "Show Given Hours" }}
|
{{ showGiveNumbers ? "Hide Given Hours" : "Show Given Hours" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex justify-between mt-1" v-if="showGiveNumbers">
|
<div class="flex justify-between mt-1" v-if="showGiveNumbers">
|
||||||
<div class="w-full text-right">
|
<div class="w-full text-right">
|
||||||
In the following, only the most recent hours are included. To see more,
|
In the following, only the most recent hours are included. To see more,
|
||||||
@@ -82,147 +111,51 @@
|
|||||||
<ul
|
<ul
|
||||||
id="listContacts"
|
id="listContacts"
|
||||||
v-if="contacts.length > 0"
|
v-if="contacts.length > 0"
|
||||||
class="border-t border-slate-300"
|
class="border-t border-slate-300 mt-1"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
class="border-b border-slate-300 pt-2.5 pb-4"
|
class="border-b border-slate-300 pt-1 pb-1"
|
||||||
v-for="contact in contacts"
|
v-for="contact in filteredContacts()"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
>
|
>
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
<h2 class="text-base font-semibold">
|
<div class="flex items-center">
|
||||||
<EntityIcon
|
<EntityIcon
|
||||||
:contact="contact"
|
:contact="contact"
|
||||||
:iconSize="24"
|
:iconSize="24"
|
||||||
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
|
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
|
||||||
@click="showLargeIdenticon = contact"
|
@click="showLargeIdenticon = contact"
|
||||||
/>
|
/>
|
||||||
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
|
||||||
<button
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-if="!showGiveNumbers"
|
||||||
|
:checked="contactsSelected.includes(contact.did)"
|
||||||
@click="
|
@click="
|
||||||
contactEdit = contact;
|
contactsSelected.includes(contact.did)
|
||||||
contactNewName = contact.name || '';
|
? contactsSelected.splice(
|
||||||
|
contactsSelected.indexOf(contact.did),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
: contactsSelected.push(contact.did)
|
||||||
"
|
"
|
||||||
title="Edit"
|
class="ml-2 h-6 w-6"
|
||||||
>
|
/>
|
||||||
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1"></fa>
|
|
||||||
</button>
|
<h2 class="text-base font-semibold ml-2">
|
||||||
|
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
path: '/did/' + encodeURIComponent(contact.did),
|
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" />
|
<fa icon="circle-info" class="text-blue-500 ml-4" />
|
||||||
</router-link>
|
</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>
|
||||||
<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 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
|
<div
|
||||||
v-if="showGiveNumbers && contact.did != activeDid"
|
v-if="showGiveNumbers && contact.did != activeDid"
|
||||||
class="ml-auto flex gap-1.5"
|
class="ml-auto flex gap-1.5"
|
||||||
@@ -271,7 +204,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-blue-400"
|
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-blue-400"
|
||||||
@click="openOfferDialog(contact.did)"
|
@click="openOfferDialog(contact.did, contact.name)"
|
||||||
>
|
>
|
||||||
Offer
|
Offer
|
||||||
</button>
|
</button>
|
||||||
@@ -293,6 +226,33 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<p v-else>There are no contacts.</p>
|
<p v-else>There are no contacts.</p>
|
||||||
|
|
||||||
|
<div class="mt-2 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"
|
||||||
|
/>
|
||||||
|
<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 Selected Contacts
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<GiftedDialog ref="customGivenDialog" />
|
<GiftedDialog ref="customGivenDialog" />
|
||||||
<OfferDialog ref="customOfferDialog" />
|
<OfferDialog ref="customOfferDialog" />
|
||||||
|
|
||||||
@@ -308,33 +268,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -345,6 +278,7 @@ import { IndexableType } from "dexie";
|
|||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import { AppString, NotificationIface } from "@/constants/app";
|
import { AppString, NotificationIface } from "@/constants/app";
|
||||||
import { db } from "@/db/index";
|
import { db } from "@/db/index";
|
||||||
@@ -379,6 +313,7 @@ export default class ContactsView extends Vue {
|
|||||||
contactInput = "";
|
contactInput = "";
|
||||||
contactEdit: Contact | null = null;
|
contactEdit: Contact | null = null;
|
||||||
contactNewName = "";
|
contactNewName = "";
|
||||||
|
contactsSelected: Array<string> = [];
|
||||||
// { "did:...": concatenated-descriptions } entry for each contact
|
// { "did:...": concatenated-descriptions } entry for each contact
|
||||||
givenByMeDescriptions: Record<string, string> = {};
|
givenByMeDescriptions: Record<string, string> = {};
|
||||||
// { "did:...": amount } entry for each contact
|
// { "did:...": amount } entry for each contact
|
||||||
@@ -404,7 +339,7 @@ export default class ContactsView extends Vue {
|
|||||||
AppString = AppString;
|
AppString = AppString;
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
|
|
||||||
async created() {
|
public async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
@@ -427,7 +362,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(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -439,7 +374,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) {
|
if (!this.activeDid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -546,19 +491,20 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickNewContact(): Promise<void> {
|
private async onClickNewContact(): Promise<void> {
|
||||||
if (!this.contactInput) {
|
const contactInput = this.contactInput.trim();
|
||||||
|
if (!contactInput) {
|
||||||
this.danger("There was no contact info to add.", "No Contact");
|
this.danger("There was no contact info to add.", "No Contact");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
|
if (contactInput.startsWith(CONTACT_URL_PREFIX)) {
|
||||||
await this.addContactFromScan(this.contactInput);
|
await this.addContactFromScan(contactInput);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.contactInput.startsWith(CONTACT_CSV_HEADER)) {
|
if (contactInput.startsWith(CONTACT_CSV_HEADER)) {
|
||||||
const lines = this.contactInput.split(/\n/);
|
const lines = contactInput.split(/\n/);
|
||||||
const lineAdded = [];
|
const lineAdded = [];
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (!line.trim() || line.startsWith(CONTACT_CSV_HEADER)) {
|
if (!line.trim() || line.startsWith(CONTACT_CSV_HEADER)) {
|
||||||
@@ -590,20 +536,21 @@ export default class ContactsView extends Vue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let did = this.contactInput;
|
if (contactInput.startsWith("did:")) {
|
||||||
|
let did = contactInput;
|
||||||
let name, publicKeyInput, nextPublicKeyHashInput;
|
let name, publicKeyInput, nextPublicKeyHashInput;
|
||||||
const commaPos1 = this.contactInput.indexOf(",");
|
const commaPos1 = contactInput.indexOf(",");
|
||||||
if (commaPos1 > -1) {
|
if (commaPos1 > -1) {
|
||||||
did = this.contactInput.substring(0, commaPos1).trim();
|
did = contactInput.substring(0, commaPos1).trim();
|
||||||
name = this.contactInput.substring(commaPos1 + 1).trim();
|
name = contactInput.substring(commaPos1 + 1).trim();
|
||||||
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
|
const commaPos2 = contactInput.indexOf(",", commaPos1 + 1);
|
||||||
if (commaPos2 > -1) {
|
if (commaPos2 > -1) {
|
||||||
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
|
name = contactInput.substring(commaPos1 + 1, commaPos2).trim();
|
||||||
publicKeyInput = this.contactInput.substring(commaPos2 + 1).trim();
|
publicKeyInput = contactInput.substring(commaPos2 + 1).trim();
|
||||||
const commaPos3 = this.contactInput.indexOf(",", commaPos2 + 1);
|
const commaPos3 = contactInput.indexOf(",", commaPos2 + 1);
|
||||||
if (commaPos3 > -1) {
|
if (commaPos3 > -1) {
|
||||||
publicKeyInput = this.contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier
|
publicKeyInput = contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier
|
||||||
nextPublicKeyHashInput = this.contactInput.substring(commaPos3 + 1).trim(); // eslint-disable-line prettier/prettier
|
nextPublicKeyHashInput = contactInput.substring(commaPos3 + 1).trim(); // eslint-disable-line prettier/prettier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -611,7 +558,9 @@ export default class ContactsView extends Vue {
|
|||||||
let publicKeyBase64 = publicKeyInput;
|
let publicKeyBase64 = publicKeyInput;
|
||||||
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
||||||
// it must be all hex (compressed public key), so convert
|
// it must be all hex (compressed public key), so convert
|
||||||
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString(
|
||||||
|
"base64",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let nextPubKeyHashB64 = nextPublicKeyHashInput;
|
let nextPubKeyHashB64 = nextPublicKeyHashInput;
|
||||||
if (nextPubKeyHashB64 && /^[0-9A-Fa-f]{66}$/i.test(nextPubKeyHashB64)) {
|
if (nextPubKeyHashB64 && /^[0-9A-Fa-f]{66}$/i.test(nextPubKeyHashB64)) {
|
||||||
@@ -625,9 +574,33 @@ export default class ContactsView extends Vue {
|
|||||||
nextPubKeyHashB64: nextPubKeyHashB64,
|
nextPubKeyHashB64: nextPubKeyHashB64,
|
||||||
};
|
};
|
||||||
await this.addContact(newContact);
|
await this.addContact(newContact);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addContactFromEndorserMobileLine(line: string): Promise<IndexableType> {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.danger("No contact info was found in that input.", "No Contact Info");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addContactFromEndorserMobileLine(
|
||||||
|
line: string,
|
||||||
|
): Promise<IndexableType> {
|
||||||
// Note that Endorser Mobile puts name first, then did, etc.
|
// Note that Endorser Mobile puts name first, then did, etc.
|
||||||
let name = line;
|
let name = line;
|
||||||
let did = "";
|
let did = "";
|
||||||
@@ -668,7 +641,7 @@ export default class ContactsView extends Vue {
|
|||||||
return db.contacts.add(newContact);
|
return db.contacts.add(newContact);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addContactFromScan(url: string): Promise<void> {
|
private async addContactFromScan(url: string): Promise<void> {
|
||||||
const payload = getContactPayloadFromJwtUrl(url);
|
const payload = getContactPayloadFromJwtUrl(url);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -693,7 +666,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addContact(newContact: Contact) {
|
private async addContact(newContact: Contact) {
|
||||||
if (!newContact.did) {
|
if (!newContact.did) {
|
||||||
this.danger("Cannot add a contact without a DID.", "Incomplete Contact");
|
this.danger("Cannot add a contact without a DID.", "Incomplete Contact");
|
||||||
return;
|
return;
|
||||||
@@ -782,56 +755,30 @@ export default class ContactsView extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// prompt with confirmation if they want to delete a contact
|
// note that this is also in DIDView.vue
|
||||||
confirmDeleteContact(contact: Contact) {
|
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(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "modal",
|
group: "modal",
|
||||||
type: "confirm",
|
type: "confirm",
|
||||||
title: "Delete",
|
title: "Set Visibility",
|
||||||
text:
|
text: visibilityPrompt,
|
||||||
"Are you sure you want to remove " +
|
|
||||||
this.nameForDid(this.contacts, contact.did) +
|
|
||||||
" with DID " +
|
|
||||||
contact.did +
|
|
||||||
" from your contact list?",
|
|
||||||
onYes: async () => {
|
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,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteContact(contact: Contact) {
|
// note that this is also in DIDView.vue
|
||||||
await db.open();
|
private async register(contact: Contact) {
|
||||||
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) {
|
|
||||||
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
|
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -896,28 +843,8 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmSetVisibility(contact: Contact, visibility: boolean) {
|
// note that this is also in DIDView.vue
|
||||||
const visibilityPrompt = visibility
|
private async setVisibility(
|
||||||
? "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(
|
|
||||||
contact: Contact,
|
contact: Contact,
|
||||||
visibility: boolean,
|
visibility: boolean,
|
||||||
showSuccessAlert: boolean,
|
showSuccessAlert: boolean,
|
||||||
@@ -966,7 +893,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 =
|
const url =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/report/canDidExplicitlySeeMe?did=" +
|
"/api/report/canDidExplicitlySeeMe?did=" +
|
||||||
@@ -999,7 +927,7 @@ export default class ContactsView extends Vue {
|
|||||||
type: "info",
|
type: "info",
|
||||||
title: "Visibility Refreshed",
|
title: "Visibility Refreshed",
|
||||||
text:
|
text:
|
||||||
this.nameForContact(contact, true) +
|
libsUtil.nameForContact(contact, true) +
|
||||||
" can " +
|
" can " +
|
||||||
(visibility ? "" : "not ") +
|
(visibility ? "" : "not ") +
|
||||||
"see your activity.",
|
"see your activity.",
|
||||||
@@ -1033,22 +961,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private nameForDid(contacts: Array<Contact>, did: string): string {
|
private confirmShowGiftedDialog(giverDid: string, recipientDid: 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) {
|
|
||||||
// if they have unconfirmed amounts, ask to confirm those
|
// if they have unconfirmed amounts, ask to confirm those
|
||||||
if (
|
if (
|
||||||
recipientDid === this.activeDid &&
|
recipientDid === this.activeDid &&
|
||||||
@@ -1093,13 +1006,13 @@ export default class ContactsView extends Vue {
|
|||||||
if (giverDid) {
|
if (giverDid) {
|
||||||
giver = {
|
giver = {
|
||||||
did: giverDid,
|
did: giverDid,
|
||||||
name: this.nameForDid(this.contacts, giverDid),
|
name: libsUtil.nameForDid(this.activeDid, this.contacts, giverDid),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (recipientDid) {
|
if (recipientDid) {
|
||||||
receiver = {
|
receiver = {
|
||||||
did: recipientDid,
|
did: recipientDid,
|
||||||
name: this.nameForDid(this.contacts, recipientDid),
|
name: libsUtil.nameForDid(this.activeDid, this.contacts, recipientDid),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1131,23 +1044,14 @@ export default class ContactsView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
openOfferDialog(recipientDid: string) {
|
openOfferDialog(recipientDid: string, recipientName?: string) {
|
||||||
(this.$refs.customOfferDialog as OfferDialog).open(recipientDid);
|
(this.$refs.customOfferDialog as OfferDialog).open(
|
||||||
|
recipientDid,
|
||||||
|
recipientName,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onClickCancelName() {
|
private async toggleShowContactAmounts() {
|
||||||
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() {
|
|
||||||
const newShowValue = !this.showGiveNumbers;
|
const newShowValue = !this.showGiveNumbers;
|
||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
@@ -1183,7 +1087,7 @@ export default class ContactsView extends Vue {
|
|||||||
this.loadGives();
|
this.loadGives();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public toggleShowGiveTotals() {
|
private toggleShowGiveTotals() {
|
||||||
if (this.showGiveTotals) {
|
if (this.showGiveTotals) {
|
||||||
this.showGiveTotals = false;
|
this.showGiveTotals = false;
|
||||||
this.showGiveConfirmed = true;
|
this.showGiveConfirmed = true;
|
||||||
@@ -1196,7 +1100,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public showGiveAmountsClassNames() {
|
private showGiveAmountsClassNames() {
|
||||||
return {
|
return {
|
||||||
"from-slate-400": this.showGiveTotals,
|
"from-slate-400": this.showGiveTotals,
|
||||||
"to-slate-700": this.showGiveTotals,
|
"to-slate-700": this.showGiveTotals,
|
||||||
@@ -1206,76 +1110,31 @@ export default class ContactsView extends Vue {
|
|||||||
"to-yellow-700": !this.showGiveTotals && !this.showGiveConfirmed,
|
"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>
|
</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 class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
{{
|
{{ contact?.name || "(no name)" }}
|
||||||
didInfoForContact(viewingDid, activeDid, contact, allMyDids)
|
<button
|
||||||
.displayName
|
@click="
|
||||||
}}
|
contactEdit = true;
|
||||||
|
contactNewName = contact.name || '';
|
||||||
|
"
|
||||||
|
title="Edit"
|
||||||
|
>
|
||||||
|
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||||
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<button @click="showDidDetails = !showDidDetails" class="ml-2 mr-2">
|
<button
|
||||||
|
@click="showDidDetails = !showDidDetails"
|
||||||
|
class="ml-2 mr-2 mt-4"
|
||||||
|
>
|
||||||
Details
|
Details
|
||||||
<fa v-if="showDidDetails" icon="chevron-up" class="text-blue-400" />
|
<fa v-if="showDidDetails" icon="chevron-up" class="text-blue-400" />
|
||||||
<fa v-else icon="chevron-down" class="text-blue-400" />
|
<fa v-else icon="chevron-down" class="text-blue-400" />
|
||||||
@@ -49,8 +58,68 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="flex justify-between mt-4">
|
||||||
<div class="flex justify-center">Auto-Generated Icon:</div>
|
<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">
|
<div class="flex justify-center">
|
||||||
<EntityIcon
|
<EntityIcon
|
||||||
:entityId="viewingDid"
|
:entityId="viewingDid"
|
||||||
@@ -60,6 +129,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showLargeIdenticonId || showLargeIdenticonUrl"
|
v-if="showLargeIdenticonId || showLargeIdenticonUrl"
|
||||||
class="fixed z-[100] top-0 inset-x-0 w-full"
|
class="fixed z-[100] top-0 inset-x-0 w-full"
|
||||||
@@ -80,6 +150,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 -->
|
<!-- Loading Animation -->
|
||||||
<div
|
<div
|
||||||
@@ -126,15 +222,16 @@
|
|||||||
v-if="!isLoading && claims.length === 0"
|
v-if="!isLoading && claims.length === 0"
|
||||||
class="flex justify-center mt-4"
|
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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import * as yaml from "js-yaml";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import * as yaml from "js-yaml";
|
|
||||||
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
@@ -152,6 +249,8 @@ import {
|
|||||||
GenericVerifiableCredential,
|
GenericVerifiableCredential,
|
||||||
GiveVerifiableCredential,
|
GiveVerifiableCredential,
|
||||||
OfferVerifiableCredential,
|
OfferVerifiableCredential,
|
||||||
|
register,
|
||||||
|
setVisibilityUtil,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import * as libsUtil from "@/libs/util";
|
import * as libsUtil from "@/libs/util";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
@@ -174,7 +273,9 @@ export default class DIDView extends Vue {
|
|||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
|
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
|
||||||
contact?: Contact;
|
contact: Contact;
|
||||||
|
contactEdit = false;
|
||||||
|
contactNewName?: string;
|
||||||
contactYaml = "";
|
contactYaml = "";
|
||||||
hitEnd = false;
|
hitEnd = false;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
@@ -195,23 +296,29 @@ export default class DIDView extends Vue {
|
|||||||
this.apiServer = (settings?.apiServer as string) || "";
|
this.apiServer = (settings?.apiServer as string) || "";
|
||||||
|
|
||||||
const pathParam = window.location.pathname.substring("/did/".length);
|
const pathParam = window.location.pathname.substring("/did/".length);
|
||||||
|
let theContact: Contact | undefined;
|
||||||
if (pathParam) {
|
if (pathParam) {
|
||||||
this.viewingDid = decodeURIComponent(pathParam);
|
this.viewingDid = decodeURIComponent(pathParam);
|
||||||
this.contact = await db.contacts.get(this.viewingDid);
|
theContact = await db.contacts.get(this.viewingDid);
|
||||||
this.contactYaml = yaml.dump(this.contact);
|
}
|
||||||
await this.loadClaimsAbout();
|
if (theContact) {
|
||||||
|
this.contact = theContact;
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "No claim ID was provided.",
|
text: "No valid claim ID was provided.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.contactYaml = yaml.dump(this.contact);
|
||||||
|
await this.loadClaimsAbout();
|
||||||
|
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const allAccounts = await accountsDB.accounts.toArray();
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
this.allMyDids = allAccounts.map((acc) => acc.did);
|
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
@@ -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() {
|
public async loadClaimsAbout() {
|
||||||
if (!this.viewingDid) {
|
if (!this.viewingDid) {
|
||||||
console.error("This should never be called without a DID.");
|
console.error("This should never be called without a DID.");
|
||||||
@@ -323,5 +552,178 @@ export default class DIDView extends Vue {
|
|||||||
claimDescription(claim: GenericVerifiableCredential) {
|
claimDescription(claim: GenericVerifiableCredential) {
|
||||||
return claim.claim.name || claim.claim.description || "";
|
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>
|
</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>
|
||||||
|
|||||||
@@ -265,6 +265,8 @@ export default class DiscoverView extends Vue {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error with feed load:", e);
|
console.error("Error with feed load:", e);
|
||||||
|
// this sometimes gives different information
|
||||||
|
console.error("Error with feed load (error added): " + e);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
this.hideBackButton =
|
this.hideBackButton =
|
||||||
(this.$route as Router).query["hideBackButton"] === "true";
|
(this.$route as Router).query["hideBackButton"] === "true";
|
||||||
this.message = ((this.$route as Router).query["message"] as string) || "";
|
this.message = ((this.$route as Router).query["message"] as string) || "";
|
||||||
|
|
||||||
// find any offer ID
|
// find any offer ID
|
||||||
const fulfills = this.prevCredToEdit?.claim?.fulfills;
|
const fulfills = this.prevCredToEdit?.claim?.fulfills;
|
||||||
const fulfillsArray = Array.isArray(fulfills)
|
const fulfillsArray = Array.isArray(fulfills)
|
||||||
@@ -351,6 +352,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// these should be functions but something's wrong with the syntax in the <> conditional
|
||||||
this.givenToProject = !!this.projectId;
|
this.givenToProject = !!this.projectId;
|
||||||
this.givenToRecipient = !this.givenToProject && !!this.recipientDid;
|
this.givenToRecipient = !this.givenToProject && !!this.recipientDid;
|
||||||
|
|
||||||
@@ -549,7 +551,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "To assign to a project, you must open this dialog through a project.",
|
text: "To assign to a project, you must open this page through a project.",
|
||||||
},
|
},
|
||||||
3000,
|
3000,
|
||||||
);
|
);
|
||||||
@@ -574,7 +576,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "To assign to a recipient, you must open this dialog from a contact.",
|
text: "To assign to a recipient, you must open this page from a contact.",
|
||||||
},
|
},
|
||||||
3000,
|
3000,
|
||||||
);
|
);
|
||||||
@@ -694,7 +696,6 @@ export default class GiftedDetails extends Vue {
|
|||||||
constructGiveParam() {
|
constructGiveParam() {
|
||||||
const recipientDid = this.givenToRecipient ? this.recipientDid : undefined;
|
const recipientDid = this.givenToRecipient ? this.recipientDid : undefined;
|
||||||
const projectId = this.givenToProject ? this.projectId : undefined;
|
const projectId = this.givenToProject ? this.projectId : undefined;
|
||||||
// const giveClaim = constructGive(
|
|
||||||
const giveClaim = hydrateGive(
|
const giveClaim = hydrateGive(
|
||||||
this.prevCredToEdit?.claim as GiveVerifiableCredential,
|
this.prevCredToEdit?.claim as GiveVerifiableCredential,
|
||||||
this.giverDid,
|
this.giverDid,
|
||||||
@@ -97,8 +97,8 @@
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
:disabled="!startDateInput"
|
:disabled="!startDateInput"
|
||||||
v-model="startTimeInput"
|
|
||||||
placeholder="Start Time"
|
placeholder="Start Time"
|
||||||
|
v-model="startTimeInput"
|
||||||
type="time"
|
type="time"
|
||||||
class="col-span-1 w-full rounded border border-slate-400 ml-2 px-3 py-2"
|
class="col-span-1 w-full rounded border border-slate-400 ml-2 px-3 py-2"
|
||||||
/>
|
/>
|
||||||
@@ -309,7 +309,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const headers = getHeaders(this.activeDid) as AxiosRequestHeaders;
|
const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders;
|
||||||
const response = await this.axios.delete(
|
const response = await this.axios.delete(
|
||||||
DEFAULT_IMAGE_API_SERVER +
|
DEFAULT_IMAGE_API_SERVER +
|
||||||
"/image/" +
|
"/image/" +
|
||||||
|
|||||||
633
src/views/OfferDetailsView.vue
Normal file
633
src/views/OfferDetailsView.vue
Normal file
@@ -0,0 +1,633 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav />
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
|
<!-- Back -->
|
||||||
|
<div
|
||||||
|
v-if="!hideBackButton"
|
||||||
|
class="text-lg text-center font-light relative px-7"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
@click="cancelBack()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Heading -->
|
||||||
|
<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>
|
||||||
|
Offer to
|
||||||
|
{{
|
||||||
|
offeredToProject
|
||||||
|
? projectName
|
||||||
|
: offeredToRecipient
|
||||||
|
? recipientName
|
||||||
|
: "someone unidentified"
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</h1>
|
||||||
|
<textarea
|
||||||
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||||
|
placeholder="What is offered"
|
||||||
|
v-model="itemDescription"
|
||||||
|
data-testId="itemDescription"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<span
|
||||||
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
|
||||||
|
@click="changeUnitCode()"
|
||||||
|
>
|
||||||
|
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
|
@click="amountInput === '0' ? null : decrement()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
|
||||||
|
v-model="amountInput"
|
||||||
|
data-testId="inputOfferAmount"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
|
@click="increment()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row mt-2">
|
||||||
|
<span
|
||||||
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
||||||
|
>
|
||||||
|
Conditions
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
class="w-full border border-slate-400 px-3 py-2 rounded-r"
|
||||||
|
placeholder="Prerequisites, other people to include, etc."
|
||||||
|
v-model="conditionDescription"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row mt-2">
|
||||||
|
<span
|
||||||
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
||||||
|
>
|
||||||
|
{{ validThroughDateInput ? "" : "No" }} Expiration
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-model="validThroughDateInput"
|
||||||
|
type="date"
|
||||||
|
class="w-full rounded border border-slate-400 px-3 py-2 rounded-r"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-7 mt-4 flex">
|
||||||
|
<input
|
||||||
|
v-if="projectId && !offeredToRecipient"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-6 w-6 mr-2"
|
||||||
|
v-model="offeredToProject"
|
||||||
|
/>
|
||||||
|
<fa
|
||||||
|
v-else
|
||||||
|
icon="square"
|
||||||
|
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
|
||||||
|
@click="notifyUserOfProject()"
|
||||||
|
/>
|
||||||
|
<label class="text-sm mt-1">
|
||||||
|
{{
|
||||||
|
projectId
|
||||||
|
? "This is offered to " + projectName
|
||||||
|
: "No project was chosen"
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-7 mt-4 flex">
|
||||||
|
<input
|
||||||
|
v-if="recipientDid && !offeredToProject"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-6 w-6 mr-2"
|
||||||
|
v-model="offeredToRecipient"
|
||||||
|
/>
|
||||||
|
<fa
|
||||||
|
v-else
|
||||||
|
icon="square"
|
||||||
|
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
|
||||||
|
@click="notifyUserOfRecipient()"
|
||||||
|
/>
|
||||||
|
<label class="text-sm mt-1">
|
||||||
|
{{
|
||||||
|
recipientDid
|
||||||
|
? "This is offered to " + recipientName
|
||||||
|
: "No recipient was chosen."
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex">
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'claim-add-raw',
|
||||||
|
query: {
|
||||||
|
claim: constructOfferParam(),
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
|
Edit & Submit Raw
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center mb-2 mt-6 italic">
|
||||||
|
Sign & Send to publish to the world
|
||||||
|
<fa
|
||||||
|
icon="circle-info"
|
||||||
|
class="pl-2 text-blue-500 cursor-pointer"
|
||||||
|
@click="explainData()"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-lg font-bold 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-2 py-3 rounded-md"
|
||||||
|
@click="confirm"
|
||||||
|
>
|
||||||
|
Sign & Send
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
|
import { NotificationIface } from "@/constants/app";
|
||||||
|
import { accountsDB, db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
|
import {
|
||||||
|
createAndSubmitOffer,
|
||||||
|
didInfo,
|
||||||
|
editAndSubmitOffer,
|
||||||
|
GenericCredWrapper,
|
||||||
|
getPlanFromCache,
|
||||||
|
hydrateOffer,
|
||||||
|
OfferVerifiableCredential,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
|
import * as libsUtil from "@/libs/util";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
QuickNav,
|
||||||
|
TopMessage,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class OfferDetailsView extends Vue {
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
activeDid = "";
|
||||||
|
apiServer = "";
|
||||||
|
|
||||||
|
amountInput = "0";
|
||||||
|
conditionDescription = "";
|
||||||
|
itemDescription = "";
|
||||||
|
destinationPathAfter = "";
|
||||||
|
offeredToProject = false;
|
||||||
|
offeredToRecipient = false;
|
||||||
|
offererDid: string | undefined;
|
||||||
|
hideBackButton = false;
|
||||||
|
message = "";
|
||||||
|
offerId = "";
|
||||||
|
prevCredToEdit?: GenericCredWrapper<OfferVerifiableCredential>;
|
||||||
|
projectId = "";
|
||||||
|
projectName = "a project";
|
||||||
|
recipientDid = "";
|
||||||
|
recipientName = "";
|
||||||
|
unitCode = "HUR";
|
||||||
|
validThroughDateInput = "";
|
||||||
|
|
||||||
|
libsUtil = libsUtil;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
this.prevCredToEdit = (this.$route as Router).query["prevCredToEdit"]
|
||||||
|
? (JSON.parse(
|
||||||
|
(this.$route as Router).query["prevCredToEdit"],
|
||||||
|
) as GenericCredWrapper<OfferVerifiableCredential>)
|
||||||
|
: undefined;
|
||||||
|
} catch (error) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Retrieval Error",
|
||||||
|
text: "The previous record isn't available for editing. If you submit, you'll create a new record.",
|
||||||
|
},
|
||||||
|
6000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevAmount =
|
||||||
|
this.prevCredToEdit?.claim?.includesObject?.amountOfThisGood;
|
||||||
|
this.amountInput =
|
||||||
|
(this.$route as Router).query["amountInput"] ||
|
||||||
|
(prevAmount ? String(prevAmount) : "") ||
|
||||||
|
this.amountInput;
|
||||||
|
this.unitCode = ((this.$route as Router).query["unitCode"] ||
|
||||||
|
this.prevCredToEdit?.claim?.includesObject?.unitCode ||
|
||||||
|
this.unitCode) as string;
|
||||||
|
|
||||||
|
this.conditionDescription =
|
||||||
|
this.prevCredToEdit?.claim?.description || this.conditionDescription;
|
||||||
|
this.itemDescription =
|
||||||
|
(this.$route as Router).query["description"] ||
|
||||||
|
this.prevCredToEdit?.claim?.itemOffered?.description ||
|
||||||
|
this.itemDescription;
|
||||||
|
this.destinationPathAfter = (this.$route as Router).query[
|
||||||
|
"destinationPathAfter"
|
||||||
|
];
|
||||||
|
this.offererDid = ((this.$route as Router).query["offererDid"] ||
|
||||||
|
this.prevCredToEdit?.claim?.agent?.identifier ||
|
||||||
|
this.offererDid) as string;
|
||||||
|
this.hideBackButton =
|
||||||
|
(this.$route as Router).query["hideBackButton"] === "true";
|
||||||
|
this.message = ((this.$route as Router).query["message"] as string) || "";
|
||||||
|
|
||||||
|
// find any project ID
|
||||||
|
let project;
|
||||||
|
if (
|
||||||
|
this.prevCredToEdit?.claim?.itemOffered?.isPartOf?.["@type"] ===
|
||||||
|
"PlanAction"
|
||||||
|
) {
|
||||||
|
project = this.prevCredToEdit?.claim?.itemOffered?.isPartOf;
|
||||||
|
}
|
||||||
|
this.projectId = ((this.$route as Router).query["projectId"] ||
|
||||||
|
project?.identifier ||
|
||||||
|
this.projectId) as string;
|
||||||
|
this.projectName = ((this.$route as Router).query["projectName"] ||
|
||||||
|
project?.name ||
|
||||||
|
this.projectName) as string;
|
||||||
|
|
||||||
|
this.recipientDid = ((this.$route as Router).query["recipientDid"] ||
|
||||||
|
this.prevCredToEdit?.claim?.recipient?.identifier) as string;
|
||||||
|
this.recipientName =
|
||||||
|
((this.$route as Router).query["recipientName"] as string) || "";
|
||||||
|
|
||||||
|
this.validThroughDateInput =
|
||||||
|
this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
|
||||||
|
let allContacts: Contact[] = [];
|
||||||
|
let allMyDids: string[] = [];
|
||||||
|
if (this.recipientDid && !this.recipientName) {
|
||||||
|
allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
|
this.recipientName = didInfo(
|
||||||
|
this.recipientDid,
|
||||||
|
this.activeDid,
|
||||||
|
allMyDids,
|
||||||
|
allContacts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// these should be functions but something's wrong with the syntax in the <> conditional
|
||||||
|
this.offeredToProject = !!this.projectId;
|
||||||
|
this.offeredToRecipient = !this.offeredToProject && !!this.recipientDid;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Error retrieving settings from database:", err);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: err.message || "There was an error retrieving your settings.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.projectId && !this.projectName) {
|
||||||
|
// console.log("Getting project name from cache", this.projectId);
|
||||||
|
const project = await getPlanFromCache(
|
||||||
|
this.projectId,
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
this.activeDid,
|
||||||
|
);
|
||||||
|
this.projectName = project?.name
|
||||||
|
? "the project: " + project.name
|
||||||
|
: "a project";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeUnitCode() {
|
||||||
|
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
||||||
|
const index = units.indexOf(this.unitCode);
|
||||||
|
this.unitCode = units[(index + 1) % units.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
decrement() {
|
||||||
|
this.amountInput = `${Math.max(
|
||||||
|
0,
|
||||||
|
(parseFloat(this.amountInput) || 1) - 1,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
if (this.destinationPathAfter) {
|
||||||
|
(this.$router as Router).push({ path: this.destinationPathAfter });
|
||||||
|
} else {
|
||||||
|
(this.$router as Router).back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelBack() {
|
||||||
|
(this.$router as Router).back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirm() {
|
||||||
|
if (!this.activeDid) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must select an identifier before you can record a offer.",
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parseFloat(this.amountInput) < 0) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
text: "You may not send a negative number.",
|
||||||
|
title: "",
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.itemDescription && !parseFloat(this.amountInput)) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: `You must enter a description or some number of ${
|
||||||
|
this.libsUtil.UNIT_LONG[this.unitCode]
|
||||||
|
}.`,
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
text: "Recording the offer...",
|
||||||
|
title: "",
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// this is asynchronous, but we don't need to wait for it to complete
|
||||||
|
await this.recordOffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyUserOfProject() {
|
||||||
|
if (!this.projectId) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Error",
|
||||||
|
text: "To assign to a project, you must open this page through a project.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// must be because offeredToRecipient is true
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Error",
|
||||||
|
text: "You cannot assign both to a project and to a recipient.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyUserOfRecipient() {
|
||||||
|
if (!this.recipientDid) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Error",
|
||||||
|
text: "To assign to a recipient, you must open this page from a contact.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// must be because offeredToProject is true
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Error",
|
||||||
|
text: "You cannot assign both to a recipient and to a project.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param offererDid may be null
|
||||||
|
* @param description may be an empty string
|
||||||
|
* @param amountInput may be 0
|
||||||
|
* @param unitCode may be omitted, defaults to "HUR"
|
||||||
|
*/
|
||||||
|
public async recordOffer() {
|
||||||
|
try {
|
||||||
|
const recipientDid = this.offeredToRecipient
|
||||||
|
? this.recipientDid
|
||||||
|
: undefined;
|
||||||
|
const projectId = this.offeredToProject ? this.projectId : undefined;
|
||||||
|
let result;
|
||||||
|
if (this.prevCredToEdit) {
|
||||||
|
// don't create from a blank one in case some properties were set from a different interface
|
||||||
|
result = await editAndSubmitOffer(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
this.prevCredToEdit,
|
||||||
|
this.activeDid,
|
||||||
|
this.itemDescription,
|
||||||
|
parseFloat(this.amountInput),
|
||||||
|
this.unitCode,
|
||||||
|
this.conditionDescription,
|
||||||
|
this.validThroughDateInput,
|
||||||
|
recipientDid,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = await createAndSubmitOffer(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
this.activeDid,
|
||||||
|
this.itemDescription,
|
||||||
|
parseFloat(this.amountInput),
|
||||||
|
this.unitCode,
|
||||||
|
this.conditionDescription,
|
||||||
|
this.validThroughDateInput,
|
||||||
|
recipientDid,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.type === "error" || this.isCreationError(result.response)) {
|
||||||
|
const errorMessage = this.getCreationErrorMessage(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 offer.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Success",
|
||||||
|
text: `That offer was recorded.`,
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
localStorage.removeItem("imageUrl");
|
||||||
|
if (this.destinationPathAfter) {
|
||||||
|
(this.$router as Router).push({ path: this.destinationPathAfter });
|
||||||
|
} else {
|
||||||
|
(this.$router as Router).back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error with offer recordation caught:", error);
|
||||||
|
const errorMessage =
|
||||||
|
error.userMessage ||
|
||||||
|
error.response?.data?.error?.message ||
|
||||||
|
"There was an error recording the offer.";
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: errorMessage,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructOfferParam() {
|
||||||
|
const recipientDid = this.offeredToRecipient
|
||||||
|
? this.recipientDid
|
||||||
|
: undefined;
|
||||||
|
const projectId = this.offeredToProject ? this.projectId : undefined;
|
||||||
|
const offerClaim = hydrateOffer(
|
||||||
|
this.prevCredToEdit?.claim as OfferVerifiableCredential,
|
||||||
|
this.activeDid,
|
||||||
|
recipientDid,
|
||||||
|
this.itemDescription,
|
||||||
|
parseFloat(this.amountInput),
|
||||||
|
this.unitCode,
|
||||||
|
this.conditionDescription,
|
||||||
|
projectId,
|
||||||
|
this.validThroughDateInput,
|
||||||
|
this.prevCredToEdit?.id as string,
|
||||||
|
);
|
||||||
|
const claimStr = JSON.stringify(offerClaim);
|
||||||
|
return claimStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for readability
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param result response "data" from the server
|
||||||
|
* @returns true if the result indicates an error
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
isCreationError(result: any) {
|
||||||
|
return result.status !== 201 || result.data?.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||||
|
* @returns best guess at an error message
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
getCreationErrorMessage(result: any) {
|
||||||
|
return (
|
||||||
|
result.error?.userMessage ||
|
||||||
|
result.error?.error ||
|
||||||
|
result.response?.data?.error?.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
explainData() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Data Sharing",
|
||||||
|
text: libsUtil.PRIVACY_MESSAGE,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -162,6 +162,7 @@
|
|||||||
<div v-if="activeDid && isRegistered" class="mt-4">
|
<div v-if="activeDid && isRegistered" class="mt-4">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button
|
<button
|
||||||
|
data-testId="offerButton"
|
||||||
@click="openOfferDialog()"
|
@click="openOfferDialog()"
|
||||||
class="block w-full text-lg font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
class="block w-full text-lg font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
@@ -169,7 +170,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<OfferDialog ref="customOfferDialog" :projectId="this.projectId" />
|
<OfferDialog
|
||||||
|
ref="customOfferDialog"
|
||||||
|
:projectId="this.projectId"
|
||||||
|
:projectName="this.name"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-if="activeDid && isRegistered">
|
<div v-if="activeDid && isRegistered">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|||||||
@@ -109,6 +109,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<div>
|
||||||
|
To
|
||||||
|
{{
|
||||||
|
offer.fulfillsPlanHandleId
|
||||||
|
? projectNameFromHandleId[offer.fulfillsPlanHandleId]
|
||||||
|
: didInfo(
|
||||||
|
offer.recipientDid,
|
||||||
|
activeDid,
|
||||||
|
allMyDids,
|
||||||
|
allContacts,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ offer.objectDescription }}
|
{{ offer.objectDescription }}
|
||||||
</div>
|
</div>
|
||||||
@@ -244,11 +257,14 @@ import QuickNav from "@/components/QuickNav.vue";
|
|||||||
import ProjectIcon from "@/components/ProjectIcon.vue";
|
import ProjectIcon from "@/components/ProjectIcon.vue";
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import {
|
import {
|
||||||
|
didInfo,
|
||||||
getHeaders,
|
getHeaders,
|
||||||
|
getPlanFromCache,
|
||||||
OfferSummaryRecord,
|
OfferSummaryRecord,
|
||||||
PlanData,
|
PlanData,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { EntityIcon, InfiniteScroll, QuickNav, ProjectIcon, TopMessage },
|
components: { EntityIcon, InfiniteScroll, QuickNav, ProjectIcon, TopMessage },
|
||||||
@@ -263,16 +279,19 @@ export default class ProjectsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
|
allContacts: Array<Contact> = [];
|
||||||
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
projects: PlanData[] = [];
|
projects: PlanData[] = [];
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
numAccounts = 0;
|
|
||||||
offers: OfferSummaryRecord[] = [];
|
offers: OfferSummaryRecord[] = [];
|
||||||
|
projectNameFromHandleId: Record<string, string> = {}; // mapping from handleId to description
|
||||||
showOffers = true;
|
showOffers = true;
|
||||||
showProjects = false;
|
showProjects = false;
|
||||||
|
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
|
didInfo = didInfo;
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
@@ -282,9 +301,13 @@ export default class ProjectsView extends Vue {
|
|||||||
this.apiServer = (settings?.apiServer as string) || "";
|
this.apiServer = (settings?.apiServer as string) || "";
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
this.isRegistered = !!settings?.isRegistered;
|
||||||
|
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
this.numAccounts = await accountsDB.accounts.count();
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
if (this.numAccounts === 0) {
|
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
|
|
||||||
|
if (allAccounts.length === 0) {
|
||||||
console.error("No accounts found.");
|
console.error("No accounts found.");
|
||||||
this.errNote("You need an identifier to load your projects.");
|
this.errNote("You need an identifier to load your projects.");
|
||||||
} else {
|
} else {
|
||||||
@@ -343,10 +366,7 @@ export default class ProjectsView extends Vue {
|
|||||||
async loadMoreProjectData(payload: boolean) {
|
async loadMoreProjectData(payload: boolean) {
|
||||||
if (this.projects.length > 0 && payload) {
|
if (this.projects.length > 0 && payload) {
|
||||||
const latestProject = this.projects[this.projects.length - 1];
|
const latestProject = this.projects[this.projects.length - 1];
|
||||||
await this.loadProjects(
|
await this.loadProjects(`beforeId=${latestProject.rowid}`);
|
||||||
this.activeDid,
|
|
||||||
`beforeId=${latestProject.rowid}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,7 +375,7 @@ export default class ProjectsView extends Vue {
|
|||||||
* @param issuerDid of the user
|
* @param issuerDid of the user
|
||||||
* @param urlExtra additional url parameters in a string
|
* @param urlExtra additional url parameters in a string
|
||||||
**/
|
**/
|
||||||
async loadProjects(activeDid?: string, urlExtra: string = "") {
|
async loadProjects(urlExtra: string = "") {
|
||||||
const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`;
|
const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`;
|
||||||
await this.projectDataLoader(url);
|
await this.projectDataLoader(url);
|
||||||
}
|
}
|
||||||
@@ -396,13 +416,37 @@ export default class ProjectsView extends Vue {
|
|||||||
* @param token Authorization token
|
* @param token Authorization token
|
||||||
**/
|
**/
|
||||||
async offerDataLoader(url: string) {
|
async offerDataLoader(url: string) {
|
||||||
const headers = getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const resp = await this.axios.get(url, { headers } as AxiosRequestConfig);
|
const resp = await this.axios.get(url, { headers } as AxiosRequestConfig);
|
||||||
if (resp.status === 200 && resp.data.data) {
|
if (resp.status === 200 && resp.data.data) {
|
||||||
this.offers = this.offers.concat(resp.data.data);
|
// add one-by-one as they retrieve project names, potentially from the server
|
||||||
|
for (const offer of resp.data.data) {
|
||||||
|
if (offer.fulfillsPlanHandleId) {
|
||||||
|
const project = await getPlanFromCache(
|
||||||
|
offer.fulfillsPlanHandleId,
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
this.activeDid,
|
||||||
|
);
|
||||||
|
const projectName = project?.name as string;
|
||||||
|
console.log(
|
||||||
|
"now have name for",
|
||||||
|
offer.fulfillsPlanHandleId,
|
||||||
|
projectName,
|
||||||
|
);
|
||||||
|
this.projectNameFromHandleId[offer.fulfillsPlanHandleId] =
|
||||||
|
projectName;
|
||||||
|
console.log(
|
||||||
|
"now have a real name for",
|
||||||
|
offer.fulfillsPlanHandleId,
|
||||||
|
this.projectNameFromHandleId[offer.fulfillsPlanHandleId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.offers = this.offers.concat([offer]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
"Bad server response & data for offers:",
|
"Bad server response & data for offers:",
|
||||||
@@ -443,7 +487,7 @@ export default class ProjectsView extends Vue {
|
|||||||
async loadMoreOfferData(payload: boolean) {
|
async loadMoreOfferData(payload: boolean) {
|
||||||
if (this.offers.length > 0 && payload) {
|
if (this.offers.length > 0 && payload) {
|
||||||
const latestOffer = this.offers[this.offers.length - 1];
|
const latestOffer = this.offers[this.offers.length - 1];
|
||||||
await this.loadOffers(this.activeDid, `&beforeId=${latestOffer.jwtId}`);
|
await this.loadOffers(`&beforeId=${latestOffer.jwtId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,8 +496,8 @@ export default class ProjectsView extends Vue {
|
|||||||
* @param issuerDid of the user
|
* @param issuerDid of the user
|
||||||
* @param urlExtra additional url parameters in a string
|
* @param urlExtra additional url parameters in a string
|
||||||
**/
|
**/
|
||||||
async loadOffers(issuerDid?: string, urlExtra: string = "") {
|
async loadOffers(urlExtra: string = "") {
|
||||||
const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${issuerDid}${urlExtra}`;
|
const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${this.activeDid}${urlExtra}`;
|
||||||
await this.offerDataLoader(url);
|
await this.offerDataLoader(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center mb-4">
|
<div v-else class="text-center mb-4">
|
||||||
<p>No image found.</p>
|
<p>No image found.</p>
|
||||||
|
<p class="mt-4">
|
||||||
|
If you shared an image, the cause is usually that you do not have the
|
||||||
|
recent version of this app, or that the app has not refreshed the
|
||||||
|
service code underneath. To fix this, first make sure you have latest
|
||||||
|
version by comparing your version at the bottom of "Help" with the
|
||||||
|
version at the bottom of https://timesafari.app/help in a browser. After
|
||||||
|
that, it may eventually work, but you can speed up the process by
|
||||||
|
clearing your data cache (in the browser on mobile, even if you
|
||||||
|
installed it) and/or reinstalling the app (after backing up all your
|
||||||
|
data, of course).
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -122,7 +133,7 @@ export default class SharedPhotoView extends Vue {
|
|||||||
name: "gifted-details",
|
name: "gifted-details",
|
||||||
// this might be wrong since "name" goes with params, but it works so test well when you change it
|
// this might be wrong since "name" goes with params, but it works so test well when you change it
|
||||||
query: {
|
query: {
|
||||||
destinationPathAfter: "/home",
|
destinationPathAfter: "/",
|
||||||
hideBackButton: true,
|
hideBackButton: true,
|
||||||
imageUrl: url,
|
imageUrl: url,
|
||||||
recipientDid: this.activeDid,
|
recipientDid: this.activeDid,
|
||||||
|
|||||||
@@ -566,14 +566,27 @@ async function getNotificationCount() {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function blobToBase64String(blob) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => resolve(reader.result); // potential problem if it returns an ArrayBuffer?
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Store the image blob and go immediate to a page to upload it.
|
// Store the image blob and go immediate to a page to upload it.
|
||||||
// @param photo - image Blob to store for later retrieval after redirect
|
// @param photo - image Blob to store for later retrieval after redirect
|
||||||
async function savePhoto(photo) {
|
async function savePhoto(photo) {
|
||||||
try {
|
try {
|
||||||
|
const photoBase64 = await blobToBase64String(photo);
|
||||||
const db = await openIndexedDB("TimeSafari");
|
const db = await openIndexedDB("TimeSafari");
|
||||||
const transaction = db.transaction("temp", "readwrite");
|
const transaction = db.transaction("temp", "readwrite");
|
||||||
const store = transaction.objectStore("temp");
|
const store = transaction.objectStore("temp");
|
||||||
await updateRecord(store, { id: "shared-photo", blob: photo });
|
await updateRecord(store, {
|
||||||
|
id: "shared-photo-base64",
|
||||||
|
blobB64: photoBase64,
|
||||||
|
});
|
||||||
transaction.oncomplete = () => db.close();
|
transaction.oncomplete = () => db.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("safari-notifications logMessage IndexedDB error", error);
|
console.error("safari-notifications logMessage IndexedDB error", error);
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ test('Create new project, then search for it', async ({ page }) => {
|
|||||||
const finalRandomString = randomString.substring(0, 16);
|
const finalRandomString = randomString.substring(0, 16);
|
||||||
|
|
||||||
// Standard texts
|
// Standard texts
|
||||||
const standardTitle = "Idea ";
|
const standardTitle = 'Idea ';
|
||||||
const standardDescription = "Description of Idea ";
|
const standardDescription = 'Description of Idea ';
|
||||||
|
|
||||||
// Combine texts with the random string
|
// Combine texts with the random string
|
||||||
const finalTitle = standardTitle + finalRandomString;
|
const finalTitle = standardTitle + finalRandomString;
|
||||||
@@ -43,12 +43,10 @@ test('Create new project, then search for it', async ({ page }) => {
|
|||||||
// Search for newly-created project in /projects
|
// Search for newly-created project in /projects
|
||||||
await page.goto('./projects');
|
await page.goto('./projects');
|
||||||
await page.getByRole('link', { name: 'Projects', exact: true }).click();
|
await page.getByRole('link', { name: 'Projects', exact: true }).click();
|
||||||
await page.waitForTimeout(3000); // Wait for a bit
|
|
||||||
await expect(page.locator('ul#listProjects li.border-b:nth-child(1)')).toContainText(finalRandomString); // Assumes newest project always appears first in the Projects tab list
|
await expect(page.locator('ul#listProjects li.border-b:nth-child(1)')).toContainText(finalRandomString); // Assumes newest project always appears first in the Projects tab list
|
||||||
|
|
||||||
// Search for newly-created project in /discover
|
// Search for newly-created project in /discover
|
||||||
await page.goto('./discover');
|
await page.goto('./discover');
|
||||||
await page.waitForTimeout(3000); // Wait for a bit
|
|
||||||
await page.getByPlaceholder('Search…').fill(finalRandomString);
|
await page.getByPlaceholder('Search…').fill(finalRandomString);
|
||||||
await page.locator('#QuickSearch button').click();
|
await page.locator('#QuickSearch button').click();
|
||||||
await expect(page.locator('ul#listDiscoverResults li.border-b:nth-child(1)')).toContainText(finalRandomString);
|
await expect(page.locator('ul#listDiscoverResults li.border-b:nth-child(1)')).toContainText(finalRandomString);
|
||||||
|
|||||||
@@ -2,23 +2,17 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { importUser } from './testUtils';
|
import { importUser } from './testUtils';
|
||||||
|
|
||||||
test('Record something given', async ({ page }) => {
|
test('Record something given', async ({ page }) => {
|
||||||
// Generate a random string of 16 characters
|
// Generate a random string of a few characters
|
||||||
let randomString = Math.random().toString(36).substring(2, 18);
|
const randomString = Math.random().toString(36).substring(2, 6);
|
||||||
|
|
||||||
// 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 non-zero single-digit number
|
// Generate a random non-zero single-digit number
|
||||||
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;
|
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;
|
||||||
|
|
||||||
// Standard title prefix
|
// Standard title prefix
|
||||||
const standardTitle = "Gift ";
|
const standardTitle = 'Gift ';
|
||||||
|
|
||||||
// Combine title prefix with the random string
|
// Combine title prefix with the random string
|
||||||
const finalTitle = standardTitle + finalRandomString;
|
const finalTitle = standardTitle + randomString;
|
||||||
|
|
||||||
// Import user 00
|
// Import user 00
|
||||||
await importUser(page, '00');
|
await importUser(page, '00');
|
||||||
@@ -27,7 +21,7 @@ test('Record something given', async ({ page }) => {
|
|||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
|
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
|
||||||
await page.getByPlaceholder('What was given').fill(finalTitle);
|
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 page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||||
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { importUser } from './testUtils';
|
||||||
|
|
||||||
test('Record item given from image-share', async ({ page }) => {
|
test('Record item given from image-share', async ({ page }) => {
|
||||||
|
|
||||||
@@ -8,11 +9,7 @@ test('Record item given from image-share', async ({ page }) => {
|
|||||||
// Combine title prefix with the random string
|
// Combine title prefix with the random string
|
||||||
const finalTitle = `Gift ${randomString} from image-share`;
|
const finalTitle = `Gift ${randomString} from image-share`;
|
||||||
|
|
||||||
// Create new ID using seed phrase "rigid shrug mobile…"
|
await importUser(page, '00');
|
||||||
await page.goto('./start');
|
|
||||||
await page.getByText('You have a seed').click();
|
|
||||||
await page.getByPlaceholder('Seed Phrase').fill('rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage');
|
|
||||||
await page.getByRole('button', { name: 'Import' }).click();
|
|
||||||
|
|
||||||
// Record something given
|
// Record something given
|
||||||
await page.goto('./test');
|
await page.goto('./test');
|
||||||
@@ -38,3 +35,35 @@ test('Record item given from image-share', async ({ page }) => {
|
|||||||
const item1 = page.locator('li').filter({ hasText: finalTitle });
|
const item1 = page.locator('li').filter({ hasText: finalTitle });
|
||||||
await expect(item1.getByRole('img')).toBeVisible();
|
await expect(item1.getByRole('img')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// // I believe there's a way to test this service worker feature.
|
||||||
|
// // The following is what I got from ChatGPT. I wonder if it doesn't work because it's not registering the service worker correctly.
|
||||||
|
//
|
||||||
|
// test('Trigger a photo-sharing fetch event in service worker with POST to /share-target', async ({ page }) => {
|
||||||
|
// await importUser(page, '00');
|
||||||
|
//
|
||||||
|
// // Create a FormData object with a photo
|
||||||
|
// const photoPath = path.join(__dirname, '..', 'public', 'img', 'icons', 'android-chrome-192x192.png');
|
||||||
|
// const photoContent = await fs.readFileSync(photoPath);
|
||||||
|
// const [response] = await Promise.all([
|
||||||
|
// page.waitForResponse(response => response.url().includes('/share-target')), // also check for response.status() === 303 ?
|
||||||
|
// page.evaluate(async (photoContent) => {
|
||||||
|
// const formData = new FormData();
|
||||||
|
// formData.append('photo', new Blob([photoContent], { type: 'image/png' }), 'test-photo.jpg');
|
||||||
|
//
|
||||||
|
// const response = await fetch('/share-target', {
|
||||||
|
// method: 'POST',
|
||||||
|
// body: formData,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// return response;
|
||||||
|
// }, photoContent)
|
||||||
|
// ]);
|
||||||
|
//
|
||||||
|
// // Verify the response redirected to /shared-photo
|
||||||
|
// //expect(response.status).toBe(303);
|
||||||
|
// console.log('response headers', response.headers());
|
||||||
|
// console.log('response status', response.status());
|
||||||
|
// console.log('response url', response.url());
|
||||||
|
// expect(response.url()).toContain('/shared-photo');
|
||||||
|
// });
|
||||||
|
|||||||
@@ -15,20 +15,20 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
|||||||
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;
|
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;
|
||||||
|
|
||||||
// Standard title prefix
|
// Standard title prefix
|
||||||
const standardTitle = "Gift ";
|
const standardTitle = 'Gift ';
|
||||||
|
|
||||||
// Combine title prefix with the random string
|
// Combine title prefix with the random string
|
||||||
const finalTitle = standardTitle + finalRandomString;
|
const finalTitle = standardTitle + finalRandomString;
|
||||||
|
|
||||||
// Contact name
|
// Contact name
|
||||||
const contactName = 'Contact 00';
|
const contactName = 'Contact #000';
|
||||||
|
|
||||||
// Import user 01
|
// Import user 01
|
||||||
await importUser(page, '01');
|
await importUser(page, '01');
|
||||||
|
|
||||||
// Add new contact 00
|
// Add new contact 00
|
||||||
await page.goto('./contacts');
|
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 page.locator('button > svg.fa-plus').click();
|
||||||
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
||||||
|
|
||||||
@@ -36,10 +36,11 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
|||||||
// 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
|
// 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
|
// 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[title="Edit"]').click();
|
||||||
await expect(page.locator('div.dialog-overlay > div.dialog').filter({ hasText: 'Edit Name' })).toBeVisible();
|
await expect(page.locator('div.dialog-overlay > div.dialog').filter({ hasText: 'Edit Name' })).toBeVisible();
|
||||||
await page.getByPlaceholder('Name', { exact: true }).fill(contactName);
|
await page.getByPlaceholder('Name', { exact: true }).fill(contactName);
|
||||||
await page.locator('.dialog > .flex > button').first().click();
|
await page.locator('.dialog > .flex > button').first().click();
|
||||||
|
|||||||
63
test-playwright/50-record-offer.spec.ts
Normal file
63
test-playwright/50-record-offer.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { importUser } from './testUtils';
|
||||||
|
|
||||||
|
test('Record an offer', async ({ page }) => {
|
||||||
|
// 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 description = `Offering of ${randomString}`;
|
||||||
|
const updatedDescription = `Updated ${description}`;
|
||||||
|
const randomNonZeroNumber = Math.floor(Math.random() * 998) + 1;
|
||||||
|
|
||||||
|
// Create new ID for default user
|
||||||
|
await importUser(page);
|
||||||
|
|
||||||
|
// Select a project
|
||||||
|
await page.goto('./discover');
|
||||||
|
await page.locator('ul#listDiscoverResults li:nth-child(1)').click();
|
||||||
|
|
||||||
|
// Record an offer
|
||||||
|
await page.getByTestId('offerButton').click();
|
||||||
|
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: description }).locator('a').first().click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).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(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: description }).locator('a').first().click();
|
||||||
|
await page.getByTestId('editClaimButton').click();
|
||||||
|
await page.locator('heading', { hasText: 'What is offered' }).isVisible();
|
||||||
|
const itemDesc = await page.getByTestId('itemDescription');
|
||||||
|
await expect(itemDesc).toHaveValue(description);
|
||||||
|
const amount = await page.getByTestId('inputOfferAmount');
|
||||||
|
await expect(amount).toHaveValue(randomNonZeroNumber.toString());
|
||||||
|
// update the values
|
||||||
|
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: description }).locator('a').first().click();
|
||||||
|
const newItemDesc = await page.getByTestId('description');
|
||||||
|
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 + 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;
|
let seedPhrase, userName, did;
|
||||||
|
|
||||||
// Set seed phrase and DID based on user ID
|
// Set seed phrase and DID based on user ID
|
||||||
@@ -21,6 +21,8 @@ export async function importUser(page, id) {
|
|||||||
await page.getByText('You have a seed').click();
|
await page.getByText('You have a seed').click();
|
||||||
await page.getByPlaceholder('Seed Phrase').fill(seedPhrase);
|
await page.getByPlaceholder('Seed Phrase').fill(seedPhrase);
|
||||||
await page.getByRole('button', { name: 'Import' }).click();
|
await page.getByRole('button', { name: 'Import' }).click();
|
||||||
|
await expect(page.locator('#sectionUsageLimits')).toContainText('You have done');
|
||||||
|
await expect(page.locator('#sectionUsageLimits')).toContainText('You have uploaded');
|
||||||
|
|
||||||
// Set name
|
// Set name
|
||||||
await page.getByRole('link', { name: 'Set Your Name' }).click();
|
await page.getByRole('link', { name: 'Set Your Name' }).click();
|
||||||
@@ -16,7 +16,7 @@ export default defineConfig({
|
|||||||
srcDir: '.',
|
srcDir: '.',
|
||||||
filename: 'sw_scripts-combined.js',
|
filename: 'sw_scripts-combined.js',
|
||||||
manifest: {
|
manifest: {
|
||||||
// This is used for the app name. It doesn't include a space, because iOS complains if i recall correctly.
|
// This is used for the app name. It doesn't include a space, because iOS complains if I recall correctly.
|
||||||
// There is a name with spaces in the constants/app.js file for use internally.
|
// There is a name with spaces in the constants/app.js file for use internally.
|
||||||
name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
|
name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
|
||||||
short_name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
|
short_name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
|
||||||
|
|||||||
Reference in New Issue
Block a user