Compare commits
29 Commits
plan-loc
...
miscellany
| Author | SHA1 | Date | |
|---|---|---|---|
| a053c48819 | |||
| 9486142b2a | |||
| bdb8e2e32a | |||
| 06b173e861 | |||
| 6a8b9d36a7 | |||
| 52a6451a2d | |||
| 4b9cbd0e9f | |||
| a5e0c847b1 | |||
|
|
fd43da93a5 | ||
| b59bcf249a | |||
| b05b602acd | |||
| b8aaffbf8d | |||
|
|
5501ac1a2f | ||
|
|
b514d64068 | ||
|
|
c4537420b4 | ||
|
|
5f50338dd0 | ||
| 308386d829 | |||
| 999d7abc04 | |||
| f7f947bfdd | |||
| 26d9b134c7 | |||
| 43f942c905 | |||
| 8ee610c1bc | |||
| 8d15b7bfb8 | |||
| 5c57ee3e72 | |||
|
|
3f7bcbfd76 | ||
| ef0988c9ec | |||
| 22de6113e9 | |||
| 87139f203c | |||
| c8de13d376 |
@@ -99,6 +99,9 @@ See https://tea.xyz
|
||||
|
||||
### Reference Material
|
||||
|
||||
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
|
||||
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
|
||||
|
||||
```
|
||||
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
|
||||
|
||||
|
||||
33246
package-lock.json
generated
33246
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -52,11 +52,11 @@
|
||||
"vue": "^3.3.4",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-facing-decorator": "^2.1.20",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-router": "^4.2.3",
|
||||
"web-did-resolver": "^2.0.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/leaflet": "^1.9.4",
|
||||
"@types/ramda": "^0.29.3",
|
||||
"@types/three": "^0.152.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
|
||||
tasks:
|
||||
|
||||
- 08 Scan QR code to import into contacts assignee:matthew
|
||||
- SEE - https://github.com/gruhn/vue-qrcode-reader
|
||||
|
||||
- in endorser-push-server - mount folder for persistent sqlite DB outside of container
|
||||
- test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView)
|
||||
- .2 bug - on contacts view, click on "to" & "from" and nothing happens
|
||||
- 01 add a location for a project via map pin :
|
||||
- add with a "location" field containing this: { "geo":{ "@type":"GeoCoordinates", "latitude":40.883944, "longitude":-111.884787 } }
|
||||
- 40 notifications :
|
||||
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
||||
|
||||
- 01 add a location for a project via map pin
|
||||
- 04 search by a bounding box for local projects (see API by clicking on "Nearby")
|
||||
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
|
||||
- 02 Fix images on projectview - allow choice of image from a pallete of images or a url image (discovery page display also)
|
||||
- SEE: https://github.com/dmester/jdenticon assignee:jose
|
||||
|
||||
- 08 Scan QR code to import into contacts assignee:matthew
|
||||
- SEE: https://github.com/gruhn/vue-qrcode-reader
|
||||
- 01 fix the Discovery map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
|
||||
|
||||
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew assignee-group:ui
|
||||
|
||||
@@ -28,6 +26,7 @@ tasks:
|
||||
|
||||
- 24 Move to Vite assignee:matthew
|
||||
|
||||
- .2 Edit Plan does not have icons across the bottom assignee-group:ui
|
||||
- .5 include the hash of the latest commit, and maybe a version
|
||||
- .5 add link to further project / people when a project pays ahead
|
||||
- .5 add project ID to the URL, to make a project publicly-accessible
|
||||
@@ -36,7 +35,6 @@ tasks:
|
||||
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist assignee-group:ui
|
||||
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose assignee-group:ui
|
||||
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
|
||||
- .2 move 'switch identity' to the advanced section assignee-group:ui
|
||||
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
|
||||
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
||||
|
||||
@@ -107,6 +105,8 @@ tasks:
|
||||
- 40 notifications v+ :
|
||||
- pull, w/ scheduled runs
|
||||
|
||||
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
|
||||
|
||||
log:
|
||||
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
|
||||
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<div v-bind:class="computedAlertClassNames()">
|
||||
<button
|
||||
class="close-button bg-amber-400 w-8 leading-loose rounded-full absolute top-2 right-2"
|
||||
@click="onClickClose()"
|
||||
>
|
||||
<fa icon="xmark"></fa>
|
||||
</button>
|
||||
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
|
||||
<p>{{ alertMessage }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
|
||||
@Component
|
||||
export default class AlertMessage extends Vue {
|
||||
@Prop alertTitle = "";
|
||||
@Prop alertMessage = "";
|
||||
isAlertVisible = this.alertMessage;
|
||||
|
||||
public onClickClose() {
|
||||
this.isAlertVisible = false;
|
||||
}
|
||||
|
||||
public computedAlertClassNames() {
|
||||
return {
|
||||
hidden: !this.isAlertVisible,
|
||||
"dismissable-alert": true,
|
||||
"bg-amber-200": true,
|
||||
"p-5": true,
|
||||
rounded: true,
|
||||
"drop-shadow-lg": true,
|
||||
fixed: true,
|
||||
"top-3": true,
|
||||
"inset-x-3": true,
|
||||
"transition-transform": true,
|
||||
"ease-in": true,
|
||||
"duration-300": true,
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped></style>
|
||||
@@ -5,13 +5,26 @@
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
import { toSvg } from "jdenticon";
|
||||
|
||||
const BLANK_CONFIG = {
|
||||
lightness: {
|
||||
color: [1.0, 1.0],
|
||||
grayscale: [1.0, 1.0],
|
||||
},
|
||||
saturation: {
|
||||
color: 0.0,
|
||||
grayscale: 0.0,
|
||||
},
|
||||
backColor: "#0000",
|
||||
};
|
||||
|
||||
@Component
|
||||
export default class EntityIcon extends Vue {
|
||||
@Prop entityId = "";
|
||||
@Prop iconSize = "";
|
||||
@Prop iconSize = 0;
|
||||
|
||||
generateIdenticon() {
|
||||
const svgString = toSvg(this.entityId, this.iconSize);
|
||||
const config = this.entityId ? undefined : BLANK_CONFIG;
|
||||
const svgString = toSvg(this.entityId, this.iconSize, config);
|
||||
return svgString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,18 +52,18 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
|
||||
import { GiverInputInfo, GiverOutputInfo } from "@/libs/endorserServer";
|
||||
|
||||
@Component
|
||||
export default class GiftedDialog extends Vue {
|
||||
@Prop message = "";
|
||||
|
||||
giver = null;
|
||||
giver?: GiverInputInfo;
|
||||
description = "";
|
||||
hours = "0";
|
||||
visible = false;
|
||||
|
||||
open(giver) {
|
||||
// giver: GiverInputInfo
|
||||
open(giver: GiverInputInfo) {
|
||||
this.giver = giver;
|
||||
this.visible = true;
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export default class GiftedDialog extends Vue {
|
||||
}
|
||||
|
||||
@Emit("dialog-result")
|
||||
confirm() {
|
||||
confirm(): GiverOutputInfo {
|
||||
const result = {
|
||||
action: "confirm",
|
||||
giver: this.giver,
|
||||
@@ -90,14 +90,14 @@ export default class GiftedDialog extends Vue {
|
||||
};
|
||||
this.close();
|
||||
this.description = "";
|
||||
this.giver = null;
|
||||
this.giver = undefined;
|
||||
this.hours = "0";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Emit("dialog-result")
|
||||
cancel() {
|
||||
cancel(): GiverOutputInfo {
|
||||
const result = { action: "cancel" };
|
||||
this.close();
|
||||
return result;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export type BoundingBox = {
|
||||
eastLong: number;
|
||||
maxLat: number;
|
||||
minLat: number;
|
||||
westLong: number;
|
||||
};
|
||||
|
||||
// a singleton
|
||||
export type Settings = {
|
||||
id: number; // there's only one entry: MASTER_SETTINGS_KEY
|
||||
@@ -7,6 +14,10 @@ export type Settings = {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
lastViewedClaimId?: string;
|
||||
searchBoxes?: Array<{
|
||||
name: string;
|
||||
bbox: BoundingBox;
|
||||
}>;
|
||||
showContactGivesInline?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,13 @@ export interface GiverInputInfo {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface GiverOutputInfo {
|
||||
action: string;
|
||||
giver?: GiverInputInfo;
|
||||
description?: string;
|
||||
hours?: number;
|
||||
}
|
||||
|
||||
export interface ClaimResult {
|
||||
success: { claimId: string; handleId: string };
|
||||
error: { code: string; message: string };
|
||||
@@ -55,7 +62,30 @@ export interface GiveVerifiableCredential {
|
||||
fulfills?: { "@type": string; identifier: string };
|
||||
identifier?: string;
|
||||
object?: { amountOfThisGood: number; unitCode: string };
|
||||
recipient: { identifier: string };
|
||||
recipient?: { identifier: string };
|
||||
}
|
||||
|
||||
export interface PlanVerifiableCredential {
|
||||
"@context": "https://schema.org";
|
||||
"@type": "PlanAction";
|
||||
name: string;
|
||||
description: string;
|
||||
identifier?: string;
|
||||
location?: {
|
||||
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlanServerRecord {
|
||||
agentDid?: string; // optional, if the issuer wants someone else to manage as well
|
||||
description: string;
|
||||
endTime?: string;
|
||||
issuerDid: string;
|
||||
handleId: string;
|
||||
locLat?: number;
|
||||
locLon?: number;
|
||||
startTime?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface RegisterVerifiableCredential {
|
||||
@@ -63,7 +93,7 @@ export interface RegisterVerifiableCredential {
|
||||
"@type": string;
|
||||
agent: { identifier: string };
|
||||
object: string;
|
||||
recipient: { identifier: string };
|
||||
participant: { identifier: string };
|
||||
}
|
||||
|
||||
export interface InternalError {
|
||||
@@ -75,7 +105,7 @@ export interface InternalError {
|
||||
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
||||
const HIDDEN_DID = "did:none:HIDDEN";
|
||||
|
||||
export function isHiddenDid(did) {
|
||||
export function isHiddenDid(did: string) {
|
||||
return did === HIDDEN_DID;
|
||||
}
|
||||
|
||||
@@ -88,7 +118,7 @@ export function didInfo(
|
||||
allMyDids: Array<string>,
|
||||
contacts: Array<Contact>,
|
||||
): string {
|
||||
const myId: string | undefined = R.find(R.equals(did), allMyDids, did);
|
||||
const myId: string | undefined = R.find(R.equals(did), allMyDids);
|
||||
if (myId) {
|
||||
return "You" + (myId !== activeDid ? " (Alt ID)" : "");
|
||||
} else {
|
||||
@@ -105,6 +135,22 @@ export function didInfo(
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResultWithType {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SuccessResult extends ResultWithType {
|
||||
type: "success";
|
||||
response: AxiosResponse<ClaimResult>;
|
||||
}
|
||||
|
||||
export interface ErrorResult {
|
||||
type: "error";
|
||||
error: InternalError;
|
||||
}
|
||||
|
||||
export type CreateAndSubmitGiveResult = SuccessResult | ErrorResult;
|
||||
|
||||
/**
|
||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||
*
|
||||
@@ -118,76 +164,115 @@ export async function createAndSubmitGive(
|
||||
axios: Axios,
|
||||
apiServer: string,
|
||||
identity: IIdentifier,
|
||||
fromDid: string,
|
||||
toDid: string,
|
||||
description: string,
|
||||
hours: number,
|
||||
fromDid?: string,
|
||||
toDid?: string,
|
||||
description?: string,
|
||||
hours?: number,
|
||||
fulfillsProjectHandleId?: string,
|
||||
): Promise<AxiosResponse<ClaimResult> | InternalError> {
|
||||
// Make a claim
|
||||
const vcClaim: GiveVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "GiveAction",
|
||||
recipient: { identifier: toDid },
|
||||
};
|
||||
if (fromDid) {
|
||||
vcClaim.agent = { identifier: fromDid };
|
||||
}
|
||||
if (description) {
|
||||
vcClaim.description = description;
|
||||
}
|
||||
if (hours) {
|
||||
vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
|
||||
}
|
||||
if (fulfillsProjectHandleId) {
|
||||
vcClaim.fulfills = {
|
||||
"@type": "PlanAction",
|
||||
identifier: fulfillsProjectHandleId,
|
||||
): Promise<CreateAndSubmitGiveResult> {
|
||||
try {
|
||||
// Make a claim
|
||||
const vcClaim: GiveVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "GiveAction",
|
||||
};
|
||||
if (toDid) {
|
||||
vcClaim.recipient = { identifier: toDid };
|
||||
}
|
||||
if (fromDid) {
|
||||
vcClaim.agent = { identifier: fromDid };
|
||||
}
|
||||
if (description) {
|
||||
vcClaim.description = description;
|
||||
}
|
||||
if (hours) {
|
||||
vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
|
||||
}
|
||||
if (fulfillsProjectHandleId) {
|
||||
vcClaim.fulfills = {
|
||||
"@type": "PlanAction",
|
||||
identifier: fulfillsProjectHandleId,
|
||||
};
|
||||
}
|
||||
// Make a payload for the claim
|
||||
const vcPayload = {
|
||||
vc: {
|
||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||
type: ["VerifiableCredential"],
|
||||
credentialSubject: vcClaim,
|
||||
},
|
||||
};
|
||||
|
||||
// Create a signature using private key of identity
|
||||
const firstKey = identity.keys[0];
|
||||
if (!firstKey || !firstKey.privateKeyHex) {
|
||||
throw {
|
||||
error: "No private key",
|
||||
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
|
||||
};
|
||||
}
|
||||
|
||||
const privateKeyHex = firstKey.privateKeyHex;
|
||||
|
||||
if (!privateKeyHex) {
|
||||
throw {
|
||||
error: "No private key",
|
||||
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
|
||||
};
|
||||
}
|
||||
|
||||
const signer = await SimpleSigner(privateKeyHex);
|
||||
const alg = undefined;
|
||||
|
||||
// Create a JWT for the request
|
||||
|
||||
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
||||
alg: alg,
|
||||
issuer: identity.did,
|
||||
signer: signer,
|
||||
});
|
||||
|
||||
// Make the xhr request payload
|
||||
|
||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||
const url = apiServer + "/api/v2/claim";
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + token,
|
||||
};
|
||||
|
||||
const response = await axios.post(url, payload, { headers });
|
||||
return {
|
||||
type: "success",
|
||||
response,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
let errorMessage: string;
|
||||
|
||||
if (error instanceof Error) {
|
||||
// If it's a JavaScript Error object
|
||||
errorMessage = error.message;
|
||||
} else if (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"message" in error
|
||||
) {
|
||||
// If it's an object that has a 'message' property
|
||||
errorMessage = (error as { message: string }).message;
|
||||
} else {
|
||||
// Unknown error shape, default message
|
||||
errorMessage = "Unknown error";
|
||||
}
|
||||
|
||||
return {
|
||||
type: "error",
|
||||
error: {
|
||||
error: errorMessage,
|
||||
userMessage: "Failed to create and submit the claim.",
|
||||
},
|
||||
};
|
||||
}
|
||||
// Make a payload for the claim
|
||||
const vcPayload = {
|
||||
vc: {
|
||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||
type: ["VerifiableCredential"],
|
||||
credentialSubject: vcClaim,
|
||||
},
|
||||
};
|
||||
// Create a signature using private key of identity
|
||||
if (identity.keys[0].privateKeyHex == null) {
|
||||
return new Promise<InternalError>((resolve, reject) => {
|
||||
reject({
|
||||
error: "No private key",
|
||||
message:
|
||||
"Your identifier " +
|
||||
identity.did +
|
||||
" is not configured correctly. Use a different identifier.",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
|
||||
const signer = await SimpleSigner(privateKeyHex);
|
||||
const alg = undefined;
|
||||
// Create a JWT for the request
|
||||
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
||||
alg: alg,
|
||||
issuer: identity.did,
|
||||
signer: signer,
|
||||
});
|
||||
|
||||
// Make the xhr request payload
|
||||
|
||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||
const url = apiServer + "/api/v2/claim";
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + token,
|
||||
};
|
||||
|
||||
return axios.post(url, payload, { headers });
|
||||
}
|
||||
|
||||
// from https://stackoverflow.com/a/175787/845494
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
faGift,
|
||||
faHand,
|
||||
faHouseChimney,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
@@ -80,6 +81,7 @@ library.add(
|
||||
faGift,
|
||||
faHand,
|
||||
faHouseChimney,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
||||
import { accountsDB } from "@/db";
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
NavigationGuardNext,
|
||||
RouteLocationNormalized,
|
||||
RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
import { accountsDB } from "@/db/index";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -7,7 +13,11 @@ import { accountsDB } from "@/db";
|
||||
* @param from :RouteLocationNormalized
|
||||
* @param next :NavigationGuardNext
|
||||
*/
|
||||
const enterOrStart = async (to, from, next) => {
|
||||
const enterOrStart = async (
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext,
|
||||
) => {
|
||||
await accountsDB.open();
|
||||
const num_accounts = await accountsDB.accounts.count();
|
||||
if (num_accounts > 0) {
|
||||
@@ -179,7 +189,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "contact-gives",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "statistics" */ "../views/ContactGiftingView.vue"
|
||||
/* webpackChunkName: "contact-gives" */ "../views/ContactGiftingView.vue"
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -190,7 +200,12 @@ const router = createRouter({
|
||||
routes,
|
||||
});
|
||||
|
||||
const errorHandler = (error, to, from) => {
|
||||
const errorHandler = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: any,
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
) => {
|
||||
// Handle the error here
|
||||
console.error("Caught in top level error handler:", error, to, from);
|
||||
|
||||
|
||||
@@ -117,12 +117,6 @@
|
||||
>
|
||||
Edit Identity
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'identity-switcher' }"
|
||||
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||
>
|
||||
Switch Identity / No Identity
|
||||
</router-link>
|
||||
<button
|
||||
@click="
|
||||
this.$notify(
|
||||
@@ -176,6 +170,33 @@
|
||||
</button>
|
||||
</dialog>
|
||||
|
||||
<div class="flex py-2">
|
||||
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
||||
Check Limits
|
||||
</button>
|
||||
<!-- show spinner if loading limits -->
|
||||
<div v-if="loadingLimits" class="ml-2">
|
||||
Checking... <fa icon="spinner" class="fa-spin"></fa>
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
{{ limitsMessage }}
|
||||
</div>
|
||||
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
|
||||
<span class="font-bold">Rate Limits</span>
|
||||
<p>
|
||||
You have done {{ limits.doneClaimsThisWeek }} claims out of
|
||||
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
|
||||
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
|
||||
</p>
|
||||
<p>
|
||||
You have done {{ limits.doneRegistrationsThisMonth }} registrations
|
||||
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
|
||||
registrations counter resets at
|
||||
{{ readableTime(limits.nextMonthBeginDateTime) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3
|
||||
class="text-sm uppercase font-semibold mb-3"
|
||||
@click="showAdvanced = !showAdvanced"
|
||||
@@ -209,33 +230,12 @@
|
||||
</label>
|
||||
|
||||
<div class="flex py-2">
|
||||
<button
|
||||
class="text-center text-md text-blue-500"
|
||||
@click="checkLimits()"
|
||||
<router-link
|
||||
:to="{ name: 'identity-switcher' }"
|
||||
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||
>
|
||||
Check Limits
|
||||
</button>
|
||||
<!-- show spinner if loading limits -->
|
||||
<div v-if="loadingLimits" class="ml-2">
|
||||
Checking... <fa icon="spinner" class="fa-spin"></fa>
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
{{ limitsMessage }}
|
||||
</div>
|
||||
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
|
||||
<span class="font-bold">Rate Limits</span>
|
||||
<p>
|
||||
You have done {{ limits.doneClaimsThisWeek }} claims out of
|
||||
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
|
||||
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
|
||||
</p>
|
||||
<p>
|
||||
You have done {{ limits.doneRegistrationsThisMonth }} registrations
|
||||
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
|
||||
registrations counter resets at
|
||||
{{ readableTime(limits.nextMonthBeginDateTime) }}
|
||||
</p>
|
||||
</div>
|
||||
Switch Identity / No Identity
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="flex py-2">
|
||||
@@ -283,32 +283,37 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { AxiosError } from "axios/index";
|
||||
import "dexie-export-import";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import { AppString } from "@/constants/app";
|
||||
import { db, accountsDB } from "@/db";
|
||||
import { db, accountsDB } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { AxiosError } from "axios/index";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Buffer = require("buffer/").Buffer;
|
||||
|
||||
@Component({ components: { AlertMessage, QuickNav } })
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class AccountViewView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
Constants = AppString;
|
||||
|
||||
activeDid = "";
|
||||
@@ -334,7 +339,7 @@ export default class AccountViewView extends Vue {
|
||||
alertMessage = "";
|
||||
alertTitle = "";
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
@@ -344,7 +349,7 @@ export default class AccountViewView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -354,7 +359,7 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
|
||||
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
||||
doCopyTwoSecRedo(text, fn) {
|
||||
doCopyTwoSecRedo(text: string, fn: () => void) {
|
||||
fn();
|
||||
useClipboard()
|
||||
.copy(text)
|
||||
@@ -406,7 +411,8 @@ export default class AccountViewView extends Vue {
|
||||
});
|
||||
this.checkLimitsFor(identity);
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
if (
|
||||
err.message ===
|
||||
"Attempted to load account records with no identity available."
|
||||
@@ -509,7 +515,8 @@ export default class AccountViewView extends Vue {
|
||||
if (resp.status === 200) {
|
||||
this.limits = resp.data;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error.message ===
|
||||
"Attempted to load Give records with no identity available."
|
||||
@@ -520,13 +527,9 @@ export default class AccountViewView extends Vue {
|
||||
const serverError = error as AxiosError;
|
||||
console.error("Bad response retrieving limits: ", serverError);
|
||||
|
||||
const data: ErrorResponse | undefined =
|
||||
serverError.response && serverError.response.data;
|
||||
if (data && data.error && data.error.message) {
|
||||
this.limitsMessage = data.error.message;
|
||||
} else {
|
||||
this.limitsMessage = "Bad server response.";
|
||||
}
|
||||
const data = (serverError.response &&
|
||||
serverError.response.data) as ErrorResponse;
|
||||
this.limitsMessage = data?.error?.message || "Bad server response.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +579,7 @@ export default class AccountViewView extends Vue {
|
||||
this.apiServer = this.apiServerInput;
|
||||
}
|
||||
|
||||
setApiServerInput(value) {
|
||||
setApiServerInput(value: string) {
|
||||
this.apiServerInput = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +90,6 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -101,7 +97,7 @@
|
||||
import * as R from "ramda";
|
||||
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||
@@ -113,17 +109,24 @@ import {
|
||||
} from "@/libs/endorserServer";
|
||||
import * as didJwt from "did-jwt";
|
||||
import { AxiosError } from "axios";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
|
||||
@Component({ components: { AlertMessage, QuickNav } })
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class ContactsView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
contact: Contact | null = null;
|
||||
giveRecords: Array<GiveServerRecord> = [];
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
numAccounts = 0;
|
||||
|
||||
async beforeCreate() {
|
||||
@@ -131,7 +134,7 @@ export default class ContactsView extends Vue {
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
@@ -147,7 +150,7 @@ export default class ContactsView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -169,7 +172,8 @@ export default class ContactsView extends Vue {
|
||||
if (this.activeDid && this.contact) {
|
||||
this.loadGives(this.activeDid, this.contact);
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -187,7 +191,7 @@ export default class ContactsView extends Vue {
|
||||
async loadGives(activeDid: string, contact: Contact) {
|
||||
try {
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
let result = [];
|
||||
let result: Array<GiveServerRecord> = [];
|
||||
const url =
|
||||
this.apiServer +
|
||||
"/api/v2/report/gives?agentDid=" +
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
<h2 class="text-base flex gap-4 items-center">
|
||||
<span class="grow italic text-slate-500"
|
||||
><EntityIcon
|
||||
entityId="Anonymous"
|
||||
:entityId="null"
|
||||
:iconSize="32"
|
||||
class="opacity-50 inline-block align-middle border border-dashed border-slate-400 bg-slate-200 rounded-md mr-1"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
></EntityIcon>
|
||||
Anonymous
|
||||
</span>
|
||||
@@ -76,48 +76,53 @@
|
||||
message="Received from"
|
||||
>
|
||||
</GiftedDialog>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||
import { db, accountsDB } from "@/db";
|
||||
import { db, accountsDB } from "@/db/index";
|
||||
import { AccountsSchema } from "@/db/tables/accounts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { createAndSubmitGive } from "@/libs/endorserServer";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
CreateAndSubmitGiveResult,
|
||||
ErrorResult,
|
||||
GiverInputInfo,
|
||||
GiverOutputInfo,
|
||||
} from "@/libs/endorserServer";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||
})
|
||||
export default class HomeView extends Vue {
|
||||
export default class ContactGiftingView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
allAccounts: Array<Account> = [];
|
||||
allContacts: Array<Contact> = [];
|
||||
apiServer = "";
|
||||
isHiddenSpinner = true;
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
accounts: AccountsSchema;
|
||||
accounts: typeof AccountsSchema;
|
||||
numAccounts = 0;
|
||||
|
||||
async beforeCreate() {
|
||||
accountsDB.open();
|
||||
this.accounts = accountsDB.accounts;
|
||||
this.numAccounts = await this.accounts.count();
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
@@ -133,7 +138,7 @@ export default class HomeView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -144,23 +149,20 @@ export default class HomeView extends Vue {
|
||||
|
||||
async created() {
|
||||
try {
|
||||
await accountsDB.open();
|
||||
this.allAccounts = await accountsDB.accounts.toArray();
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||
this.updateAllFeed();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
err.userMessage ||
|
||||
err.message ||
|
||||
"There was an error retrieving the latest sweet, sweet action.",
|
||||
},
|
||||
-1,
|
||||
@@ -168,37 +170,20 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
public async buildHeaders() {
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
|
||||
if (this.activeDid) {
|
||||
await accountsDB.open();
|
||||
const allAccounts = await accountsDB.accounts.toArray();
|
||||
const account = allAccounts.find((acc) => acc.did === this.activeDid);
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
throw new Error(
|
||||
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
|
||||
);
|
||||
}
|
||||
|
||||
headers["Authorization"] = "Bearer " + (await accessToken(identity));
|
||||
} else {
|
||||
// it's OK without auth... we just won't get any identifiers
|
||||
}
|
||||
return headers;
|
||||
openDialog(giver: GiverInputInfo) {
|
||||
(this.$refs.customDialog as GiftedDialog).open(giver);
|
||||
}
|
||||
|
||||
openDialog(giver) {
|
||||
this.$refs.customDialog.open(giver);
|
||||
}
|
||||
|
||||
handleDialogResult(result) {
|
||||
handleDialogResult(result: GiverOutputInfo) {
|
||||
if (result.action === "confirm") {
|
||||
return new Promise((resolve) => {
|
||||
this.recordGive(result.contact?.did, result.description, result.hours);
|
||||
resolve();
|
||||
this.recordGive(
|
||||
result.giver?.did,
|
||||
result.description,
|
||||
result.hours,
|
||||
).then(() => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// action was "cancel" so do nothing
|
||||
@@ -211,7 +196,11 @@ export default class HomeView extends Vue {
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
*/
|
||||
public async recordGive(giverDid, description, hours) {
|
||||
public async recordGive(
|
||||
giverDid?: string,
|
||||
description?: string,
|
||||
hours?: number,
|
||||
) {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
{
|
||||
@@ -250,8 +239,8 @@ export default class HomeView extends Vue {
|
||||
hours,
|
||||
);
|
||||
|
||||
if (isGiveCreationError(result)) {
|
||||
const errorMessage = getGiveCreationErrorMessage(result);
|
||||
if (this.isGiveCreationError(result)) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
console.log("Error with give result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
@@ -273,39 +262,34 @@ export default class HomeView extends Vue {
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.log("Error with give caught:", error);
|
||||
|
||||
const message =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
"There was an error recording the Give.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
getGiveErrorMessage(error) ||
|
||||
"There was an error recording the give.",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private setAlert(title, message) {
|
||||
this.alertTitle = title;
|
||||
this.alertMessage = message;
|
||||
}
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
isGiveCreationError(result) {
|
||||
return result.status !== 201 || result.data?.error;
|
||||
isGiveCreationError(result: CreateAndSubmitGiveResult) {
|
||||
return result.type == "error";
|
||||
}
|
||||
|
||||
getGiveCreationErrorMessage(result) {
|
||||
return result.data?.error?.message;
|
||||
}
|
||||
|
||||
getGiveErrorMessage(error) {
|
||||
return error.userMessage || error.response?.data?.error?.message;
|
||||
getGiveCreationErrorMessage(result: CreateAndSubmitGiveResult) {
|
||||
return (result as ErrorResult).error?.userMessage;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,17 +23,24 @@
|
||||
<script lang="ts">
|
||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import * as R from "ramda";
|
||||
import { SimpleSigner } from "@/libs/crypto";
|
||||
import * as didJwt from "did-jwt";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Buffer = require("buffer/").Buffer;
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
QRCodeVue3,
|
||||
@@ -41,11 +48,13 @@ const Buffer = require("buffer/").Buffer;
|
||||
},
|
||||
})
|
||||
export default class ContactQRScanShow extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
qrValue = "";
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
const account: Account | undefined = R.find(
|
||||
|
||||
@@ -121,19 +121,21 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="contact.registered"
|
||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||
title="Registered"
|
||||
>
|
||||
<fa icon="person-circle-check" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@click="register(contact)"
|
||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||
title="Registration unknown"
|
||||
>
|
||||
<fa icon="person-circle-question" class="fa-fw" />
|
||||
<fa
|
||||
v-if="contact.registered"
|
||||
icon="person-circle-check"
|
||||
class="fa-fw"
|
||||
title="Registered"
|
||||
/>
|
||||
<fa
|
||||
v-else
|
||||
icon="person-circle-question"
|
||||
class="fa-fw"
|
||||
title="Registration Unknown"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -202,10 +204,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else>This identity has no contacts.</p>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -214,27 +212,36 @@ import { AxiosError } from "axios";
|
||||
import * as didJwt from "did-jwt";
|
||||
import * as R from "ramda";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||
import {
|
||||
GiveServerRecord,
|
||||
GiveVerifiableCredential,
|
||||
RegisterVerifiableCredential,
|
||||
SERVICE_ID,
|
||||
} from "@/libs/endorserServer";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Buffer = require("buffer/").Buffer;
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { AlertMessage, QuickNav, EntityIcon },
|
||||
components: { QuickNav, EntityIcon },
|
||||
})
|
||||
export default class ContactsView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
contacts: Array<Contact> = [];
|
||||
@@ -256,8 +263,6 @@ export default class ContactsView extends Vue {
|
||||
showGiveNumbers = false;
|
||||
showGiveTotals = true;
|
||||
showGiveConfirmed = true;
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
|
||||
async created() {
|
||||
await db.open();
|
||||
@@ -276,7 +281,7 @@ export default class ContactsView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
const account = R.find((acc) => acc.did === activeDid, accounts);
|
||||
@@ -290,7 +295,7 @@ export default class ContactsView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -299,7 +304,7 @@ export default class ContactsView extends Vue {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public async getHeadersAndIdentity(activeDid) {
|
||||
public async getHeadersAndIdentity(activeDid: string) {
|
||||
const identity = await this.getIdentity(activeDid);
|
||||
const headers = await this.getHeaders(identity);
|
||||
|
||||
@@ -308,11 +313,11 @@ export default class ContactsView extends Vue {
|
||||
|
||||
async loadGives() {
|
||||
const handleResponse = (
|
||||
resp,
|
||||
descriptions,
|
||||
confirmed,
|
||||
unconfirmed,
|
||||
useRecipient,
|
||||
resp: { status: number; data: { data: GiveServerRecord[] } },
|
||||
descriptions: Record<string, string>,
|
||||
confirmed: Record<string, number>,
|
||||
unconfirmed: Record<string, number>,
|
||||
useRecipient: boolean,
|
||||
) => {
|
||||
if (resp.status === 200) {
|
||||
const allData = resp.data.data;
|
||||
@@ -344,9 +349,8 @@ export default class ContactsView extends Vue {
|
||||
title: "Error With Server",
|
||||
text:
|
||||
"Got an error retrieving your " +
|
||||
resp.config.url.includes("recipientDid")
|
||||
? "received"
|
||||
: "given" + " time from the server.",
|
||||
(useRecipient ? "given" : "received") +
|
||||
" time from the server.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -457,6 +461,9 @@ export default class ContactsView extends Vue {
|
||||
confirm(
|
||||
"Are you sure you want to use one of your registrations for " +
|
||||
this.nameForDid(this.contacts, contact.did) +
|
||||
(contact.registered
|
||||
? " -- especially since they are already marked as registered"
|
||||
: "") +
|
||||
"?",
|
||||
)
|
||||
) {
|
||||
@@ -608,6 +615,8 @@ export default class ContactsView extends Vue {
|
||||
this.apiServer +
|
||||
"/api/report/canDidExplicitlySeeMe?did=" +
|
||||
encodeURIComponent(contact.did);
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
const headers = await this.getHeaders(identity);
|
||||
|
||||
try {
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
@@ -685,11 +694,17 @@ export default class ContactsView extends Vue {
|
||||
|
||||
// if they have unconfirmed amounts, ask to confirm those first
|
||||
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
|
||||
const isare = this.givenToMeUnconfirmed[fromDid] == 1 ? "is" : "are";
|
||||
const hours = this.givenToMeUnconfirmed[fromDid] == 1 ? "hour" : "hours";
|
||||
if (
|
||||
confirm(
|
||||
"There are " +
|
||||
"There " +
|
||||
isare +
|
||||
" " +
|
||||
this.givenToMeUnconfirmed[fromDid] +
|
||||
" unconfirmed hours from them." +
|
||||
" unconfirmed " +
|
||||
hours +
|
||||
" from them." +
|
||||
" Would you like to confirm some of those hours?",
|
||||
)
|
||||
) {
|
||||
@@ -697,6 +712,7 @@ export default class ContactsView extends Vue {
|
||||
name: "contact-amounts",
|
||||
query: { contactDid: fromDid },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!this.isNumeric(this.hourInput)) {
|
||||
@@ -747,7 +763,9 @@ export default class ContactsView extends Vue {
|
||||
confirm(
|
||||
"Are you sure you want to record " +
|
||||
this.hourInput +
|
||||
" hours " +
|
||||
" hour" +
|
||||
(this.hourInput == "1" ? "" : "s") +
|
||||
" " +
|
||||
toFrom +
|
||||
description +
|
||||
"?",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</h1>
|
||||
|
||||
<!-- Quick Search -->
|
||||
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="search()">
|
||||
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchAll()">
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchTerms"
|
||||
@@ -17,7 +17,7 @@
|
||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
||||
/>
|
||||
<button
|
||||
@click="search()"
|
||||
@click="searchAll()"
|
||||
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
||||
>
|
||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||
@@ -32,6 +32,8 @@
|
||||
href="#"
|
||||
@click="
|
||||
projects = [];
|
||||
isLocalActive = true;
|
||||
isRemoteActive = false;
|
||||
searchLocal();
|
||||
"
|
||||
v-bind:class="computedLocalTabClassNames()"
|
||||
@@ -49,10 +51,12 @@
|
||||
v-bind:class="computedRemoteTabClassNames()"
|
||||
@click="
|
||||
projects = [];
|
||||
search();
|
||||
isRemoteActive = true;
|
||||
isLocalActive = false;
|
||||
searchAll();
|
||||
"
|
||||
>
|
||||
Remote
|
||||
Anywhere
|
||||
<span
|
||||
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
||||
>{{ remoteCount }}</span
|
||||
@@ -62,6 +66,49 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="isLocalActive">
|
||||
<div v-if="!isChoosingSearchBox">
|
||||
<button
|
||||
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="isChoosingSearchBox = true"
|
||||
>
|
||||
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
|
||||
Choose Location Below for Nearby Search
|
||||
</button>
|
||||
<button
|
||||
v-if="isNewMarkerSet"
|
||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="storeSearchBox"
|
||||
>
|
||||
Store This Location for Nearby Search
|
||||
</button>
|
||||
<button
|
||||
v-if="searchBox"
|
||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="forgetSearchBox"
|
||||
>
|
||||
Delete Stored Location
|
||||
</button>
|
||||
<button
|
||||
v-if="isNewMarkerSet"
|
||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="resetLatLong"
|
||||
>
|
||||
Reset Marker
|
||||
</button>
|
||||
<button
|
||||
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="cancelSearchBoxSelect"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Animation -->
|
||||
<div
|
||||
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||
@@ -71,7 +118,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Results List -->
|
||||
<InfiniteScroll @reached-bottom="loadMoreData">
|
||||
<InfiniteScroll @reached-bottom="loadMoreData" v-if="!isChoosingSearchBox">
|
||||
<ul>
|
||||
<li
|
||||
class="border-b border-slate-300"
|
||||
@@ -103,42 +150,103 @@
|
||||
</li>
|
||||
</ul>
|
||||
</InfiniteScroll>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
|
||||
<div
|
||||
v-if="isLocalActive && isChoosingSearchBox"
|
||||
style="height: 600px; width: 800px"
|
||||
>
|
||||
<l-map
|
||||
ref="map"
|
||||
:center="[localCenterLat, localCenterLong]"
|
||||
v-model:zoom="localZoom"
|
||||
@click="setMapPoint"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
layer-type="base"
|
||||
name="OpenStreetMap"
|
||||
/>
|
||||
<l-marker
|
||||
v-if="isNewMarkerSet"
|
||||
:lat-lng="[localCenterLat, localCenterLong]"
|
||||
@click="isNewMarkerSet = false"
|
||||
/>
|
||||
<l-rectangle
|
||||
v-if="isNewMarkerSet"
|
||||
:bounds="[
|
||||
[localCenterLat - localLatDiff, localCenterLong - localLongDiff],
|
||||
[localCenterLat + localLatDiff, localCenterLong + localLongDiff],
|
||||
]"
|
||||
:weight="1"
|
||||
/>
|
||||
</l-map>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { LeafletMouseEvent } from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import {
|
||||
LMap,
|
||||
LMarker,
|
||||
LRectangle,
|
||||
LTileLayer,
|
||||
} from "@vue-leaflet/vue-leaflet";
|
||||
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { didInfo } from "@/libs/endorserServer";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import { didInfo, ProjectData } from "@/libs/endorserServer";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
|
||||
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
||||
const WORLD_ZOOM = 2;
|
||||
const DEFAULT_ZOOM = 2;
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { AlertMessage, QuickNav, InfiniteScroll, EntityIcon },
|
||||
components: {
|
||||
LRectangle,
|
||||
QuickNav,
|
||||
InfiniteScroll,
|
||||
EntityIcon,
|
||||
LMap,
|
||||
LMarker,
|
||||
LTileLayer,
|
||||
},
|
||||
})
|
||||
export default class DiscoverView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: Array<string> = [];
|
||||
apiServer = "";
|
||||
searchTerms = "";
|
||||
alertMessage = "";
|
||||
alertTitle = "";
|
||||
projects: ProjectData[] = [];
|
||||
isChoosingSearchBox = false;
|
||||
isLocalActive = true;
|
||||
isRemoteActive = false;
|
||||
isNewMarkerSet = false;
|
||||
localCenterLat = 0;
|
||||
localCenterLong = 0;
|
||||
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
localCount = 0;
|
||||
localZoom = DEFAULT_ZOOM;
|
||||
remoteCount = 0;
|
||||
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||
isLoading = false;
|
||||
|
||||
// make this function available to the Vue template
|
||||
@@ -149,6 +257,9 @@ export default class DiscoverView extends Vue {
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||
this.resetLatLong();
|
||||
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
|
||||
await accountsDB.open();
|
||||
@@ -158,8 +269,10 @@ export default class DiscoverView extends Vue {
|
||||
this.searchLocal();
|
||||
}
|
||||
|
||||
public async buildHeaders() {
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
public async buildHeaders(): Promise<HeadersInit> {
|
||||
const headers: HeadersInit = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (this.activeDid) {
|
||||
await accountsDB.open();
|
||||
@@ -180,16 +293,13 @@ export default class DiscoverView extends Vue {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public async search(beforeId?: string) {
|
||||
public async searchAll(beforeId?: string) {
|
||||
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
|
||||
|
||||
if (beforeId) {
|
||||
queryParams = queryParams + `&beforeId=${beforeId}`;
|
||||
}
|
||||
|
||||
this.isRemoteActive = true;
|
||||
this.isLocalActive = false;
|
||||
|
||||
try {
|
||||
this.isLoading = true;
|
||||
const response = await fetch(
|
||||
@@ -202,12 +312,13 @@ export default class DiscoverView extends Vue {
|
||||
|
||||
if (response.status !== 200) {
|
||||
const details = await response.text();
|
||||
console.log("Problem with full search:", details);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: `There was a problem accessing the server. Please try again later. (${details})`,
|
||||
text: `There was a problem accessing the server. Try again later.`,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -220,14 +331,15 @@ export default class DiscoverView extends Vue {
|
||||
const plans: ProjectData[] = results.data;
|
||||
if (plans) {
|
||||
for (const plan of plans) {
|
||||
const { name, description, handleId, rowid, issuerDid } = plan;
|
||||
this.projects.push({ name, description, handleId, rowid, issuerDid });
|
||||
const { name, description, handleId, rowid } = plan;
|
||||
this.projects.push({ name, description, handleId, rowid });
|
||||
}
|
||||
this.remoteCount = this.projects.length;
|
||||
} else {
|
||||
throw JSON.stringify(results);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
console.log("Error with feed load:", e);
|
||||
this.$notify(
|
||||
{
|
||||
@@ -244,14 +356,19 @@ export default class DiscoverView extends Vue {
|
||||
}
|
||||
|
||||
public async searchLocal(beforeId?: string) {
|
||||
if (!this.searchBox) {
|
||||
this.projects = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const claimContents =
|
||||
"claimContents=" + encodeURIComponent(this.searchTerms);
|
||||
let queryParams = [
|
||||
claimContents,
|
||||
"minLocLat=40.901000",
|
||||
"maxLocLat=40.904000",
|
||||
"westLocLon=-111.914000",
|
||||
"eastLocLon=-111.909000",
|
||||
"minLocLat=" + this.searchBox.bbox.minLat,
|
||||
"maxLocLat=" + this.searchBox.bbox.maxLat,
|
||||
"westLocLon=" + this.searchBox.bbox.westLong,
|
||||
"eastLocLon=" + this.searchBox.bbox.eastLong,
|
||||
].join("&");
|
||||
|
||||
if (beforeId) {
|
||||
@@ -260,8 +377,6 @@ export default class DiscoverView extends Vue {
|
||||
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.isLocalActive = true;
|
||||
this.isRemoteActive = false;
|
||||
const response = await fetch(
|
||||
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
|
||||
{
|
||||
@@ -272,12 +387,13 @@ export default class DiscoverView extends Vue {
|
||||
|
||||
if (response.status !== 200) {
|
||||
const details = await response.text();
|
||||
console.log("Problem with nearby search:", details);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: `There was a problem accessing the server. Please try again later. (${details})`,
|
||||
text: "There was a problem accessing the server. Try again later.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -302,7 +418,8 @@ export default class DiscoverView extends Vue {
|
||||
} else {
|
||||
throw JSON.stringify(results);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
console.log("Error with feed load:", e);
|
||||
this.$notify(
|
||||
{
|
||||
@@ -328,7 +445,7 @@ export default class DiscoverView extends Vue {
|
||||
if (this.isLocalActive) {
|
||||
this.searchLocal(latestProject["rowid"]);
|
||||
} else if (this.isRemoteActive) {
|
||||
this.search(latestProject["rowid"]);
|
||||
this.searchAll(latestProject["rowid"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,6 +462,128 @@ export default class DiscoverView extends Vue {
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
setMapPoint(event: LeafletMouseEvent) {
|
||||
if (this.isNewMarkerSet) {
|
||||
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
|
||||
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
|
||||
} else {
|
||||
// marker is not set
|
||||
this.localCenterLat = event.latlng.lat;
|
||||
this.localCenterLong = event.latlng.lng;
|
||||
|
||||
let latDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
let longDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
// Guess at a size for the bounding box.
|
||||
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
|
||||
const bounds = event.target.boxZoom?._map?.getBounds();
|
||||
if (bounds) {
|
||||
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
|
||||
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
|
||||
}
|
||||
this.localLatDiff = latDiff;
|
||||
this.localLongDiff = longDiff;
|
||||
this.isNewMarkerSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
public resetLatLong() {
|
||||
if (this.searchBox?.bbox) {
|
||||
const bbox = this.searchBox.bbox;
|
||||
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
|
||||
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
|
||||
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
|
||||
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
|
||||
this.localZoom = WORLD_ZOOM;
|
||||
this.isNewMarkerSet = true;
|
||||
} else {
|
||||
this.isNewMarkerSet = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async storeSearchBox() {
|
||||
if (this.localCenterLong || this.localCenterLat) {
|
||||
try {
|
||||
const newSearchBox = {
|
||||
name: "Local",
|
||||
bbox: {
|
||||
eastLong: this.localCenterLong + this.localLongDiff,
|
||||
maxLat: this.localCenterLat + this.localLatDiff,
|
||||
minLat: this.localCenterLat - this.localLatDiff,
|
||||
westLong: this.localCenterLong - this.localLongDiff,
|
||||
},
|
||||
};
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
searchBoxes: [newSearchBox],
|
||||
});
|
||||
this.searchBox = newSearchBox;
|
||||
this.isChoosingSearchBox = false;
|
||||
this.searchLocal();
|
||||
} catch (err) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Updating Search Settings",
|
||||
text: "Try going to a different page and then coming back.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
console.error(
|
||||
"Telling user to retry the location search setting because:",
|
||||
err,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "No Location Selected",
|
||||
text: "Select a location on the map.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async forgetSearchBox() {
|
||||
try {
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
searchBoxes: [],
|
||||
});
|
||||
this.searchBox = null;
|
||||
this.localCenterLat = 0;
|
||||
this.localCenterLong = 0;
|
||||
this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
this.localZoom = DEFAULT_ZOOM;
|
||||
this.isChoosingSearchBox = false;
|
||||
this.isNewMarkerSet = false;
|
||||
this.searchLocal();
|
||||
} catch (err) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Updating Search Settings",
|
||||
text: "Try going to a different page and then coming back.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
console.error(
|
||||
"Telling user to retry the location search setting because:",
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public cancelSearchBoxSelect() {
|
||||
this.isChoosingSearchBox = false;
|
||||
this.localZoom = WORLD_ZOOM;
|
||||
}
|
||||
|
||||
public computedLocalTabClassNames() {
|
||||
return {
|
||||
"inline-block": true,
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
<script lang="ts">
|
||||
import * as Package from "../../package.json";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class Help extends Vue {
|
||||
|
||||
@@ -111,12 +111,12 @@
|
||||
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold">Quick Action</h2>
|
||||
<p class="mb-4">Show appreciation to a contact:</p>
|
||||
<p class="mb-4">Record a gift from a contact:</p>
|
||||
|
||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||
<li @click="openDialog()">
|
||||
<EntityIcon
|
||||
:entityId="Anonymous"
|
||||
:entityId="null"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
></EntityIcon>
|
||||
@@ -196,40 +196,49 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||
import { db, accountsDB } from "@/db";
|
||||
import { db, accountsDB } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
didInfo,
|
||||
GiverInputInfo,
|
||||
GiverOutputInfo,
|
||||
GiveServerRecord,
|
||||
} from "@/libs/endorserServer";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||
})
|
||||
export default class HomeView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: Array<string> = [];
|
||||
apiServer = "";
|
||||
feedAllLoaded = false;
|
||||
feedData = [];
|
||||
feedPreviousOldestId = null;
|
||||
feedLastViewedId = null;
|
||||
feedPreviousOldestId?: string;
|
||||
feedLastViewedId?: string;
|
||||
isHiddenSpinner = true;
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
numAccounts = 0;
|
||||
|
||||
async beforeCreate() {
|
||||
@@ -237,7 +246,7 @@ export default class HomeView extends Vue {
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
@@ -253,7 +262,7 @@ export default class HomeView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -275,7 +284,8 @@ export default class HomeView extends Vue {
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||
this.updateAllFeed();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -291,7 +301,9 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
|
||||
public async buildHeaders() {
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
const headers: HeadersInit = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (this.activeDid) {
|
||||
await accountsDB.open();
|
||||
@@ -314,7 +326,7 @@ export default class HomeView extends Vue {
|
||||
|
||||
public async updateAllFeed() {
|
||||
this.isHiddenSpinner = false;
|
||||
await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
|
||||
await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId)
|
||||
.then(async (results) => {
|
||||
if (results.data.length > 0) {
|
||||
this.feedData = this.feedData.concat(results.data);
|
||||
@@ -349,7 +361,7 @@ export default class HomeView extends Vue {
|
||||
this.isHiddenSpinner = true;
|
||||
}
|
||||
|
||||
public async retrieveClaims(endorserApiServer, identifier, beforeId) {
|
||||
public async retrieveClaims(endorserApiServer: string, beforeId?: string) {
|
||||
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
||||
const response = await fetch(
|
||||
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
|
||||
@@ -372,13 +384,13 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
giveDescription(giveRecord) {
|
||||
let claim = giveRecord.fullClaim;
|
||||
if (claim.claim) {
|
||||
claim = claim.claim;
|
||||
}
|
||||
giveDescription(giveRecord: GiveServerRecord) {
|
||||
// claim.claim happen for some claims wrapped in a Verifiable Credential
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim;
|
||||
// agent.did is for legacy data, before March 2023
|
||||
const giverDid = claim.agent?.identifier || claim.agent?.did;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const giverDid = claim.agent?.identifier || (claim.agent as any)?.did;
|
||||
const giverInfo = didInfo(
|
||||
giverDid,
|
||||
this.activeDid,
|
||||
@@ -389,7 +401,9 @@ export default class HomeView extends Vue {
|
||||
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
||||
: claim.description || "something unknown";
|
||||
// recipient.did is for legacy data, before March 2023
|
||||
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
|
||||
const gaveRecipientId =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
claim.recipient?.identifier || (claim.recipient as any)?.did;
|
||||
const gaveRecipientInfo = gaveRecipientId
|
||||
? " to " +
|
||||
didInfo(
|
||||
@@ -402,23 +416,28 @@ export default class HomeView extends Vue {
|
||||
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
|
||||
}
|
||||
|
||||
displayAmount(code, amt) {
|
||||
displayAmount(code: string, amt: number) {
|
||||
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
|
||||
}
|
||||
|
||||
currencyShortWordForCode(unitCode, single) {
|
||||
currencyShortWordForCode(unitCode: string, single: boolean) {
|
||||
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
|
||||
}
|
||||
|
||||
openDialog(giver) {
|
||||
this.$refs.customDialog.open(giver);
|
||||
openDialog(giver: GiverInputInfo) {
|
||||
(this.$refs.customDialog as GiftedDialog).open(giver);
|
||||
}
|
||||
|
||||
handleDialogResult(result) {
|
||||
handleDialogResult(result: GiverOutputInfo) {
|
||||
if (result.action === "confirm") {
|
||||
return new Promise((resolve) => {
|
||||
this.recordGive(result.giver?.did, result.description, result.hours);
|
||||
resolve();
|
||||
this.recordGive(
|
||||
result.giver?.did,
|
||||
result.description,
|
||||
result.hours,
|
||||
).then(() => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// action was "cancel" so do nothing
|
||||
@@ -431,7 +450,11 @@ export default class HomeView extends Vue {
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
*/
|
||||
public async recordGive(giverDid, description, hours) {
|
||||
public async recordGive(
|
||||
giverDid?: string,
|
||||
description?: string,
|
||||
hours?: number,
|
||||
) {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
{
|
||||
@@ -493,39 +516,35 @@ export default class HomeView extends Vue {
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.log("Error with give caught:", error);
|
||||
const message =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
"There was an error recording the give.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
this.getGiveErrorMessage(error) ||
|
||||
"There was an error recording the give.",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private setAlert(title, message) {
|
||||
this.alertTitle = title;
|
||||
this.alertMessage = message;
|
||||
}
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
isGiveCreationError(result) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isGiveCreationError(result: any) {
|
||||
return result.status !== 201 || result.data?.error;
|
||||
}
|
||||
|
||||
getGiveCreationErrorMessage(result) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getGiveCreationErrorMessage(result: any) {
|
||||
return result.data?.error?.message;
|
||||
}
|
||||
|
||||
getGiveErrorMessage(error) {
|
||||
return error.userMessage || error.response?.data?.error?.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -62,34 +62,38 @@
|
||||
>
|
||||
No Identity
|
||||
</a>
|
||||
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { AppString } from "@/constants/app";
|
||||
import { db, accountsDB } from "@/db";
|
||||
import { db, accountsDB } from "@/db/index";
|
||||
import { AccountsSchema } from "@/db/tables/accounts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
@Component({ components: { AlertMessage, QuickNav } })
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class IdentitySwitcherView extends Vue {
|
||||
Constants = AppString;
|
||||
public accounts: AccountsSchema;
|
||||
public activeDid;
|
||||
public firstName;
|
||||
public lastName;
|
||||
public alertTitle;
|
||||
public alertMessage;
|
||||
public otherIdentities = [];
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
Constants = AppString;
|
||||
public accounts: typeof AccountsSchema;
|
||||
public activeDid = "";
|
||||
public apiServer = "";
|
||||
public apiServerInput = "";
|
||||
public firstName = "";
|
||||
public lastName = "";
|
||||
public otherIdentities: Array<{ did: string }> = [];
|
||||
public showContactGives = false;
|
||||
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
@@ -126,31 +130,20 @@ export default class IdentitySwitcherView extends Vue {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err.message ===
|
||||
"Attempted to load account records with no identity available."
|
||||
) {
|
||||
this.limitsMessage = "No identity.";
|
||||
this.loadingLimits = false;
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Creating Account",
|
||||
text: "Clear your cache and start over (after data backup).",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
console.error(
|
||||
"Telling user to clear cache at page create because:",
|
||||
err,
|
||||
);
|
||||
}
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Loading Accounts",
|
||||
text: "Clear your cache and start over (after data backup).",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
console.error("Telling user to clear cache at page create because:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async switchAccount(did: string) {
|
||||
async switchAccount(did?: string) {
|
||||
// 0 means none
|
||||
if (did === "0") {
|
||||
did = undefined;
|
||||
@@ -159,7 +152,7 @@ export default class IdentitySwitcherView extends Vue {
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: did,
|
||||
});
|
||||
this.activeDid = did;
|
||||
this.activeDid = did || "";
|
||||
this.otherIdentities = [];
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
deriveAddress,
|
||||
newIdentifier,
|
||||
} from "../libs/crypto";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -73,7 +73,7 @@ import {
|
||||
deriveAddress,
|
||||
newIdentifier,
|
||||
} from "../libs/crypto";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
|
||||
@Component({
|
||||
@@ -87,9 +87,9 @@ export default class ImportAccountView extends Vue {
|
||||
async mounted() {
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
const seedDids = {};
|
||||
const seedDids: Record<string, Array<string>> = {};
|
||||
accounts.forEach((account) => {
|
||||
const prevDids = seedDids[account.mnemonic] || [];
|
||||
const prevDids: Array<string> = seedDids[account.mnemonic] || [];
|
||||
seedDids[account.mnemonic] = prevDids.concat([account.did]);
|
||||
});
|
||||
this.didArrays = Object.values(seedDids);
|
||||
@@ -107,9 +107,9 @@ export default class ImportAccountView extends Vue {
|
||||
public async incrementDerivation() {
|
||||
await accountsDB.open();
|
||||
// find the maximum derivation path for the selected DIDs
|
||||
const selectedArray: Array<string> = this.didArrays.find(
|
||||
(dids) => dids[0] === this.selectedArrayFirstDid,
|
||||
);
|
||||
const selectedArray: Array<string> =
|
||||
this.didArrays.find((dids) => dids[0] === this.selectedArrayFirstDid) ||
|
||||
[];
|
||||
const allMatchingAccounts = await accountsDB.accounts
|
||||
.where("did")
|
||||
.anyOf(...selectedArray)
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { db } from "@/db";
|
||||
import { db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -97,10 +97,6 @@
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -111,20 +107,27 @@ import * as didJwt from "did-jwt";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||
import { useAppStore } from "@/store/app";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import { PlanVerifiableCredential } from "@/libs/endorserServer";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { AlertMessage, LMap, LMarker, LTileLayer },
|
||||
components: { LMap, LMarker, LTileLayer },
|
||||
})
|
||||
export default class NewEditProjectView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
apiServer = "";
|
||||
description = "";
|
||||
errorMessage = "";
|
||||
@@ -140,7 +143,7 @@ export default class NewEditProjectView extends Vue {
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
@@ -156,7 +159,7 @@ export default class NewEditProjectView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -215,7 +218,7 @@ export default class NewEditProjectView extends Vue {
|
||||
|
||||
private async SaveProject(identity: IIdentifier) {
|
||||
// Make a claim
|
||||
const vcClaim: VerifiableCredential = {
|
||||
const vcClaim: PlanVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "PlanAction",
|
||||
name: this.projectName,
|
||||
@@ -270,19 +273,15 @@ export default class NewEditProjectView extends Vue {
|
||||
// version shows up here: https://api.endorser.ch/api-docs/
|
||||
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
|
||||
this.errorMessage = "";
|
||||
this.alertTitle = "";
|
||||
this.alertMessage = "";
|
||||
|
||||
// handleId is new in server v release-1.6.0; remove fullIri when that
|
||||
// version shows up here: https://api.endorser.ch/api-docs/
|
||||
useAppStore().setProjectId(
|
||||
resp.data.success.handleId || resp.data.success.fullIri,
|
||||
);
|
||||
setTimeout(
|
||||
function (that: Vue) {
|
||||
const route = {
|
||||
name: "project",
|
||||
};
|
||||
that.$router.push(route);
|
||||
function (that: NewEditProjectView) {
|
||||
that.$router.push({ name: "project" });
|
||||
},
|
||||
2000,
|
||||
this,
|
||||
@@ -290,11 +289,13 @@ export default class NewEditProjectView extends Vue {
|
||||
}
|
||||
} catch (error) {
|
||||
let userMessage = "There was an error saving the project.";
|
||||
const serverError = error as AxiosError;
|
||||
const serverError = error as AxiosError<{
|
||||
error?: { message?: string };
|
||||
}>;
|
||||
if (serverError) {
|
||||
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
||||
console.log(serverError);
|
||||
userMessage = serverError.response.data.error.message; // This is info for the user.
|
||||
userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user.
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
<script lang="ts">
|
||||
import "dexie-export-import";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class AccountViewView extends Vue {
|
||||
|
||||
@@ -39,6 +39,16 @@
|
||||
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||
{{ timeSince }}
|
||||
</div>
|
||||
<div v-if="latitude || longitude">
|
||||
<fa icon="location-dot" class="fa-fw text-slate-400"></fa>
|
||||
<a
|
||||
:href="getOpenStreetMapUrl()"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
Map View
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,7 +95,7 @@
|
||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||
<li @click="openDialog()">
|
||||
<EntityIcon
|
||||
:entityId="Anonymous"
|
||||
:entityId="null"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
></EntityIcon>
|
||||
@@ -190,40 +200,45 @@
|
||||
message="Received from"
|
||||
>
|
||||
</GiftedDialog>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { AxiosError } from "axios";
|
||||
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
||||
import * as moment from "moment";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
didInfo,
|
||||
GiverInputInfo,
|
||||
GiverOutputInfo,
|
||||
GiveServerRecord,
|
||||
ResultWithType,
|
||||
} from "@/libs/endorserServer";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||
})
|
||||
export default class ProjectViewView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
alertMessage = "";
|
||||
alertTitle = "";
|
||||
allMyDids: Array<string> = [];
|
||||
allContacts: Array<Contact> = [];
|
||||
apiServer = "";
|
||||
@@ -231,6 +246,8 @@ export default class ProjectViewView extends Vue {
|
||||
expanded = false;
|
||||
givesToThis: Array<GiveServerRecord> = [];
|
||||
givesByThis: Array<GiveServerRecord> = [];
|
||||
latitude = 0;
|
||||
longitude = 0;
|
||||
name = "";
|
||||
issuer = "";
|
||||
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
||||
@@ -254,7 +271,7 @@ export default class ProjectViewView extends Vue {
|
||||
this.LoadProject(identity);
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
@@ -270,7 +287,7 @@ export default class ProjectViewView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -288,7 +305,12 @@ export default class ProjectViewView extends Vue {
|
||||
}
|
||||
|
||||
// Isn't there a better way to make this available to the template?
|
||||
didInfo(did, activeDid, dids, contacts) {
|
||||
didInfo(
|
||||
did: string,
|
||||
activeDid: string,
|
||||
dids: Array<string>,
|
||||
contacts: Array<Contact>,
|
||||
) {
|
||||
return didInfo(did, activeDid, dids, contacts);
|
||||
}
|
||||
|
||||
@@ -305,7 +327,7 @@ export default class ProjectViewView extends Vue {
|
||||
this.apiServer +
|
||||
"/api/claim/byHandle/" +
|
||||
encodeURIComponent(this.projectId);
|
||||
const headers = {
|
||||
const headers: RawAxiosRequestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (identity) {
|
||||
@@ -326,6 +348,8 @@ export default class ProjectViewView extends Vue {
|
||||
this.name = resp.data.claim?.name || "(no name)";
|
||||
this.description = resp.data.claim?.description || "(no description)";
|
||||
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
||||
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
|
||||
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
|
||||
} else if (resp.status === 404) {
|
||||
// actually, axios throws an error so we never get here
|
||||
this.$notify(
|
||||
@@ -437,15 +461,35 @@ export default class ProjectViewView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
openDialog(contact) {
|
||||
this.$refs.customDialog.open(contact);
|
||||
openDialog(contact: GiverInputInfo) {
|
||||
const dialog: GiftedDialog = this.$refs.customDialog as GiftedDialog;
|
||||
dialog.open(contact);
|
||||
}
|
||||
|
||||
handleDialogResult(result) {
|
||||
getOpenStreetMapUrl() {
|
||||
// Google URL is https://maps.google.com/?q=LAT,LONG
|
||||
return (
|
||||
"https://www.openstreetmap.org/?mlat=" +
|
||||
this.latitude +
|
||||
"&mlon=" +
|
||||
this.longitude +
|
||||
"#map=15/" +
|
||||
this.latitude +
|
||||
"/" +
|
||||
this.longitude
|
||||
);
|
||||
}
|
||||
|
||||
handleDialogResult(result: GiverOutputInfo) {
|
||||
if (result.action === "confirm") {
|
||||
return new Promise((resolve) => {
|
||||
this.recordGive(result.contact?.did, result.description, result.hours);
|
||||
resolve();
|
||||
this.recordGive(
|
||||
result.giver?.did,
|
||||
result.description,
|
||||
result.hours,
|
||||
).then(() => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// action was not "confirm" so do nothing
|
||||
@@ -458,7 +502,7 @@ export default class ProjectViewView extends Vue {
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
*/
|
||||
async recordGive(giverDid, description, hours) {
|
||||
async recordGive(giverDid?: string, description?: string, hours?: number) {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
{
|
||||
@@ -482,10 +526,7 @@ export default class ProjectViewView extends Vue {
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
} else {
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
const result = await createAndSubmitGive(
|
||||
this.axios,
|
||||
@@ -497,21 +538,7 @@ export default class ProjectViewView extends Vue {
|
||||
hours,
|
||||
this.projectId,
|
||||
);
|
||||
|
||||
if (result.status !== 201 || result.data?.error) {
|
||||
console.log("Error with give result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
result.data?.error?.message ||
|
||||
"There was an error recording the give.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
if (result.type == "success") {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -521,21 +548,27 @@ export default class ProjectViewView extends Vue {
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
console.log("Error with give creation:", result);
|
||||
if (result.type != "error") {
|
||||
console.log(
|
||||
"... and it has an unexpected result type of",
|
||||
(result as ResultWithType).type,
|
||||
);
|
||||
}
|
||||
const message =
|
||||
result?.error?.userMessage ||
|
||||
"There was an error recording the Give.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Error with give caught:", e);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
e.userMessage ||
|
||||
e.response?.data?.error?.message ||
|
||||
"There was an error recording the give.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,28 +67,33 @@
|
||||
</li>
|
||||
</ul>
|
||||
</InfiniteScroll>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { ProjectData } from "@/libs/endorserServer";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { InfiniteScroll, AlertMessage, QuickNav, EntityIcon },
|
||||
components: { InfiniteScroll, QuickNav, EntityIcon },
|
||||
})
|
||||
export default class ProjectsView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
apiServer = "";
|
||||
projects: ProjectData[] = [];
|
||||
current: IIdentifier;
|
||||
@@ -119,21 +124,30 @@ export default class ProjectsView extends Vue {
|
||||
if (resp.status === 200 || !resp.data.data) {
|
||||
const plans: ProjectData[] = resp.data.data;
|
||||
for (const plan of plans) {
|
||||
const { name, description, handleId = plan.fullIri, rowid } = plan;
|
||||
const { name, description, handleId, rowid } = plan;
|
||||
this.projects.push({ name, description, handleId, rowid });
|
||||
}
|
||||
} else {
|
||||
console.log("Bad server response & data:", resp.status, resp.data);
|
||||
throw Error("Failed to get projects from the server.");
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to get projects from the server. Try again later.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error loading projects:", error.message);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Got error loading projects:", error.message || error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Got an error loading projects: " + error.message,
|
||||
text: "Got an error loading projects.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -177,7 +191,7 @@ export default class ProjectsView extends Vue {
|
||||
await this.dataLoader(url, token);
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
|
||||
@@ -50,28 +50,34 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>You do not have an active identity.</div>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import * as R from "ramda";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
@Component({ components: { AlertMessage, QuickNav } })
|
||||
interface Account {
|
||||
mnemonic: string;
|
||||
}
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class SeedBackupView extends Vue {
|
||||
activeAccount = null;
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeAccount: Account | null | undefined = null;
|
||||
numAccounts = 0;
|
||||
showSeed = false;
|
||||
alertMessage = "";
|
||||
alertTitle = "";
|
||||
|
||||
// 'created' hook runs when the Vue instance is first created
|
||||
async created() {
|
||||
@@ -84,7 +90,7 @@ export default class SeedBackupView extends Vue {
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
this.numAccounts = accounts.length;
|
||||
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
console.error("Got an error loading an identity:", err);
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB } from "@/db";
|
||||
import { accountsDB } from "@/db/index";
|
||||
|
||||
@Component({
|
||||
components: {},
|
||||
|
||||
@@ -34,25 +34,35 @@
|
||||
</div>
|
||||
<button class="float-right" @click="captureGraphics()">Screenshot</button>
|
||||
<div id="scene-container" class="h-screen"></div>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
|
||||
import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { World } from "@/components/World/World.js";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
@Component({ components: { AlertMessage, World, QuickNav } })
|
||||
interface RendererSVGType {
|
||||
domElement: Element;
|
||||
}
|
||||
|
||||
interface Dictionary<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({ components: { World, QuickNav } })
|
||||
export default class StatisticsView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
world: World;
|
||||
worldProperties: WorldProperties = {};
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
worldProperties: Dictionary<number> = {};
|
||||
|
||||
mounted() {
|
||||
try {
|
||||
@@ -60,14 +70,14 @@ export default class StatisticsView extends Vue {
|
||||
const newWorld = new World(container, this);
|
||||
newWorld.start();
|
||||
this.world = newWorld;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} catch (err: unknown) {
|
||||
const error = err as Error;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Mounting Error",
|
||||
text: err.message,
|
||||
text: error.message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -85,12 +95,12 @@ export default class StatisticsView extends Vue {
|
||||
ExportToSVG(rendererSVG, "test.svg");
|
||||
}
|
||||
|
||||
public setWorldProperty(propertyName, propertyValue) {
|
||||
public setWorldProperty(propertyName: string, propertyValue: number) {
|
||||
this.worldProperties[propertyName] = propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
function ExportToSVG(rendererSVG, filename) {
|
||||
function ExportToSVG(rendererSVG: RendererSVGType, filename: string) {
|
||||
const XMLS = new XMLSerializer();
|
||||
const svgfile = XMLS.serializeToString(rendererSVG.domElement);
|
||||
const svgData = svgfile;
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"useDefineForClassFields": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"useDefineForClassFields": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": "./src",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/components/*": ["components/*"],
|
||||
"@/views/*": ["views/*"],
|
||||
"@/db/*": ["db/*"],
|
||||
"@/libs/*": ["libs/*"],
|
||||
"@/constants/*": ["constants/*"],
|
||||
"@/store/*": ["store/*"],
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
44
web-push.md
44
web-push.md
@@ -314,3 +314,47 @@ OneSignal) can perform in the role of such proxies.
|
||||
|
||||
#4 -The INTERMEDIARY- doesn't appear to be anything we should be spending our
|
||||
time on.
|
||||
|
||||
A BROWSER may also remove a subscription. In order to remove a subscription,
|
||||
the registration record must be retrieved from the serviceWorker using
|
||||
`navigator.serviceWorker.ready`. Within the `ready` property is the
|
||||
`pushManager` which has a `getSubscription` method. Once you have the
|
||||
subscription object, you may call the `unsubscribe` method. `unsubscribe` is
|
||||
asynchronnous and returns a boolean true if it is successful in removing the
|
||||
subscription and false if not.
|
||||
|
||||
|
||||
```
|
||||
async function unsubscribeFromPush() {
|
||||
// Check if the browser supports service workers
|
||||
if ("serviceWorker" in navigator) {
|
||||
// Get the registration object for the service worker
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
|
||||
// Get the existing subscription
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
|
||||
if (subscription) {
|
||||
// Unsubscribe
|
||||
const successful = await subscription.unsubscribe();
|
||||
if (successful) {
|
||||
console.log("Successfully unsubscribed from push notifications.");
|
||||
// You can also inform your server to remove this subscription
|
||||
} else {
|
||||
console.log("Failed to unsubscribe from push notifications.");
|
||||
}
|
||||
} else {
|
||||
console.log("No subscription was found.");
|
||||
}
|
||||
} else {
|
||||
console.log("Service workers are not supported by this browser.");
|
||||
}
|
||||
}
|
||||
|
||||
// Unsubscribe from push notifications
|
||||
unsubscribeFromPush().catch((err) => {
|
||||
console.error("An error occurred while unsubscribing from push notifications", err);
|
||||
});
|
||||
```
|
||||
|
||||
NOTE: We could offer an option within the app to "mute" these notifications. This wouldn't turn off the notifications at the browser level, but you could make it so that your Service Worker doesn't display them even if it receives them.
|
||||
|
||||
Reference in New Issue
Block a user