Browse Source

Merging changes

kb/add-usage-guide
Matthew Aaron Raymer 1 year ago
parent
commit
7f0f1b7fc8
  1. 14
      CHANGELOG.md
  2. 18
      README.md
  3. 44
      project.task.yaml
  4. 215
      src/components/GiftedDialog.vue
  5. 12
      src/constants/app.ts
  6. 2
      src/db/tables/contacts.ts
  7. 2
      src/db/tables/settings.ts
  8. 27
      src/libs/crypto/index.ts
  9. 13
      src/libs/endorserServer.ts
  10. 148
      src/libs/veramo/setup.ts
  11. 2
      src/main.ts
  12. 50
      src/router/index.ts
  13. 2
      src/test/index.ts
  14. 268
      src/views/AccountViewView.vue
  15. 147
      src/views/ContactGiftingView.vue
  16. 56
      src/views/ContactQRScanShowView.vue
  17. 293
      src/views/ContactsView.vue
  18. 15
      src/views/HelpView.vue
  19. 404
      src/views/HomeView.vue
  20. 20
      src/views/IdentitySwitcherView.vue
  21. 2
      src/views/ImportAccountView.vue
  22. 38
      src/views/NewEditAccountView.vue
  23. 4
      src/views/NewEditProjectView.vue
  24. 2
      src/views/NewIdentifierView.vue
  25. 117
      src/views/ProjectViewView.vue
  26. 2
      src/views/ProjectsView.vue
  27. 3
      src/views/StartView.vue
  28. 151
      src/views/TestView.vue

14
CHANGELOG.md

@ -0,0 +1,14 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.2] - 2023.11.01
### Added
- Basics: create ID, record a give, declare a project, search, and get notifications.

18
README.md

@ -1,6 +1,9 @@
# kickstart-for-time-pwa # kickstart-for-time-pwa
## Project setup ## Project setup
We have pkgx.dev set up in package.json, so you can use `dev` to set up the dev environment.
``` ```
npm install npm install
``` ```
@ -23,6 +26,12 @@ npm run build
npm run lint npm run lint
``` ```
## Tests
###
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
### Test key contents ### Test key contents
See [this page](openssl_signing_console.rst) See [this page](openssl_signing_console.rst)
@ -86,15 +95,6 @@ Clear cache for localhost, then go to http://localhost:8080/start
## Dependencies
See https://tea.xyz
| Project | Version |
| ---------- | --------- |
| nodejs.org | ^16.0.0 |
| npmjs.com | ^8.0.0 |
## Other ## Other
### Reference Material ### Reference Material

44
project.task.yaml

@ -1,23 +1,18 @@
tasks: 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 - 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
- 40 notifications : - 40 notifications :
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew - push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew - 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 - 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 - 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
- SEE: https://github.com/emmanuelsw/notiwind assignee:jose assignee-group:ui
- Home Feed & Quick Give screen : - Home Feed & Quick Give screen :
- 01 save the feed-viewed status in settings storage ("afterQuery") - 01 save the feed-viewed status in settings storage ("afterQuery")
@ -26,29 +21,34 @@ tasks:
- 24 Move to Vite assignee:matthew - 24 Move to Vite assignee:matthew
- .2 Edit Plan does not have icons across the bottom assignee-group:ui - .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)
- .5 include the hash of the latest commit, and maybe a version - .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 add link to further project / people when a project pays ahead - .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 remove edit from project page for projects owned by others
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page - .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 on ProjectViewView, show different messages for "to" and "from" sections if none exist
- .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 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 - .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" - .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
- .1 Make give description text box into something that expands as they type
- .1 Make contact info specific to Time Safari - rather pointing at CommunityCred.org
- Discuss whether the remaining tasks are worthwhile before MVP release. - 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 - 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) - 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 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?)
- .2 Show a warning if both giver and recipient are the same (but still allow?) assignee-group:ui
- 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 - 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, hours, etc)
- .1 remove firstName (& lastName) from localStorage
- contacts v+ : - contacts v+ :
- 01 Import all the non-sensitive data (ie. contacts & settings). - 01 Import all the non-sensitive data (ie. contacts & settings).
@ -57,6 +57,7 @@ tasks:
- stats v1 : - stats v1 :
- 01 show numeric stats - 01 show numeric stats
- 04 show different graphic for projects vs people on world
- 01 link to world for specific stats - 01 link to world for specific stats
- .5 don't load another instance of a bush if it already exists - .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") - maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
@ -72,6 +73,11 @@ tasks:
- Test PWA features on Android and iOS. - Test PWA features on Android and iOS.
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- .5 show seed phrase in a QR code for transfer to another device
- 32 accept images for projects
- 32 accept images for contacts
- linking between projects or plans : - linking between projects or plans :
- show total time given to & from a project - show total time given to & from a project
- terminology: - terminology:

215
src/components/GiftedDialog.vue

@ -2,7 +2,7 @@
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4"> <h1 class="text-xl font-bold text-center mb-4">
{{ message }} {{ giver?.name || "somebody not specified" }} {{ message }} {{ giver?.name || "somebody not named" }}
</h1> </h1>
<input <input
type="text" type="text"
@ -51,18 +51,57 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop, Emit } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { GiverInputInfo, GiverOutputInfo } from "@/libs/endorserServer"; 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 @Component
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@Prop message = ""; @Prop message = "";
@Prop projectId = "";
activeDid = "";
apiServer = "";
giver?: GiverInputInfo; giver?: GiverInputInfo;
description = ""; description = "";
hours = "0"; hours = "0";
visible = false; 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) { open(giver: GiverInputInfo) {
this.giver = giver; this.giver = giver;
this.visible = true; this.visible = true;
@ -80,27 +119,169 @@ export default class GiftedDialog extends Vue {
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`; this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
} }
@Emit("dialog-result") cancel() {
confirm(): GiverOutputInfo {
const result = {
action: "confirm",
giver: this.giver,
hours: parseFloat(this.hours),
description: this.description,
};
this.close(); this.close();
this.description = ""; this.description = "";
this.giver = undefined; this.giver = undefined;
this.hours = "0"; this.hours = "0";
return result;
} }
@Emit("dialog-result") async confirm() {
cancel(): GiverOutputInfo {
const result = { action: "cancel" };
this.close(); 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> </script>

12
src/constants/app.ts

@ -1,5 +1,7 @@
/** /**
* Generic strings that could be used throughout the app. * Generic strings that could be used throughout the app.
*
* See also ../libs/veramo/setup.ts
*/ */
export enum AppString { export enum AppString {
APP_NAME = "TimeSafari", APP_NAME = "TimeSafari",
@ -10,3 +12,13 @@ export enum AppString {
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER, DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
} }
/**
* See notiwind package
*/
export interface NotificationIface {
group: string;
type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string;
text: string;
}

2
src/db/tables/contacts.ts

@ -7,5 +7,5 @@ export interface Contact {
} }
export const ContactsSchema = { export const ContactsSchema = {
contacts: "++did, name, publicKeyBase64, registered, seesMe", contacts: "&did, name, publicKeyBase64, registered, seesMe",
}; };

2
src/db/tables/settings.ts

@ -13,7 +13,6 @@ export type BoundingBox = {
*/ */
export type Settings = { export type Settings = {
id: number; // Only one entry using MASTER_SETTINGS_KEY id: number; // Only one entry using MASTER_SETTINGS_KEY
activeDid?: string; // Active Decentralized ID activeDid?: string; // Active Decentralized ID
apiServer?: string; // API server URL apiServer?: string; // API server URL
firstName?: string; // User's first name firstName?: string; // User's first name
@ -22,6 +21,7 @@ export type Settings = {
lastNotifiedClaimId?: string; // Last notified claim ID lastNotifiedClaimId?: string; // Last notified claim ID
// Array of named search boxes defined by bounding boxes // Array of named search boxes defined by bounding boxes
searchBoxes?: Array<{ searchBoxes?: Array<{
name: string; name: string;
bbox: BoundingBox; bbox: BoundingBox;

27
src/libs/crypto/index.ts

@ -1,5 +1,4 @@
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
import { getRandomBytesSync } from "ethereum-cryptography/random"; import { getRandomBytesSync } from "ethereum-cryptography/random";
import { entropyToMnemonic } from "ethereum-cryptography/bip39"; import { entropyToMnemonic } from "ethereum-cryptography/bip39";
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english"; import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
@ -7,7 +6,10 @@ import { HDNode } from "@ethersproject/hdnode";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as u8a from "uint8arrays"; import * as u8a from "uint8arrays";
export const DEFAULT_ROOT_DERIVATION_PATH = "m/76798669'/0'/0'/0'"; import { ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
/** /**
* *
@ -150,3 +152,24 @@ export function fromJose(signature: string): {
export function bytesToHex(b: Uint8Array): string { export function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, "base16"); return u8a.toString(b, "base16");
} }
/**
@return results of uportJwtPayload:
{ iat: number, iss: string (DID), own: { name, publicEncKey (base64-encoded key) } }
Note that similar code is also contained in time-safari
*/
export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
let jwtText = jwtUrlText;
const endorserContextLoc = jwtText.indexOf(ENDORSER_JWT_URL_LOCATION);
if (endorserContextLoc > -1) {
jwtText = jwtText.substring(
endorserContextLoc + ENDORSER_JWT_URL_LOCATION.length,
);
}
// JWT format: { header, payload, signature, data }
const jwt = didJwt.decodeJWT(jwtText);
return jwt.payload;
};

13
src/libs/endorserServer.ts

@ -6,7 +6,12 @@ import { Axios, AxiosResponse } from "axios";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims
export const SERVICE_ID = "endorser.ch"; export const SERVICE_ID = "endorser.ch";
// the prefix for the contact URL
export const CONTACT_URL_PREFIX = "https://endorser.ch";
// the suffix for the contact URL
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt=";
export interface AgreeVerifiableCredential { export interface AgreeVerifiableCredential {
"@context": string; "@context": string;
@ -111,6 +116,8 @@ export function isHiddenDid(did: string) {
/** /**
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
Similar logic is found in endorser-mobile.
**/ **/
export function didInfo( export function didInfo(
did: string, did: string,
@ -118,14 +125,14 @@ export function didInfo(
allMyDids: string[], allMyDids: string[],
contacts: Contact[], contacts: Contact[],
): string { ): string {
if (!did) return "Someone Anonymous";
const myId = R.find(R.equals(did), allMyDids); const myId = R.find(R.equals(did), allMyDids);
if (myId) return `You${myId !== activeDid ? " (Alt ID)" : ""}`; if (myId) return `You${myId !== activeDid ? " (Alt ID)" : ""}`;
const contact = R.find((c) => c.did === did, contacts); const contact = R.find((c) => c.did === did, contacts);
return contact return contact
? contact.name || "Someone Unnamed in Contacts" ? contact.name || "Contact With No Name"
: !did
? "Unspecified Person"
: isHiddenDid(did) : isHiddenDid(did)
? "Someone Not In Network" ? "Someone Not In Network"
: "Someone Not In Contacts"; : "Someone Not In Contacts";

148
src/libs/veramo/setup.ts

@ -1,151 +1,7 @@
// Created from the setup in https://veramo.io/docs/guides/react_native // see also ../constants/app.ts and
// Core interfaces
/* import {
createAgent,
IDIDManager,
IResolver,
IDataStore,
IKeyManager,
} from "@veramo/core";
*/
// Core identity manager plugin
//import { DIDManager } from "@veramo/did-manager";
// Ethr did identity provider
//import { EthrDIDProvider } from "@veramo/did-provider-ethr";
// Core key manager plugin
//import { KeyManager } from "@veramo/key-manager";
// Custom key management system for RN
//import { KeyManagementSystem } from '@veramo/kms-local-react-native'
// Custom resolver
// Custom resolvers
//import { DIDResolverPlugin } from "@veramo/did-resolver";
/* import { Resolver } from "did-resolver";
import { getResolver as ethrDidResolver } from "ethr-did-resolver";
import { getResolver as webDidResolver } from "web-did-resolver";
*/
// for VCs and VPs https://veramo.io/docs/api/credential-w3c
//import { CredentialIssuer } from '@veramo/credential-w3c'
// Storage plugin using TypeOrm
/* import {
Entities,
KeyStore,
DIDStore,
IDataStoreORM,
} from "@veramo/data-store";
*/
// TypeORM is installed with @veramo/typeorm
//import { createConnection } from 'typeorm'
//import * as R from "ramda";
/*
import { Contact } from '../entity/contact'
import { Settings } from '../entity/settings'
import { PrivateData } from '../entity/privateData'
import { Initial1616938713828 } from '../migration/1616938713828-initial'
import { SettingsContacts1616967972293 } from '../migration/1616967972293-settings-contacts'
import { EncryptedSeed1637856484788 } from '../migration/1637856484788-EncryptedSeed'
import { HomeScreenConfig1639947962124 } from '../migration/1639947962124-HomeScreenConfig'
import { HandlePublicKeys1652142819353 } from '../migration/1652142819353-HandlePublicKeys'
import { LastClaimsSeen1656811846836 } from '../migration/1656811846836-LastClaimsSeen'
import { ContactRegistered1662256903367 }from '../migration/1662256903367-ContactRegistered'
import { PrivateData1663080623479 } from '../migration/1663080623479-PrivateData'
const ALL_ENTITIES = Entities.concat([Contact, Settings, PrivateData])
// Create react native DB connection configured by ormconfig.js
export const dbConnection = createConnection({
database: 'endorser-mobile.sqlite',
entities: ALL_ENTITIES,
location: 'default',
logging: ['error', 'info', 'warn'],
migrations: [ Initial1616938713828, SettingsContacts1616967972293, EncryptedSeed1637856484788, HomeScreenConfig1639947962124, HandlePublicKeys1652142819353, LastClaimsSeen1656811846836, ContactRegistered1662256903367, PrivateData1663080623479 ],
migrationsRun: true,
type: 'react-native',
})
*/
function didProviderName(netName: string) { function didProviderName(netName: string) {
return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName); return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName);
} }
//const NETWORK_NAMES = ["mainnet", "rinkeby"]; export const DEFAULT_DID_PROVIDER_NAME = didProviderName("mainnet");
const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet";
export const DEFAULT_DID_PROVIDER_NAME = didProviderName(
DEFAULT_DID_PROVIDER_NETWORK_NAME,
);
export const HANDY_APP = false;
// this is used as the object in RegisterAction claims
export const SERVICE_ID = "endorser.ch";
//const INFURA_PROJECT_ID = "INFURA_PROJECT_ID";
/*
const providers = {}
NETWORK_NAMES.forEach((networkName) => {
providers[didProviderName(networkName)] = new EthrDIDProvider({
defaultKms: 'local',
network: networkName,
rpcUrl: 'https://' + networkName + '.infura.io/v3/' + INFURA_PROJECT_ID,
gas: 1000001,
ttl: 60 * 60 * 24 * 30 * 12 + 1,
})
})
const didManager = new DIDManager({
store: new DIDStore(dbConnection),
defaultProvider: DEFAULT_DID_PROVIDER_NAME,
providers: providers,
})
*/
/* const basicDidResolvers = NETWORK_NAMES.map((networkName) => [
networkName,
new Resolver({
ethr: ethrDidResolver({
networks: [
{
name: networkName,
rpcUrl:
"https://" + networkName + ".infura.io/v3/" + INFURA_PROJECT_ID,
},
],
}).ethr,
web: webDidResolver().web,
}),
]);
const basicResolverMap = R.fromPairs(basicDidResolvers)
export const DEFAULT_BASIC_RESOLVER = basicResolverMap[DEFAULT_DID_PROVIDER_NETWORK_NAME]
const agentDidResolvers = NETWORK_NAMES.map((networkName) => {
return new DIDResolverPlugin({
resolver: basicResolverMap[networkName],
})
})
let allPlugins = [
new CredentialIssuer(),
new KeyManager({
store: new KeyStore(dbConnection),
kms: {
local: new KeyManagementSystem(),
},
}),
didManager,
].concat(agentDidResolvers)
*/
//export const agent = createAgent<IDIDManager & IKeyManager & IDataStore & IDataStoreORM & IResolver>({ plugins: allPlugins })

2
src/main.ts

@ -13,6 +13,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { import {
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faBan,
faBurst, faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
@ -59,6 +60,7 @@ import {
library.add( library.add(
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faBan,
faBurst, faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,

50
src/router/index.ts

@ -33,7 +33,6 @@ const routes: Array<RouteRecordRaw> = [
name: "home", name: "home",
component: () => component: () =>
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"), import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
beforeEnter: enterOrStart,
}, },
{ {
path: "/account", path: "/account",
@ -58,6 +57,14 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue" /* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue"
), ),
}, },
{
path: "/contact-gives",
name: "contact-gives",
component: () =>
import(
/* webpackChunkName: "contact-gives" */ "../views/ContactGiftingView.vue"
),
},
{ {
path: "/contact-qr", path: "/contact-qr",
name: "contact-qr", name: "contact-qr",
@ -71,15 +78,6 @@ const routes: Array<RouteRecordRaw> = [
name: "contacts", name: "contacts",
component: () => component: () =>
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"), import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/scan-contact",
name: "scan-contact",
component: () =>
import(
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
),
}, },
{ {
path: "/discover", path: "/discover",
@ -93,6 +91,14 @@ const routes: Array<RouteRecordRaw> = [
component: () => component: () =>
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"), import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
}, },
{
path: "/identity-switcher",
name: "identity-switcher",
component: () =>
import(
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
),
},
{ {
path: "/import-account", path: "/import-account",
name: "import-account", name: "import-account",
@ -141,14 +147,6 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue" /* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
), ),
}, },
{
path: "/identity-switcher",
name: "identity-switcher",
component: () =>
import(
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
),
},
{ {
path: "/project", path: "/project",
name: "project", name: "project",
@ -162,6 +160,14 @@ const routes: Array<RouteRecordRaw> = [
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"), import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
beforeEnter: enterOrStart, beforeEnter: enterOrStart,
}, },
{
path: "/scan-contact",
name: "scan-contact",
component: () =>
import(
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
),
},
{ {
path: "/seed-backup", path: "/seed-backup",
name: "seed-backup", name: "seed-backup",
@ -185,12 +191,10 @@ const routes: Array<RouteRecordRaw> = [
), ),
}, },
{ {
path: "/contact-gives", path: "/test",
name: "contact-gives", name: "test",
component: () => component: () =>
import( import(/* webpackChunkName: "test" */ "../views/TestView.vue"),
/* webpackChunkName: "contact-gives" */ "../views/ContactGiftingView.vue"
),
}, },
]; ];

2
src/test/index.ts

@ -2,7 +2,7 @@ import axios from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db } from "../db"; import { db } from "../db";
import { SERVICE_ID } from "../libs/veramo/setup"; import { SERVICE_ID } from "../libs/endorserServer";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import { deriveAddress, newIdentifier } from "../libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";

268
src/views/AccountViewView.vue

@ -52,7 +52,17 @@
<!-- Identity Details --> <!-- Identity Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<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-slate-500 text-sm font-bold">ID</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1"> <div class="text-sm text-slate-500 flex justify-start items-center mb-1">
@ -67,53 +77,11 @@
</button> </button>
<span v-show="showDidCopy">Copied!</span> <span v-show="showDidCopy">Copied!</span>
</div> </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> </div>
<router-link <router-link
:to="{ name: 'new-edit-account' }" :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 Edit Identity
</router-link> </router-link>
@ -132,8 +100,10 @@
) )
" "
> >
<!-- label -->
<div>App Notifications</div>
<!-- toggle --> <!-- toggle -->
<div class="relative"> <div class="relative ml-2">
<!-- input --> <!-- input -->
<input type="checkbox" name="toggleNotifications" class="sr-only" /> <input type="checkbox" name="toggleNotifications" class="sr-only" />
<!-- line --> <!-- line -->
@ -143,8 +113,6 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition" class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div> ></div>
</div> </div>
<!-- label -->
<div class="ml-2">App Notifications</div>
</label> </label>
<label <label
for="toggleMuteNotifications" for="toggleMuteNotifications"
@ -159,8 +127,10 @@
) )
" "
> >
<!-- label -->
<div>Mute Notifications</div>
<!-- toggle --> <!-- toggle -->
<div class="relative"> <div class="relative ml-2">
<!-- input --> <!-- input -->
<input <input
type="checkbox" type="checkbox"
@ -174,8 +144,6 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition" class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div> ></div>
</div> </div>
<!-- label -->
<div class="ml-2">Mute Notifications</div>
</label> </label>
</div> </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" 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()" @click="exportDatabase()"
> >
Download Settings & Contacts (excluding Identifier Data) Download Settings & Contacts
<br />
(excluding Identifier Data)
</a> </a>
<a ref="downloadLink" /> <a ref="downloadLink" />
<!-- QR code popup --> <div v-if="activeDid" class="flex py-2">
<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">
<button class="text-center text-md text-blue-500" @click="checkLimits()"> <button class="text-center text-md text-blue-500" @click="checkLimits()">
Check Limits Check Limits
</button> </button>
@ -244,20 +193,84 @@
</div> </div>
</div> </div>
<!-- id used by puppeteer test script -->
<h3 <h3
id="advanced"
class="text-sm uppercase font-semibold mb-3" class="text-sm uppercase font-semibold mb-3"
@click="showAdvanced = !showAdvanced" @click="showAdvanced = !showAdvanced"
> >
Advanced Advanced
</h3> </h3>
<div v-if="showAdvanced"> <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 <label
for="toggleShowAmounts" for="toggleShowAmounts"
class="flex items-center cursor-pointer mb-6" class="flex items-center cursor-pointer py-2"
@click="handleChange" @click="handleChange"
> >
<!-- label -->
<h2 class="text-slate-500 text-sm font-bold mb-2">
Show amounts given with contacts
</h2>
<!-- toggle --> <!-- toggle -->
<div class="relative"> <div class="relative ml-2">
<!-- input --> <!-- input -->
<input <input
type="checkbox" type="checkbox"
@ -272,21 +285,31 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition" class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div> ></div>
</div> </div>
<!-- label -->
<div class="ml-2">Show amounts given with contacts</div>
</label> </label>
<div class="flex py-2"> <div class="flex py-2">
<router-link <button class="text-blue-500">
:to="{ name: 'identity-switcher' }" <!-- id used by puppeteer test script -->
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" <router-link
> id="switch-identity-link"
Switch Identity / No Identity :to="{ name: 'identity-switcher' }"
</router-link> class="block text-center"
>
Switch Identity / No Identity
</router-link>
</button>
</div> </div>
<div class="flex py-2"> <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 <input
type="text" type="text"
class="block w-full rounded border border-slate-400 px-3 py-2" class="block w-full rounded border border-slate-400 px-3 py-2"
@ -318,17 +341,6 @@
Use Local Use Local
</button> </button>
</div> </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> </div>
</section> </section>
</template> </template>
@ -339,11 +351,11 @@ import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; 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 { accessToken } from "@/libs/crypto";
import QuickNav from "@/components/QuickNav.vue";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { ErrorResponse, RateLimits } from "@/libs/endorserServer"; import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
@ -364,14 +376,6 @@ interface IAccount {
derivationPath: string; derivationPath: string;
} }
interface SettingsType {
activeDid?: string;
apiServer?: string;
firstName?: string;
lastName?: string;
showContactGivesInline?: boolean;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@ -382,8 +386,8 @@ export default class AccountViewView extends Vue {
apiServer = ""; apiServer = "";
apiServerInput = ""; apiServerInput = "";
derivationPath = ""; derivationPath = "";
firstName = ""; givenName = "";
lastName = ""; isRegistered = false;
numAccounts = 0; numAccounts = 0;
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
@ -398,8 +402,6 @@ export default class AccountViewView extends Vue {
showPubCopy = false; showPubCopy = false;
showAdvanced = false; showAdvanced = false;
alertMessage = "";
alertTitle = "";
public async getIdentity(activeDid: string): Promise<IIdentifier | null> { public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
try { try {
@ -424,7 +426,7 @@ export default class AccountViewView extends Vue {
} }
// Return parsed identity or null if not found // Return parsed identity or null if not found
return JSON.parse(account?.identity || "null"); return JSON.parse((account?.identity as string) || "null");
} }
/** /**
@ -505,12 +507,14 @@ export default class AccountViewView extends Vue {
* Initializes component state with values from the database or defaults. * Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database. * @param {SettingsType} settings - Object containing settings from the database.
*/ */
initializeState(settings: SettingsType | undefined) { initializeState(settings: Settings | undefined) {
this.activeDid = settings?.activeDid || ""; this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = settings?.apiServer || ""; this.apiServerInput = (settings?.apiServer as string) || "";
this.firstName = settings?.firstName || ""; this.givenName =
this.lastName = settings?.lastName || ""; (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
} }
@ -527,7 +531,7 @@ export default class AccountViewView extends Vue {
) { ) {
this.publicHex = identity.keys[0].publicKeyHex; this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); 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, { db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did, activeDid: identity.did,
@ -697,6 +701,27 @@ export default class AccountViewView extends Vue {
const resp = await this.fetchRateLimits(identity); const resp = await this.fetchRateLimits(identity);
if (resp.status === 200) { if (resp.status === 200) {
this.limits = resp.data; 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) { } catch (error) {
this.handleRateLimitsError(error); this.handleRateLimitsError(error);
@ -725,8 +750,13 @@ export default class AccountViewView extends Vue {
private handleRateLimitsError(error: unknown) { private handleRateLimitsError(error: unknown) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
const data = error.response?.data as ErrorResponse; const data = error.response?.data as ErrorResponse;
this.limitsMessage = data?.error?.message || "Bad server response."; this.limitsMessage =
console.error("Bad response retrieving limits:", error); (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 ( } else if (
error instanceof Error && error instanceof Error &&
error.message === error.message ===

147
src/views/ContactGiftingView.vue

@ -16,10 +16,6 @@
</h1> </h1>
</div> </div>
<!-- Quick Search -->
<!-- Initial Loading Animation -->
<!-- Results List --> <!-- Results List -->
<ul class="border-t border-slate-300"> <ul class="border-t border-slate-300">
<li class="border-b border-slate-300 py-3"> <li class="border-b border-slate-300 py-3">
@ -70,12 +66,7 @@
</li> </li>
</ul> </ul>
<GiftedDialog <GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
</section> </section>
</template> </template>
@ -83,16 +74,10 @@
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts"; import { Account, AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import { GiverInputInfo } from "@/libs/endorserServer";
createAndSubmitGive,
CreateAndSubmitGiveResult,
ErrorResult,
GiverInputInfo,
GiverOutputInfo,
} from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
@ -124,10 +109,10 @@ export default class ContactGiftingView extends Vue {
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = (await accountsDB.accounts
.where("did") .where("did")
.equals(activeDid) .equals(activeDid)
.first(); .first()) as Account;
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
@ -150,7 +135,7 @@ export default class ContactGiftingView extends Vue {
async created() { async created() {
try { try {
await db.open(); 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.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
@ -173,123 +158,5 @@ export default class ContactGiftingView extends Vue {
openDialog(giver: GiverInputInfo) { openDialog(giver: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver); (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> </script>

56
src/views/ContactQRScanShowView.vue

@ -3,7 +3,7 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
Your Contact Info Your Contact Info
</h1> </h1>
@ -17,12 +17,17 @@
:dotsOptions="{ type: 'square' }" :dotsOptions="{ type: 'square' }"
class="flex justify-center" class="flex justify-center"
/> />
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import QRCodeVue3 from "qr-code-generator-vue3"; import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as R from "ramda"; import * as R from "ramda";
@ -30,6 +35,10 @@ import { SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import {
CONTACT_URL_PREFIX,
ENDORSER_JWT_URL_LOCATION,
} from "@/libs/endorserServer";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
@ -43,6 +52,7 @@ interface Notification {
@Component({ @Component({
components: { components: {
QrcodeStream,
QRCodeVue3, QRCodeVue3,
QuickNav, QuickNav,
}, },
@ -98,7 +108,9 @@ export default class ContactQRScanShow extends Vue {
iat: Date.now(), iat: Date.now(),
iss: this.activeDid, iss: this.activeDid,
own: { own: {
name: (settings?.firstName || "") + " " + (settings?.lastName || ""), name:
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
publicEncKey, publicEncKey,
}, },
}; };
@ -112,9 +124,47 @@ export default class ContactQRScanShow extends Vue {
issuer: identity.did, issuer: identity.did,
signer: signer, signer: signer,
}); });
const viewPrefix = "https://endorser.ch/contact?jwt="; const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
this.qrValue = viewPrefix + vcJwt; this.qrValue = viewPrefix + vcJwt;
} }
} }
/**
*
* @param content is the result of a QR scan, an array with one item with a rawValue property
*/
// Unfortunately, there are not typescript definitions for the qrcode-stream component yet.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanDetect(content: any) {
if (content[0]?.rawValue) {
console.log("onDetect", content[0].rawValue);
localStorage.setItem("contactEndorserUrl", content[0].rawValue);
this.$router.push({ name: "contacts" });
} else {
this.$notify(
{
group: "alert",
type: "warning",
title: "Invalid Contact QR Code",
text: "No QR code detected with contact information.",
},
-1,
);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanError(error: any) {
console.log("Scan was invalid:", error);
this.$notify(
{
group: "alert",
type: "warning",
title: "Invalid Scan",
text: "The scan was invalid.",
},
-1,
);
}
} }
</script> </script>

293
src/views/ContactsView.vue

@ -20,6 +20,11 @@
<!-- New Contact --> <!-- New Contact -->
<div class="mb-4 flex"> <div class="mb-4 flex">
<span class="self-center bg-slate-500 text-white px-1.5 py-1 rounded-md">
<router-link :to="{ name: 'contact-qr' }">
<fa icon="qrcode" class="fa-fw" />
</router-link>
</span>
<input <input
type="text" type="text"
placeholder="DID, Name, Public Key" placeholder="DID, Name, Public Key"
@ -88,6 +93,16 @@
class="inline-block align-text-bottom border border-slate-300 rounded" class="inline-block align-text-bottom border border-slate-300 rounded"
></EntityIcon> ></EntityIcon>
{{ contact.name || "(no name)" }} {{ contact.name || "(no name)" }}
<button
class="text-sm uppercase bg-slate-500 text-white px-1 rounded-md"
@click="
contactEdit = contact;
contactNewName = contact.name;
"
title="Edit"
>
<fa icon="pen" class="fa-fw" />
</button>
</h2> </h2>
<div class="text-sm truncate">{{ contact.did }}</div> <div class="text-sm truncate">{{ contact.did }}</div>
<div class="text-sm truncate" v-if="contact.publicKeyBase64"> <div class="text-sm truncate" v-if="contact.publicKeyBase64">
@ -204,6 +219,31 @@
</li> </li>
</ul> </ul>
<p v-else>This identity has no contacts.</p> <p v-else>This identity has no contacts.</p>
<div v-if="contactEdit !== null" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
<input
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="Name"
v-model="contactNewName"
/>
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
@click="onClickSaveName(contactEdit, contactNewName)"
>
<fa icon="save" />
</button>
<span class="inline-block w-2" />
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
@click="onClickCancelName()"
>
<fa icon="ban" />
</button>
</div>
</div>
</section> </section>
</template> </template>
@ -211,11 +251,17 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as R from "ramda"; import * as R from "ramda";
import { NotificationIface } from "@/constants/app";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; 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, SimpleSigner } from "@/libs/crypto"; import {
accessToken,
getContactPayloadFromJwtUrl,
SimpleSigner,
} from "@/libs/crypto";
import { import {
GiveServerRecord, GiveServerRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
@ -225,27 +271,24 @@ import {
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { QuickNav, EntityIcon }, components: { QuickNav, EntityIcon },
}) })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
contacts: Array<Contact> = []; contacts: Array<Contact> = [];
contactEndorserUrl = localStorage.getItem("contactEndorserUrl") || "";
contactInput = ""; contactInput = "";
contactEdit: Contact | null = null;
contactNewName = "";
// { "did:...": concatenated-descriptions } entry for each contact // { "did:...": concatenated-descriptions } entry for each contact
givenByMeDescriptions: Record<string, string> = {}; givenByMeDescriptions: Record<string, string> = {};
// { "did:...": amount } entry for each contact // { "did:...": amount } entry for each contact
@ -266,7 +309,7 @@ export default class ContactsView extends Vue {
async created() { async created() {
await db.open(); 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.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
@ -279,12 +322,18 @@ export default class ContactsView extends Vue {
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""), (a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts, allContacts,
); );
if (this.contactEndorserUrl) {
await this.newContactFromScan(this.contactEndorserUrl);
localStorage.removeItem("contactEndorserUrl");
this.contactEndorserUrl = "";
}
} }
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); 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"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
@ -346,7 +395,7 @@ export default class ContactsView extends Vue {
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: text:
"Got an error retrieving your " + "Got an error retrieving your " +
(useRecipient ? "given" : "received") + (useRecipient ? "given" : "received") +
@ -405,7 +454,7 @@ export default class ContactsView extends Vue {
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: error as string, text: error as string,
}, },
-1, -1,
@ -414,6 +463,18 @@ export default class ContactsView extends Vue {
} }
async onClickNewContact(): Promise<void> { async onClickNewContact(): Promise<void> {
if (!this.contactInput) {
this.$notify(
{
group: "alert",
type: "warning",
title: "No Contact",
text: "There was no contact info to add.",
},
-1,
);
return;
}
let did = this.contactInput; let did = this.contactInput;
let name, publicKeyBase64; let name, publicKeyBase64;
const commaPos1 = this.contactInput.indexOf(","); const commaPos1 = this.contactInput.indexOf(",");
@ -432,12 +493,74 @@ export default class ContactsView extends Vue {
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64"); publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
} }
const newContact = { did, name, publicKeyBase64 }; const newContact = { did, name, publicKeyBase64 };
await db.contacts.add(newContact); return this.addContact(newContact);
const allContacts = this.contacts.concat([newContact]); }
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""), async newContactFromScan(url: string): Promise<void> {
allContacts, const payload = getContactPayloadFromJwtUrl(url);
); if (!payload) {
this.$notify(
{
group: "alert",
type: "danger",
title: "No Contact Info",
text: "The contact info could not be parsed.",
},
-1,
);
return;
} else {
return this.addContact({
did: payload.iss,
name: payload.own.name,
publicKeyBase64: payload.own.publicEncKey,
} as Contact);
}
}
async addContact(newContact: Contact) {
if (!newContact.did) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Incomplete Contact",
text: "Cannot add a contact without a DID.",
},
-1,
);
return;
}
return db.contacts
.add(newContact)
.then(() => {
const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts,
);
this.$notify(
{
group: "alert",
type: "success",
title: "Contact added",
text: newContact.name + " was added.",
},
-1,
);
})
.catch((err) => {
console.error("Error when adding contact to storage:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Contact Not Added",
text: "An error prevented importing.",
},
-1,
);
});
} }
async deleteContact(contact: Contact) { async deleteContact(contact: Contact) {
@ -467,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 identity = await this.getIdentity(this.activeDid);
const vcClaim: RegisterVerifiableCredential = { const vcClaim: RegisterVerifiableCredential = {
@ -549,7 +682,7 @@ export default class ContactsView extends Vue {
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: userMessage, text: userMessage,
}, },
-1, -1,
@ -574,36 +707,30 @@ export default class ContactsView extends Vue {
contact.seesMe = visibility; contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility }); db.contacts.update(contact.did, { seesMe: visibility });
} else { } else {
console.error("Bad response setting visibility: ", resp.data); console.error(
if (resp.data.error?.message) { "Got some bad server response when setting visibility: ",
this.$notify( resp,
{ );
group: "alert", const message =
type: "danger", resp.data.error?.message || "Bad server response of " + resp.status;
title: "Error With Server", this.$notify(
text: resp.data.error?.message, {
}, group: "alert",
-1, type: "danger",
); title: "Server Error",
} else { text: message,
this.$notify( },
{ -1,
group: "alert", );
type: "danger",
title: "Error With Server",
text: "Bad server response of " + resp.status,
},
-1,
);
}
} }
} catch (err) { } catch (err) {
console.error("Got some server error when setting visibility:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: err as string, text: "Check connectivity and try again.",
}, },
-1, -1,
); );
@ -628,7 +755,7 @@ export default class ContactsView extends Vue {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "toast", type: "info",
title: "Refreshed", title: "Refreshed",
text: text:
this.nameForContact(contact, true) + this.nameForContact(contact, true) +
@ -636,38 +763,29 @@ export default class ContactsView extends Vue {
(visibility ? "" : "not ") + (visibility ? "" : "not ") +
"see your activity.", "see your activity.",
}, },
5000, -1,
); );
} else { } else {
if (resp.data.error?.message) { console.log("Got bad server response when checking visibility: ", resp);
this.$notify( const message = resp.data.error?.message || "Got bad server response.";
{ this.$notify(
group: "alert", {
type: "danger", group: "alert",
title: "Error With Server", type: "danger",
text: resp.data.error?.message, title: "Server Error",
}, text: message,
-1, },
); -1,
} else { );
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: "Bad server response of " + resp.status,
},
-1,
);
}
} }
} catch (err) { } catch (err) {
console.log("Caught error from server request to check visibility:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: err as string, text: "Check connectivity and try again.",
}, },
-1, -1,
); );
@ -867,7 +985,7 @@ export default class ContactsView extends Vue {
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: userMessage, text: userMessage,
}, },
-1, -1,
@ -876,6 +994,18 @@ export default class ContactsView extends Vue {
} }
} }
private async onClickCancelName() {
this.contactEdit = null;
this.contactNewName = "";
}
private async onClickSaveName(contact: Contact, newName: string) {
contact.name = newName;
return db.contacts
.update(contact.did, { name: newName })
.then(() => (this.contactEdit = null));
}
public toggleShowGiveTotals() { public toggleShowGiveTotals() {
if (this.showGiveTotals) { if (this.showGiveTotals) {
this.showGiveTotals = false; this.showGiveTotals = false;
@ -900,6 +1030,26 @@ export default class ContactsView extends Vue {
</script> </script>
<style> <style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 500px;
}
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */ /* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
/* Tooltip container */ /* Tooltip container */
.tooltip { .tooltip {
@ -907,7 +1057,6 @@ export default class ContactsView extends Vue {
display: inline-block; display: inline-block;
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */ border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
} }
/* Tooltip text */ /* Tooltip text */
.tooltip .tooltiptext { .tooltip .tooltiptext {
visibility: hidden; visibility: hidden;

15
src/views/HelpView.vue

@ -41,14 +41,15 @@
You need someone to register you -- usually the person who told you You need someone to register you -- usually the person who told you
about this app, on the Contacts about this app, on the Contacts
<fa icon="circle-user" class="fa-fw" /> page. After they register you, <fa icon="circle-user" class="fa-fw" /> page. After they register you,
and after you have contacts, you can select any contact on the home page you can select any contact on the home page (or "anonymous") and record
and record your appreciation for... whatever. That is a claim recorded your appreciation for... whatever. The main goal is to record what
people have given you, to grow gifting economies. Each claim is recorded
on a custom ledger. The day after being registered, you'll be able to on a custom ledger. The day after being registered, you'll be able to
register others; later, you can create projects, too. able to register others; later, you can create projects, too.
</p> </p>
<p> <p>
Note that there are limits to how many each person can register, so you Note that there are limits to how many others each person can register,
may have to wait. so you may have to wait.
</p> </p>
<h2 class="text-xl font-semibold">How do I backup all my data?</h2> <h2 class="text-xl font-semibold">How do I backup all my data?</h2>
@ -130,7 +131,9 @@
<h2 class="text-xl font-semibold">How do I create another identity?</h2> <h2 class="text-xl font-semibold">How do I create another identity?</h2>
<p> <p>
Go Before doing this, note that it is an advanced feature that affects
functionality (eg. the words "Alt ID" next to results, backup features)
so beware if you think that may cause confusion. You can
<router-link to="start" class="text-blue-500"> <router-link to="start" class="text-blue-500">
create another identity here. create another identity here.
</router-link> </router-link>

404
src/views/HomeView.vue

@ -6,198 +6,82 @@
Time Safari Time Safari
</h1> </h1>
<!-- show the actions for recognizing a give -->
<div class="mb-8"> <div class="mb-8">
<h2 class="text-xl font-bold mb-4">Notiwind Alert Test Suite</h2> <div v-if="!activeDid">
To record others' giving,
<button <router-link :to="{ name: 'start' }" class="text-blue-500">
@click=" create your identifier.</router-link
this.$notify( >
{ </div>
group: 'alert',
type: 'toast',
text: 'I\'m a toast. Don\'t mind me.',
},
5000,
)
"
class="font-bold uppercase bg-slate-400 text-white px-3 py-2 rounded-md mr-2"
>
Toast (self-dismiss)
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'info',
title: 'Information Alert',
text: 'Just wanted you to know.',
},
-1,
)
"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Info
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'success',
title: 'Success Alert',
text: 'Congratulations!',
},
-1,
)
"
class="font-bold uppercase bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
>
Success
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'warning',
title: 'Warning Alert',
text: 'You might wanna look at this.',
},
-1,
)
"
class="font-bold uppercase bg-amber-600 text-white px-3 py-2 rounded-md mr-2"
>
Warning
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'danger',
title: 'Danger Alert',
text: 'Something terrible has happened!',
},
-1,
)
"
class="font-bold uppercase bg-rose-600 text-white px-3 py-2 rounded-md mr-2"
>
Danger
</button>
<button
@click="
this.$notify(
{
group: 'modal',
type: 'notification-permission',
},
-1,
)
"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Notif ON
</button>
<button
@click="
this.$notify(
{
group: 'modal',
type: 'notification-mute',
},
-1,
)
"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Notif MUTE
</button>
<button
@click="
this.$notify(
{
group: 'modal',
type: 'notification-off',
},
-1,
)
"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Notif OFF
</button>
</div>
<div class="mb-8"> <div v-else-if="!isRegistered">
<h2 class="text-xl font-bold">Quick Action</h2> To record others' giving, someone must register your account, so show
<p class="mb-4">Record a gift from a contact:</p> them
<router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5"> your identity info</router-link
<li @click="openDialog()"> >
<EntityIcon and then
:entityId="null" <router-link :to="{ name: 'account' }" class="text-blue-500">
:iconSize="64" check your limits.</router-link
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)"
> >
<EntityIcon </div>
:entityId="contact.did"
:iconSize="64" <div v-else>
class="mx-auto border border-slate-300 rounded-md mb-1" <!-- activeDid && isRegistered -->
></EntityIcon> <h2 class="text-xl font-bold">Record a Gift</h2>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden" <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 }} <EntityIcon
</h3> :entityId="contact.did"
</li> :iconSize="64"
</ul> 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) --> <!-- If there are no contacts, show this instead: -->
<router-link <div
v-if="allContacts.length > 7" class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
:to="{ name: 'contact-gives' }" v-if="allContacts.length === 0"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md" >
> (No contacts to show.)
Show More Contacts&hellip; </div>
</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.)
</div> </div>
</div> </div>
<GiftedDialog <GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-bold mb-4">Latest Activity</h2> <h2 class="text-xl font-bold mb-4">Latest Activity</h2>
@ -233,19 +117,18 @@
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db/index"; 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 { accessToken } from "@/libs/crypto";
import { import {
createAndSubmitGive,
didInfo, didInfo,
GiverInputInfo, GiverInputInfo,
GiverOutputInfo,
GiveServerRecord, GiveServerRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Account } from "@/db/tables/accounts";
interface Notification { interface Notification {
group: string; group: string;
@ -269,6 +152,7 @@ export default class HomeView extends Vue {
feedPreviousOldestId?: string; feedPreviousOldestId?: string;
feedLastViewedId?: string; feedLastViewedId?: string;
isHiddenSpinner = true; isHiddenSpinner = true;
isRegistered = false;
numAccounts = 0; numAccounts = 0;
async beforeCreate() { async beforeCreate() {
@ -278,10 +162,10 @@ export default class HomeView extends Vue {
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = (await accountsDB.accounts
.where("did") .where("did")
.equals(activeDid) .equals(activeDid)
.first(); .first()) as Account;
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
@ -308,11 +192,12 @@ export default class HomeView extends Vue {
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
await db.open(); 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.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId; this.feedLastViewedId = settings?.lastViewedClaimId;
this.isRegistered = !!settings?.isRegistered;
this.updateAllFeed(); this.updateAllFeed();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
@ -338,7 +223,9 @@ export default class HomeView extends Vue {
if (this.activeDid) { if (this.activeDid) {
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); 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"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
@ -415,6 +302,7 @@ export default class HomeView extends Vue {
} }
giveDescription(giveRecord: GiveServerRecord) { giveDescription(giveRecord: GiveServerRecord) {
// similar code is in endorser-mobile utility.ts
// claim.claim happen for some claims wrapped in a Verifiable Credential // claim.claim happen for some claims wrapped in a Verifiable Credential
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim; const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim;
@ -427,9 +315,18 @@ export default class HomeView extends Vue {
this.allMyDids, this.allMyDids,
this.allContacts, this.allContacts,
); );
const gaveAmount = claim.object?.amountOfThisGood let gaveAmount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood) ? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: claim.description || "something unknown"; : "";
if (claim.description) {
if (gaveAmount) {
gaveAmount = gaveAmount + ", and also: ";
}
gaveAmount = gaveAmount + claim.description;
}
if (!gaveAmount) {
gaveAmount = "something not described";
}
// recipient.did is for legacy data, before March 2023 // recipient.did is for legacy data, before March 2023
const gaveRecipientId = const gaveRecipientId =
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -443,7 +340,7 @@ export default class HomeView extends Vue {
this.allContacts, this.allContacts,
) )
: ""; : "";
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo; return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
} }
displayAmount(code: string, amt: number) { displayAmount(code: string, amt: number) {
@ -457,124 +354,5 @@ export default class HomeView extends Vue {
openDialog(giver: GiverInputInfo) { openDialog(giver: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver); (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
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isGiveCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getGiveCreationErrorMessage(result: any) {
return result.data?.error?.message;
}
} }
</script> </script>

20
src/views/IdentitySwitcherView.vue

@ -22,7 +22,7 @@
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa> <fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
<span class="overflow-hidden"> <span class="overflow-hidden">
<h2 class="text-xl font-semibold mb-0"> <h2 class="text-xl font-semibold mb-0">
{{ firstName }} {{ lastName }} {{ givenName }}
</h2> </h2>
<div class="text-sm text-slate-500 truncate"> <div class="text-sm text-slate-500 truncate">
<b>ID:</b> <code>{{ activeDid }}</code> <b>ID:</b> <code>{{ activeDid }}</code>
@ -49,7 +49,9 @@
</ul> </ul>
<!-- Actions --> <!-- Actions -->
<!-- id used by puppeteer test script -->
<router-link <router-link
id="start-link"
:to="{ name: 'start' }" :to="{ name: 'start' }"
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-blue-600 text-white px-2 py-3 rounded-md mb-2"
> >
@ -69,7 +71,7 @@ import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts"; 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"; import QuickNav from "@/components/QuickNav.vue";
interface Notification { interface Notification {
@ -88,8 +90,7 @@ export default class IdentitySwitcherView extends Vue {
public activeDid = ""; public activeDid = "";
public apiServer = ""; public apiServer = "";
public apiServerInput = ""; public apiServerInput = "";
public firstName = ""; public givenName = "";
public lastName = "";
public otherIdentities: Array<{ did: string }> = []; public otherIdentities: Array<{ did: string }> = [];
public showContactGives = false; public showContactGives = false;
@ -99,19 +100,20 @@ export default class IdentitySwitcherView extends Vue {
.where("did") .where("did")
.equals(activeDid) .equals(activeDid)
.first(); .first();
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse((account?.identity as string) || "null");
return identity; return identity;
} }
async created() { async created() {
try { try {
await db.open(); 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.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || ""; this.apiServerInput = settings?.apiServer || "";
this.firstName = settings?.firstName || "No"; this.givenName =
this.lastName = settings?.lastName || "Name"; (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
@ -149,7 +151,7 @@ export default class IdentitySwitcherView extends Vue {
did = undefined; did = undefined;
} }
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did, activeDid: did,
}); });
this.activeDid = did || ""; this.activeDid = did || "";

2
src/views/ImportAccountView.vue

@ -17,7 +17,9 @@
<p class="text-center text-xl mb-4 font-light"> <p class="text-center text-xl mb-4 font-light">
Enter your seed phrase below to import your identity on this device. Enter your seed phrase below to import your identity on this device.
</p> </p>
<!-- id used by puppeteer test script -->
<input <input
id="seed-input"
type="text" type="text"
placeholder="Seed Phrase" placeholder="Seed Phrase"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"

38
src/views/NewEditAccountView.vue

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

4
src/views/NewEditProjectView.vue

@ -1,4 +1,5 @@
<template> <template>
<QuickNav selected="Projects"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Breadcrumb --> <!-- Breadcrumb -->
@ -107,6 +108,7 @@ import * as didJwt from "did-jwt";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import QuickNav from "@/components/QuickNav.vue";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
@ -122,7 +124,7 @@ interface Notification {
} }
@Component({ @Component({
components: { LMap, LMarker, LTileLayer }, components: { LMap, LMarker, LTileLayer, QuickNav },
}) })
export default class NewEditProjectView extends Vue { export default class NewEditProjectView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;

2
src/views/NewIdentifierView.vue

@ -46,7 +46,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class AccountViewView extends Vue { export default class NewIdentifierView extends Vue {
loading = true; loading = true;
async mounted() { async mounted() {

117
src/views/ProjectViewView.vue

@ -102,7 +102,7 @@
<h3 <h3
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden" class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
> >
Anonymous Anonymous/Unnamed
</h3> </h3>
</li> </li>
<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) --> <!-- 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 <router-link
v-if="allContacts.length > 7" v-if="allContacts.length >= 7"
:to="{ name: 'contact-gives' }" :to="{ name: 'contact-gives' }"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md" 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 <GiftedDialog
ref="customDialog" ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from" message="Received from"
:projectId="this.projectId"
> >
</GiftedDialog> </GiftedDialog>
</section> </section>
@ -212,18 +212,16 @@ import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; 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 { accessToken } from "@/libs/crypto";
import { import {
createAndSubmitGive,
didInfo, didInfo,
GiverInputInfo, GiverInputInfo,
GiverOutputInfo,
GiveServerRecord, GiveServerRecord,
ResultWithType,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts";
interface Notification { interface Notification {
group: string; group: string;
@ -257,7 +255,7 @@ export default class ProjectViewView extends Vue {
async created() { async created() {
await db.open(); 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.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
@ -266,17 +264,17 @@ export default class ProjectViewView extends Vue {
const accounts = accountsDB.accounts; const accounts = accountsDB.accounts;
const accountsArr = await accounts?.toArray(); const accountsArr = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did); 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"); const identity = JSON.parse(account?.identity || "null");
this.LoadProject(identity); this.LoadProject(identity);
} }
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = (await accountsDB.accounts
.where("did") .where("did")
.equals(activeDid) .equals(activeDid)
.first(); .first()) as Account;
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { 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() { getOpenStreetMapUrl() {
// Google URL is https://maps.google.com/?q=LAT,LONG // Google URL is https://maps.google.com/?q=LAT,LONG
return ( return (
@ -480,96 +473,8 @@ export default class ProjectViewView extends Vue {
); );
} }
handleDialogResult(result: GiverOutputInfo) { openDialog(contact: GiverInputInfo) {
if (result.action === "confirm") { (this.$refs.customDialog as GiftedDialog).open(contact);
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,
);
}
}
} }
} }
</script> </script>

2
src/views/ProjectsView.vue

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

3
src/views/StartView.vue

@ -8,7 +8,8 @@
Start Here Start Here
</h1> </h1>
<div class="mt-8"> <!-- id used by puppeteer test script -->
<div id="start-question" class="mt-8">
<p class="text-center text-xl mb-4 font-light"> <p class="text-center text-xl mb-4 font-light">
Do you have an identity to import? Do you have an identity to import?
</p> </p>

151
src/views/TestView.vue

@ -0,0 +1,151 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Test
</h1>
<div class="mb-8">
<h2 class="text-xl font-bold mb-4">Notiwind Alert Test Suite</h2>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'toast',
text: 'I\'m a toast. Don\'t mind me.',
},
5000,
)
"
class="font-bold uppercase bg-slate-400 text-white px-3 py-2 rounded-md mr-2"
>
Toast (self-dismiss)
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'info',
title: 'Information Alert',
text: 'Just wanted you to know.',
},
-1,
)
"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Info
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'success',
title: 'Success Alert',
text: 'Congratulations!',
},
-1,
)
"
class="font-bold uppercase bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
>
Success
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'warning',
title: 'Warning Alert',
text: 'You might wanna look at this.',
},
-1,
)
"
class="font-bold uppercase bg-amber-600 text-white px-3 py-2 rounded-md mr-2"
>
Warning
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'danger',
title: 'Danger Alert',
text: 'Something terrible has happened!',
},
-1,
)
"
class="font-bold uppercase bg-rose-600 text-white px-3 py-2 rounded-md mr-2"
>
Danger
</button>
<button
@click="
this.$notify(
{
group: 'modal',
type: 'notification-permission',
},
-1,
)
"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Notif ON
</button>
<button
@click="
this.$notify(
{
group: 'modal',
type: 'notification-mute',
},
-1,
)
"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Notif MUTE
</button>
<button
@click="
this.$notify(
{
group: 'modal',
type: 'notification-off',
},
-1,
)
"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Notif OFF
</button>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { QuickNav } })
export default class Help extends Vue {}
</script>
Loading…
Cancel
Save