Browse Source

Merge branch 'master' into why-migrate-fail

pull/77/head
Matthew Aaron Raymer 11 months ago
parent
commit
aa6cf0c9f6
  1. 42322
      package-lock.json
  2. 72
      package.json
  3. 34
      project.task.yaml
  4. 215
      src/components/GiftedDialog.vue
  5. 3
      src/db/tables/settings.ts
  6. 38
      src/router/index.ts
  7. 266
      src/views/AccountViewView.vue
  8. 147
      src/views/ContactGiftingView.vue
  9. 4
      src/views/ContactQRScanShowView.vue
  10. 108
      src/views/ContactsView.vue
  11. 271
      src/views/HomeView.vue
  12. 18
      src/views/IdentitySwitcherView.vue
  13. 38
      src/views/NewEditAccountView.vue
  14. 117
      src/views/ProjectViewView.vue
  15. 2
      src/views/ProjectsView.vue
  16. 15
      web-push.md

42322
package-lock.json

File diff suppressed because it is too large

72
package.json

@ -1,6 +1,6 @@
{
"name": "kickstart-for-time-pwa",
"version": "0.1.2",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
@ -9,59 +9,58 @@
},
"dependencies": {
"@ethersproject/hdnode": "^5.7.0",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0",
"@veramo/core": "^5.2.0",
"@veramo/credential-w3c": "^5.2.0",
"@veramo/data-store": "^5.2.0",
"@veramo/did-manager": "^5.1.2",
"@veramo/did-provider-ethr": "^5.1.2",
"@veramo/did-resolver": "^5.2.0",
"@veramo/key-manager": "^5.1.2",
"@vueuse/core": "^10.2.1",
"@veramo/core": "^5.4.1",
"@veramo/credential-w3c": "^5.4.1",
"@veramo/data-store": "^5.4.1",
"@veramo/did-manager": "^5.4.1",
"@veramo/did-provider-ethr": "^5.4.1",
"@veramo/did-resolver": "^5.4.1",
"@veramo/key-manager": "^5.4.1",
"@vueuse/core": "^10.4.1",
"@zxing/text-encoding": "^0.9.0",
"axios": "^1.4.0",
"axios": "^1.5.0",
"buffer": "^6.0.3",
"class-transformer": "^0.5.1",
"core-js": "^3.31.1",
"core-js": "^3.32.1",
"dexie": "^3.2.4",
"dexie-export-import": "^4.0.7",
"did-jwt": "^7.2.4",
"ethereum-cryptography": "^2.0.0",
"did-jwt": "^7.2.7",
"ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.0.0",
"ethr-did-resolver": "^8.1.2",
"jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9",
"localstorage-slim": "^2.4.0",
"luxon": "^3.3.0",
"localstorage-slim": "^2.5.0",
"luxon": "^3.4.3",
"merkletreejs": "^0.3.10",
"moment": "^2.29.4",
"notiwind": "^2.0.2",
"papaparse": "^5.4.1",
"pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.1.0",
"pinia-plugin-persistedstate": "^3.2.0",
"qr-code-generator-vue3": "^1.4.21",
"ramda": "^0.29.0",
"readable-stream": "^4.4.2",
"reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2",
"three": "^0.154.0",
"three": "^0.156.1",
"vue": "^3.3.4",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "^2.1.20",
"vue-qrcode-reader": "^5.3.4",
"vue-router": "^4.2.3",
"vue-facing-decorator": "^3.0.2",
"vue-router": "^4.2.4",
"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",
"@typescript-eslint/parser": "^5.61.0",
"@types/three": "^0.155.1",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"@vue-leaflet/vue-leaflet": "^0.10.1",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
@ -71,16 +70,15 @@
"@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.14",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^5.0.0-alpha.1",
"eslint-plugin-vue": "^9.15.1",
"autoprefixer": "^10.4.15",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"leaflet": "^1.9.4",
"postcss": "^8.4.24",
"prettier": "^3.0.0",
"tailwindcss": "^3.3.2",
"typescript": "~5.1.6"
},
"pkgx": "node^18 npm^10"
"postcss": "^8.4.29",
"prettier": "^3.0.3",
"tailwindcss": "^3.3.3",
"typescript": "~5.2.2"
}
}

34
project.task.yaml

@ -2,23 +2,17 @@
tasks:
- 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)
- 40 notifications :
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
- 01 bug - we get a 404 when reloading the page anyplace except "/", and it's hard to get back
- .1 test - make sure that a registration failure (including network failure) doesn't give a success message (which may have happened during board meeting)
- .1 don't allow to even see the claim actions if they're not registered
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
- 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
- .1 add instructions for map location selection
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew assignee-group:ui
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s) assignee-group:ui
- SEE: https://github.com/emmanuelsw/notiwind assignee:jose assignee-group:ui
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
- Home Feed & Quick Give screen :
- 01 save the feed-viewed status in settings storage ("afterQuery")
@ -27,18 +21,16 @@ tasks:
- 24 Move to Vite assignee:matthew
- .2 fit more icons on home screen, with a "more" button to contacts page if there is more than 2 rows
- .1 Remove notification alert visuals on home page
- .5 switch so DiscoverView shows anywhere by default, and no number unless search is done (and maybe a better filter UI, including "mine" to consolidate with ProjectsView)
- .2 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
- .5 Add infinite scroll to gifts on the home page
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
- .1 when creating a plan, select location and then make sure you can deselect on Android
- .5 include a version, maybe the hash of the latest commit -- figuring out how it works on prod now
- .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
- .5 add project ID to the URL of the project-view, to make a project publicly-accessible
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
- .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 on ProjectViewView, show different messages for "to" and "from" sections if none exist
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
- .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"
@ -48,16 +40,15 @@ tasks:
- Discuss whether the remaining tasks are worthwhile before MVP release.
- 04 allow user to download claims, mine + ones I can see about me from others
- .5 change the derivation path, and regenerate test IDs
- 02 allow user to create new DIDs from the same seed phrase (ie. increment derivation path)
- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages assignee-group:ui
- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages
- .5 customize favicon assignee-group:ui
- .5 Do we want to combine first name & last name?
- .2 Show a warning if both giver and recipient are the same (but still allow?) assignee-group:ui
- .2 Show a warning if both giver and recipient are the same (but still allow?)
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
- .5 Display a more appealing confirmation on the map when erasing the marker assignee-group:ui
- .5 Display a more appealing confirmation on the map when erasing the marker
- .5 make a VC details page
- .1 Add units or different icon to the coins (to distinguish $, BTC, etc)
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
- .1 remove firstName (& lastName) from localStorage
- contacts v+ :
- 01 Import all the non-sensitive data (ie. contacts & settings).
@ -66,6 +57,7 @@ tasks:
- stats v1 :
- 01 show numeric stats
- 04 show different graphic for projects vs people on world
- 01 link to world for specific stats
- .5 don't load another instance of a bush if it already exists
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")

215
src/components/GiftedDialog.vue

@ -2,7 +2,7 @@
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">
{{ message }} {{ giver?.name || "somebody not specified" }}
{{ message }} {{ giver?.name || "somebody not named" }}
</h1>
<input
type="text"
@ -51,18 +51,57 @@
</template>
<script lang="ts">
import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
import { GiverInputInfo, GiverOutputInfo } from "@/libs/endorserServer";
import { Vue, Component, Prop } from "vue-facing-decorator";
import { createAndSubmitGive, GiverInputInfo } from "@/libs/endorserServer";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Account } from "@/db/tables/accounts";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component
export default class GiftedDialog extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@Prop message = "";
@Prop projectId = "";
activeDid = "";
apiServer = "";
giver?: GiverInputInfo;
description = "";
hours = "0";
visible = false;
async created() {
try {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.log("Error retrieving settings from database:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
},
-1,
);
}
}
open(giver: GiverInputInfo) {
this.giver = giver;
this.visible = true;
@ -80,27 +119,169 @@ export default class GiftedDialog extends Vue {
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
}
@Emit("dialog-result")
confirm(): GiverOutputInfo {
const result = {
action: "confirm",
giver: this.giver,
hours: parseFloat(this.hours),
description: this.description,
};
cancel() {
this.close();
this.description = "";
this.giver = undefined;
this.hours = "0";
return result;
}
@Emit("dialog-result")
cancel(): GiverOutputInfo {
const result = { action: "cancel" };
async confirm() {
this.close();
return result;
this.$notify(
{
group: "alert",
type: "toast",
text: "Recording the give...",
title: "",
},
1000,
);
// this is asynchronous, but we don't need to wait for it to complete
this.recordGive(
this.giver?.did as string | undefined,
this.description,
parseFloat(this.hours),
).then(() => {
this.description = "";
this.giver = undefined;
this.hours = "0";
});
}
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records for DID ${activeDid} but no identity was found",
);
}
return identity;
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
public async recordGive(
giverDid?: string,
description?: string,
hours?: number,
) {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identity before you can record a give.",
},
-1,
);
return;
}
if (!description && !hours) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must enter a description or some number of hours.",
},
-1,
);
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
this.projectId,
);
if (
result.type === "error" ||
this.isGiveCreationError(result.response)
) {
const errorMessage = this.getGiveCreationErrorMessage(result);
console.log("Error with give creation result:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: errorMessage || "There was an error creating the give.",
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "That gift was recorded.",
},
10000,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log("Error with give recordation 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: message,
},
-1,
);
}
}
// Helper functions for readability
/**
* @param result response "data" from the server
* @returns true if the result indicates an error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isGiveCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
/**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getGiveCreationErrorMessage(result: any) {
return (
result.error?.userMessage ||
result.error?.error ||
result.response?.data?.error?.message
);
}
}
</script>

3
src/db/tables/settings.ts

@ -12,7 +12,8 @@ export type Settings = {
activeDid?: string;
apiServer?: string;
firstName?: string;
lastName?: string;
isRegistered?: boolean;
lastName?: string; // deprecated, pre v 0.1.3
lastViewedClaimId?: string;
searchBoxes?: Array<{
name: string;

38
src/router/index.ts

@ -33,7 +33,6 @@ const routes: Array<RouteRecordRaw> = [
name: "home",
component: () =>
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/account",
@ -79,15 +78,6 @@ const routes: Array<RouteRecordRaw> = [
name: "contacts",
component: () =>
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/scan-contact",
name: "scan-contact",
component: () =>
import(
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
),
},
{
path: "/discover",
@ -101,6 +91,14 @@ const routes: Array<RouteRecordRaw> = [
component: () =>
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
},
{
path: "/identity-switcher",
name: "identity-switcher",
component: () =>
import(
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
),
},
{
path: "/import-account",
name: "import-account",
@ -149,14 +147,6 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
),
},
{
path: "/identity-switcher",
name: "identity-switcher",
component: () =>
import(
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
),
},
{
path: "/project",
name: "project",
@ -170,6 +160,14 @@ const routes: Array<RouteRecordRaw> = [
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/scan-contact",
name: "scan-contact",
component: () =>
import(
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
),
},
{
path: "/seed-backup",
name: "seed-backup",
@ -196,9 +194,7 @@ const routes: Array<RouteRecordRaw> = [
path: "/test",
name: "test",
component: () =>
import(
/* webpackChunkName: "test" */ "../views/TestView.vue"
),
import(/* webpackChunkName: "test" */ "../views/TestView.vue"),
},
];

266
src/views/AccountViewView.vue

@ -52,7 +52,17 @@
<!-- Identity Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-semibold mb-2">{{ firstName }} {{ lastName }}</h2>
<h2 v-if="givenName" class="text-xl font-semibold mb-2">
{{ givenName }}
</h2>
<span v-else>
<router-link
:to="{ name: 'new-edit-account' }"
class="text-xs bg-blue-500 text-white px-1.5 py-1 rounded-md"
>
(set name)
</router-link>
</span>
<div class="text-slate-500 text-sm font-bold">ID</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
@ -67,53 +77,11 @@
</button>
<span v-show="showDidCopy">Copied!</span>
</div>
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ publicBase64 }}</code>
<button
@click="
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showB64Copy">Copied!</span>
</div>
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ publicHex }}</code>
<button
@click="
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showPubCopy">Copied!</span>
</div>
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ derivationPath }}</code>
<button
@click="
doCopyTwoSecRedo(derivationPath, () => (showDerCopy = !showDerCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDerCopy">Copied!</span>
</div>
</div>
<router-link
:to="{ name: 'new-edit-account' }"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
class="block text-center text-lg font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md mb-2"
>
Edit Identity
</router-link>
@ -132,8 +100,10 @@
)
"
>
<!-- label -->
<div>App Notifications</div>
<!-- toggle -->
<div class="relative">
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" name="toggleNotifications" class="sr-only" />
<!-- line -->
@ -143,8 +113,6 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
<!-- label -->
<div class="ml-2">App Notifications</div>
</label>
<label
for="toggleMuteNotifications"
@ -159,8 +127,10 @@
)
"
>
<!-- label -->
<div>Mute Notifications</div>
<!-- toggle -->
<div class="relative">
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
@ -174,8 +144,6 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
<!-- label -->
<div class="ml-2">Mute Notifications</div>
</label>
</div>
@ -192,32 +160,13 @@
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="exportDatabase()"
>
Download Settings & Contacts (excluding Identifier Data)
Download Settings & Contacts
<br />
(excluding Identifier Data)
</a>
<a ref="downloadLink" />
<!-- QR code popup -->
<dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md">
<div class="text-slate-500 text-center">
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
</div>
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
<button
value="cancel"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Copy to Clipboard
</button>
<button
value="cancel"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Close
</button>
</dialog>
<div class="flex py-2">
<div v-if="activeDid" class="flex py-2">
<button class="text-center text-md text-blue-500" @click="checkLimits()">
Check Limits
</button>
@ -252,14 +201,76 @@
>
Advanced
</h3>
<div v-if="showAdvanced">
<!-- Deep Identity Details -->
<h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
Deep Identity Details
</h2>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
<div
class="text-sm text-slate-500 flex justify-start items-center mb-1"
>
<code class="truncate">{{ publicBase64 }}</code>
<button
@click="
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showB64Copy">Copied!</span>
</div>
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
<div
class="text-sm text-slate-500 flex justify-start items-center mb-1"
>
<code class="truncate">{{ publicHex }}</code>
<button
@click="
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showPubCopy">Copied!</span>
</div>
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
<div
class="text-sm text-slate-500 flex justify-start items-center mb-1"
>
<code class="truncate">{{ derivationPath }}</code>
<button
@click="
doCopyTwoSecRedo(
derivationPath,
() => (showDerCopy = !showDerCopy),
)
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDerCopy">Copied!</span>
</div>
</div>
<label
for="toggleShowAmounts"
class="flex items-center cursor-pointer mb-6"
class="flex items-center cursor-pointer py-2"
@click="handleChange"
>
<!-- label -->
<h2 class="text-slate-500 text-sm font-bold mb-2">
Show amounts given with contacts
</h2>
<!-- toggle -->
<div class="relative">
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
@ -274,23 +285,31 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
<!-- label -->
<div class="ml-2">Show amounts given with contacts</div>
</label>
<div class="flex py-2">
<!-- id used by puppeteer test script -->
<router-link
id="switch-identity-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 class="text-blue-500">
<!-- id used by puppeteer test script -->
<router-link
id="switch-identity-link"
:to="{ name: 'identity-switcher' }"
class="block text-center"
>
Switch Identity / No Identity
</router-link>
</button>
</div>
<div class="flex py-2">
Claim Server
<button class="text-blue-500">
<router-link :to="{ name: 'statistics' }" class="block text-center">
See Achievements & Statistics
</router-link>
</button>
</div>
<div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
<input
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
@ -322,17 +341,6 @@
Use Local
</button>
</div>
<div>
<button class="text-blue-500">
<router-link
:to="{ name: 'statistics' }"
class="block text-center py-3"
>
See Achievements & Statistics
</router-link>
</button>
</div>
</div>
</section>
</template>
@ -346,7 +354,7 @@ import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
@ -368,14 +376,6 @@ interface IAccount {
derivationPath: string;
}
interface SettingsType {
activeDid?: string;
apiServer?: string;
firstName?: string;
lastName?: string;
showContactGivesInline?: boolean;
}
@Component({ components: { QuickNav } })
export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@ -386,8 +386,8 @@ export default class AccountViewView extends Vue {
apiServer = "";
apiServerInput = "";
derivationPath = "";
firstName = "";
lastName = "";
givenName = "";
isRegistered = false;
numAccounts = 0;
publicHex = "";
publicBase64 = "";
@ -402,8 +402,6 @@ export default class AccountViewView extends Vue {
showPubCopy = false;
showAdvanced = false;
alertMessage = "";
alertTitle = "";
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
try {
@ -428,7 +426,7 @@ export default class AccountViewView extends Vue {
}
// Return parsed identity or null if not found
return JSON.parse(account?.identity || "null");
return JSON.parse((account?.identity as string) || "null");
}
/**
@ -509,12 +507,14 @@ export default class AccountViewView extends Vue {
* Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/
initializeState(settings: SettingsType | undefined) {
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || "";
this.firstName = settings?.firstName || "";
this.lastName = settings?.lastName || "";
initializeState(settings: Settings | undefined) {
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.showContactGives = !!settings?.showContactGivesInline;
}
@ -531,7 +531,7 @@ export default class AccountViewView extends Vue {
) {
this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta.derivationPath;
this.derivationPath = identity.keys[0].meta.derivationPath as string;
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did,
@ -701,6 +701,27 @@ export default class AccountViewView extends Vue {
const resp = await this.fetchRateLimits(identity);
if (resp.status === 200) {
this.limits = resp.data;
if (!this.isRegistered) {
// the user is not known to be registered, but they are so let's record it
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
isRegistered: true,
});
this.isRegistered = true;
} catch (err) {
console.log("Got an error updating settings:", err);
this.$notify(
{
group: "alert",
type: "warning",
title: "Update Error",
text: "Unable to update your settings. Check claim limits again.",
},
-1,
);
}
}
}
} catch (error) {
this.handleRateLimitsError(error);
@ -729,8 +750,13 @@ export default class AccountViewView extends Vue {
private handleRateLimitsError(error: unknown) {
if (error instanceof AxiosError) {
const data = error.response?.data as ErrorResponse;
this.limitsMessage = data?.error?.message || "Bad server response.";
console.error("Bad response retrieving limits:", error);
this.limitsMessage =
(data?.error?.message as string) || "Bad server response.";
console.log(
"Got bad response retrieving limits, which usually means user isn't registered. Server says:",
this.limitsMessage,
//error,
);
} else if (
error instanceof Error &&
error.message ===

147
src/views/ContactGiftingView.vue

@ -16,10 +16,6 @@
</h1>
</div>
<!-- Quick Search -->
<!-- Initial Loading Animation -->
<!-- Results List -->
<ul class="border-t border-slate-300">
<li class="border-b border-slate-300 py-3">
@ -70,12 +66,7 @@
</li>
</ul>
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
</section>
</template>
@ -83,16 +74,10 @@
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { Account, AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
createAndSubmitGive,
CreateAndSubmitGiveResult,
ErrorResult,
GiverInputInfo,
GiverOutputInfo,
} from "@/libs/endorserServer";
import { GiverInputInfo } from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
@ -124,10 +109,10 @@ export default class ContactGiftingView extends Vue {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
@ -150,7 +135,7 @@ export default class ContactGiftingView extends Vue {
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
@ -173,123 +158,5 @@ export default class ContactGiftingView extends Vue {
openDialog(giver: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver);
}
handleDialogResult(result: GiverOutputInfo) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(
result.giver?.did,
result.description,
result.hours,
).then(() => {
resolve(null);
});
});
} else {
// action was "cancel" so do nothing
}
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
public async recordGive(
giverDid?: string,
description?: string,
hours?: number,
) {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identity before you can record a give.",
},
-1,
);
return;
}
if (!description && !hours) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must enter a description or some number of hours.",
},
-1,
);
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
);
if (this.isGiveCreationError(result)) {
const errorMessage = this.getGiveCreationErrorMessage(result);
console.log("Error with give result:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: errorMessage || "There was an error recording the give.",
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "That gift was recorded.",
},
-1,
);
}
// 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: message,
},
-1,
);
}
}
// Helper functions for readability
isGiveCreationError(result: CreateAndSubmitGiveResult) {
return result.type == "error";
}
getGiveCreationErrorMessage(result: CreateAndSubmitGiveResult) {
return (result as ErrorResult).error?.userMessage;
}
}
</script>

4
src/views/ContactQRScanShowView.vue

@ -108,7 +108,9 @@ export default class ContactQRScanShow extends Vue {
iat: Date.now(),
iss: this.activeDid,
own: {
name: (settings?.firstName || "") + " " + (settings?.lastName || ""),
name:
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
publicEncKey,
},
};

108
src/views/ContactsView.vue

@ -256,7 +256,7 @@ import { NotificationIface } from "@/constants/app";
import { IIdentifier } from "@veramo/core";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import {
accessToken,
getContactPayloadFromJwtUrl,
@ -271,6 +271,7 @@ import {
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
@ -308,7 +309,7 @@ export default class ContactsView extends Vue {
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
@ -332,7 +333,7 @@ export default class ContactsView extends Vue {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
@ -394,7 +395,7 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Error With Server",
title: "Server Error",
text:
"Got an error retrieving your " +
(useRecipient ? "given" : "received") +
@ -453,7 +454,7 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Error With Server",
title: "Server Error",
text: error as string,
},
-1,
@ -589,6 +590,16 @@ export default class ContactsView extends Vue {
"?",
)
) {
this.$notify(
{
group: "alert",
type: "toast",
text: "",
title: "Registration submitted...",
},
1000,
);
const identity = await this.getIdentity(this.activeDid);
const vcClaim: RegisterVerifiableCredential = {
@ -671,7 +682,7 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Error With Server",
title: "Server Error",
text: userMessage,
},
-1,
@ -696,36 +707,30 @@ export default class ContactsView extends Vue {
contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility });
} else {
console.error("Bad response setting visibility: ", resp.data);
if (resp.data.error?.message) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: resp.data.error?.message,
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: "Bad server response of " + resp.status,
},
-1,
);
}
console.error(
"Got some bad server response when setting visibility: ",
resp,
);
const message =
resp.data.error?.message || "Bad server response of " + resp.status;
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Error",
text: message,
},
-1,
);
}
} catch (err) {
console.error("Got some server error when setting visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: err as string,
title: "Server Error",
text: "Check connectivity and try again.",
},
-1,
);
@ -750,7 +755,7 @@ export default class ContactsView extends Vue {
this.$notify(
{
group: "alert",
type: "toast",
type: "info",
title: "Refreshed",
text:
this.nameForContact(contact, true) +
@ -758,38 +763,29 @@ export default class ContactsView extends Vue {
(visibility ? "" : "not ") +
"see your activity.",
},
5000,
-1,
);
} else {
if (resp.data.error?.message) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: resp.data.error?.message,
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: "Bad server response of " + resp.status,
},
-1,
);
}
console.log("Got bad server response when checking visibility: ", resp);
const message = resp.data.error?.message || "Got bad server response.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Error",
text: message,
},
-1,
);
}
} catch (err) {
console.log("Caught error from server request to check visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: err as string,
title: "Server Error",
text: "Check connectivity and try again.",
},
-1,
);
@ -989,7 +985,7 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Error With Server",
title: "Server Error",
text: userMessage,
},
-1,

271
src/views/HomeView.vue

@ -6,64 +6,82 @@
Time Safari
</h1>
<!-- show the actions for recognizing a give -->
<div class="mb-8">
<h2 class="text-xl font-bold">Record a Gift</h2>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openDialog()">
<EntityIcon
:entityId="null"
:iconSize="64"
class="mx-auto border border-slate-300 rounded-md mb-1"
></EntityIcon>
<h3
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
Anonymous
</h3>
</li>
<li
v-for="contact in allContacts"
:key="contact.did"
@click="openDialog(contact)"
<div v-if="!activeDid">
To record others' giving,
<router-link :to="{ name: 'start' }" class="text-blue-500">
create your identifier.</router-link
>
</div>
<div v-else-if="!isRegistered">
To record others' giving, someone must register your account, so show
them
<router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
your identity info</router-link
>
and then
<router-link :to="{ name: 'account' }" class="text-blue-500">
check your limits.</router-link
>
<EntityIcon
:entityId="contact.did"
:iconSize="64"
class="mx-auto border border-slate-300 rounded-md mb-1"
></EntityIcon>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
</div>
<div v-else>
<!-- activeDid && isRegistered -->
<h2 class="text-xl font-bold">Record a Gift</h2>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openDialog()">
<EntityIcon
:entityId="null"
:iconSize="64"
class="mx-auto border border-slate-300 rounded-md mb-1"
></EntityIcon>
<h3
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
Anonymous/Unnamed
</h3>
</li>
<li
v-for="contact in allContacts"
:key="contact.did"
@click="openDialog(contact)"
>
{{ contact.name || contact.did }}
</h3>
</li>
</ul>
<EntityIcon
:entityId="contact.did"
:iconSize="64"
class="mx-auto border border-slate-300 rounded-md mb-1"
></EntityIcon>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
{{ contact.name || contact.did }}
</h3>
</li>
</ul>
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
<router-link
v-if="allContacts.length >= 7"
:to="{ name: 'contact-gives' }"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
>
Show More Contacts&hellip;
</router-link>
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
<router-link
v-if="allContacts.length > 7"
:to="{ name: 'contact-gives' }"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
>
Show More Contacts&hellip;
</router-link>
<!-- If there are no contacts, show this instead: -->
<div
class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
v-if="allContacts.length === 0"
>
(No contacts to show.)
<!-- If there are no contacts, show this instead: -->
<div
class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
v-if="allContacts.length === 0"
>
(No contacts to show.)
</div>
</div>
</div>
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
@ -99,19 +117,18 @@
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
createAndSubmitGive,
didInfo,
GiverInputInfo,
GiverOutputInfo,
GiveServerRecord,
} from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { IIdentifier } from "@veramo/core";
import { Account } from "@/db/tables/accounts";
interface Notification {
group: string;
@ -135,6 +152,7 @@ export default class HomeView extends Vue {
feedPreviousOldestId?: string;
feedLastViewedId?: string;
isHiddenSpinner = true;
isRegistered = false;
numAccounts = 0;
async beforeCreate() {
@ -144,10 +162,10 @@ export default class HomeView extends Vue {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
@ -174,11 +192,12 @@ export default class HomeView extends Vue {
this.allMyDids = allAccounts.map((acc) => acc.did);
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId;
this.isRegistered = !!settings?.isRegistered;
this.updateAllFeed();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
@ -204,7 +223,9 @@ export default class HomeView extends Vue {
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const account = allAccounts.find(
(acc) => acc.did === this.activeDid,
) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
@ -333,139 +354,5 @@ export default class HomeView extends Vue {
openDialog(giver: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver);
}
handleDialogResult(result: GiverOutputInfo) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(
result.giver?.did,
result.description,
result.hours,
).then(() => {
resolve(null);
});
});
} else {
// action was "cancel" so do nothing
}
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
public async recordGive(
giverDid?: string,
description?: string,
hours?: number,
) {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identity before you can record a give.",
},
-1,
);
return;
}
if (!description && !hours) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must enter a description or some number of hours.",
},
-1,
);
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
);
if (
result.type === "error" ||
this.isGiveCreationError(result.response)
) {
const errorMessage = this.getGiveCreationErrorMessage(result);
console.log("Error with give creation result:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: errorMessage || "There was an error creating the give.",
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "That gift was recorded.",
},
-1,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log("Error with give recordation 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: message,
},
-1,
);
}
}
// Helper functions for readability
/**
* @param result response "data" from the server
* @returns true if the result indicates an error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isGiveCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
/**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getGiveCreationErrorMessage(result: any) {
return (
result.error?.userMessage ||
result.error?.error ||
result.response?.data?.error?.message
);
}
}
</script>

18
src/views/IdentitySwitcherView.vue

@ -22,7 +22,7 @@
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
<span class="overflow-hidden">
<h2 class="text-xl font-semibold mb-0">
{{ firstName }} {{ lastName }}
{{ givenName }}
</h2>
<div class="text-sm text-slate-500 truncate">
<b>ID:</b> <code>{{ activeDid }}</code>
@ -71,7 +71,7 @@ import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue";
interface Notification {
@ -90,8 +90,7 @@ export default class IdentitySwitcherView extends Vue {
public activeDid = "";
public apiServer = "";
public apiServerInput = "";
public firstName = "";
public lastName = "";
public givenName = "";
public otherIdentities: Array<{ did: string }> = [];
public showContactGives = false;
@ -101,19 +100,20 @@ export default class IdentitySwitcherView extends Vue {
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
const identity = JSON.parse((account?.identity as string) || "null");
return identity;
}
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || "";
this.firstName = settings?.firstName || "No";
this.lastName = settings?.lastName || "Name";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
this.showContactGives = !!settings?.showContactGivesInline;
const identity = await this.getIdentity(this.activeDid);
@ -151,7 +151,7 @@ export default class IdentitySwitcherView extends Vue {
did = undefined;
}
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did,
});
this.activeDid = did || "";

38
src/views/NewEditAccountView.vue

@ -10,21 +10,15 @@
>
<fa icon="chevron-left" class="fa-fw"></fa>
</button>
[New/Edit] Identity
Edit Identity
</h1>
</div>
<input
type="text"
placeholder="First Name"
placeholder="Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="firstName"
/>
<input
type="text"
placeholder="Last Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="lastName"
v-model="givenName"
/>
<div class="mt-8">
@ -50,36 +44,30 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@Component({
components: {},
})
export default class NewEditAccountView extends Vue {
firstName =
localStorage.getItem("firstName") === null
? "--"
: localStorage.getItem("firstName");
lastName =
localStorage.getItem("lastName") === null
? "--"
: localStorage.getItem("lastName");
givenName = "";
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.firstName = settings?.firstName || "";
this.lastName = settings?.lastName || "";
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
}
onClickSaveChanges() {
db.settings.update(MASTER_SETTINGS_KEY, {
firstName: this.firstName,
lastName: this.lastName,
firstName: this.givenName,
lastName: "", // deprecated, pre v 0.1.3
});
localStorage.setItem("firstName", this.firstName as string);
localStorage.setItem("lastName", this.lastName as string);
localStorage.setItem("firstName", this.givenName as string);
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
this.$router.push({ name: "account" });
}

117
src/views/ProjectViewView.vue

@ -102,7 +102,7 @@
<h3
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
Anonymous
Anonymous/Unnamed
</h3>
</li>
<li
@ -125,7 +125,7 @@
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
<router-link
v-if="allContacts.length > 7"
v-if="allContacts.length >= 7"
:to="{ name: 'contact-gives' }"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
>
@ -196,8 +196,8 @@
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
:projectId="this.projectId"
>
</GiftedDialog>
</section>
@ -212,18 +212,16 @@ import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
createAndSubmitGive,
didInfo,
GiverInputInfo,
GiverOutputInfo,
GiveServerRecord,
ResultWithType,
} from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts";
interface Notification {
group: string;
@ -257,7 +255,7 @@ export default class ProjectViewView extends Vue {
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.allContacts = await db.contacts.toArray();
@ -266,17 +264,17 @@ export default class ProjectViewView extends Vue {
const accounts = accountsDB.accounts;
const accountsArr = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr?.find((acc) => acc.did === this.activeDid);
const account = accountsArr.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
this.LoadProject(identity);
}
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
@ -461,11 +459,6 @@ export default class ProjectViewView extends Vue {
}
}
openDialog(contact: GiverInputInfo) {
const dialog: GiftedDialog = this.$refs.customDialog as GiftedDialog;
dialog.open(contact);
}
getOpenStreetMapUrl() {
// Google URL is https://maps.google.com/?q=LAT,LONG
return (
@ -480,96 +473,8 @@ export default class ProjectViewView extends Vue {
);
}
handleDialogResult(result: GiverOutputInfo) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(
result.giver?.did,
result.description,
result.hours,
).then(() => {
resolve(null);
});
});
} else {
// action was not "confirm" so do nothing
}
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
async recordGive(giverDid?: string, description?: string, hours?: number) {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identity before you can record a give.",
},
-1,
);
return;
}
if (!description && !hours) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must enter a description or some number of hours.",
},
-1,
);
} else {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
this.projectId,
);
if (result.type == "success") {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "That gift was recorded.",
},
-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,
);
}
}
openDialog(contact: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(contact);
}
}
</script>

2
src/views/ProjectsView.vue

@ -98,8 +98,6 @@ export default class ProjectsView extends Vue {
projects: ProjectData[] = [];
current: IIdentifier;
isLoading = false;
alertTitle = "";
alertMessage = "";
numAccounts = 0;
async beforeCreate() {

15
web-push.md

@ -1,3 +1,6 @@
# Overivew of Web Push
Web Push notifications is a web browser messaging protocol defined by the W3C.
Discussions of this interesting technology are clouded because of a
@ -360,29 +363,29 @@ unsubscribeFromPush().catch((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.
## NOTIFICATION DIALOG WORKFLOW
# NOTIFICATION DIALOG WORKFLOW
# ON APP FIRST-LAUNCH:
## ON APP FIRST-LAUNCH:
The user is periodically presented with the notification permission dialog that asks them if they want to turn on notifications. User is given 3 choices:
- "Turn on Notifications": triggers the browser's own notification permission prompt.
- "Maybe Later": dismisses the dialog, to reappear at a later instance. (The next time the user launches the app? After X amount of days? A combination of both?)
- "Never": dismisses the dialog; app remembers to not automatically present the dialog again.
# IF THE USER CHOOSES "NEVER":
## IF THE USER CHOOSES "NEVER":
The dialog can still be accessed via the Notifications toggle switch in `AccountViewView` (which also tells the user if notifications are turned on or off).
# TO TEMPORARILY MUTE NOTIFICATIONS:
## TO TEMPORARILY MUTE NOTIFICATIONS:
While notifications are turned on, the user can tap on the Mute Notifications toggle switch in `AccountViewView` (visible only when notifications are turned on) to trigger the Mute Notifications Dialog. User is given the following choices:
- Several "Mute for X Hour/s" buttons to temporarily mute notifications.
- "Mute until I turn it back on" button to indefinitely mute notifications.
- "Cancel" to make no changes and dismiss the dialog.
# TO UNMUTE NOTIFICATIONS:
## TO UNMUTE NOTIFICATIONS:
Simply tap on the Mute Notifications toggle switch in `AccountViewView` to immediately unmute notifications. No dialog needed.
# TO TURN OFF NOTIFICATIONS:
## TO TURN OFF NOTIFICATIONS:
While notifications are turned on, the user can tap on the App Notifications toggle switch in `AccountViewView` to trigger the Turn Off Notifications Dialog. User is given the following choices:
- "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on).

Loading…
Cancel
Save