diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..84a0b85 --- /dev/null +++ b/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. diff --git a/README.md b/README.md index 93c1b8d..76c09f3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # kickstart-for-time-pwa ## Project setup + +We have pkgx.dev set up in package.json, so you can use `dev` to set up the dev environment. + ``` npm install ``` @@ -23,6 +26,12 @@ npm run build 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 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 ### Reference Material diff --git a/project.task.yaml b/project.task.yaml index e562137..779df7b 100644 --- a/project.task.yaml +++ b/project.task.yaml @@ -1,23 +1,18 @@ tasks: -- 08 Scan QR code to import into contacts assignee:matthew - - SEE - https://github.com/gruhn/vue-qrcode-reader - - in endorser-push-server - mount folder for persistent sqlite DB outside of container -- test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView) -- .2 bug - on contacts view, click on "to" & "from" and nothing happens - 40 notifications : - 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 fix the Discovery map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui +- .1 add instructions for map location selection -- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew assignee-group:ui +- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished -- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s) assignee-group:ui - - SEE: https://github.com/emmanuelsw/notiwind assignee:jose assignee-group:ui +- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s) - Home Feed & Quick Give screen : - 01 save the feed-viewed status in settings storage ("afterQuery") @@ -26,29 +21,34 @@ tasks: - 24 Move to Vite assignee:matthew -- .2 Edit Plan does not have icons across the bottom assignee-group:ui -- .5 include the hash of the latest commit, and maybe a version +- .5 switch so DiscoverView shows anywhere by default, and no number unless search is done (and maybe a better filter UI, including "mine" to consolidate with ProjectsView) +- .2 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui +- .5 Add infinite scroll to gifts on the home page +- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all +- .2 figure out why endorser-mobile search doesn't find recently created PlanAction +- .1 when creating a plan, select location and then make sure you can deselect on Android - .5 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 remove edit from project page for projects owned by others +- .5 add project ID to the URL of the project-view, to make a project publicly-accessible - .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page -- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist assignee-group:ui -- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose assignee-group:ui +- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist - .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent - .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164 - .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show" +- .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. - 04 allow user to download claims, mine + ones I can see about me from others -- .5 change the derivation path, and regenerate test IDs - 02 allow user to create new DIDs from the same seed phrase (ie. increment derivation path) -- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages assignee-group:ui +- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages - .5 customize favicon assignee-group:ui -- .5 Do we want to combine first name & last name? -- .2 Show a warning if both giver and recipient are the same (but still allow?) assignee-group:ui +- .2 Show a warning if both giver and recipient are the same (but still allow?) - 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui -- .5 Display a more appealing confirmation on the map when erasing the marker assignee-group:ui +- .5 Display a more appealing confirmation on the map when erasing the marker +- .5 make a VC details page +- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc) +- .1 remove firstName (& lastName) from localStorage - contacts v+ : - 01 Import all the non-sensitive data (ie. contacts & settings). @@ -57,6 +57,7 @@ tasks: - stats v1 : - 01 show numeric stats + - 04 show different graphic for projects vs people on world - 01 link to world for specific stats - .5 don't load another instance of a bush if it already exists - maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version") @@ -72,6 +73,11 @@ tasks: - Test PWA features on Android and iOS. 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 : - show total time given to & from a project - terminology: diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index d06d334..f803046 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -2,7 +2,7 @@

- {{ message }} {{ giver?.name || "somebody not specified" }} + {{ message }} {{ giver?.name || "somebody not named" }}

diff --git a/src/constants/app.ts b/src/constants/app.ts index ce4ddb4..270277d 100644 --- a/src/constants/app.ts +++ b/src/constants/app.ts @@ -1,5 +1,7 @@ /** * Generic strings that could be used throughout the app. + * + * See also ../libs/veramo/setup.ts */ export enum AppString { APP_NAME = "TimeSafari", @@ -10,3 +12,13 @@ export enum AppString { 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; +} diff --git a/src/db/tables/contacts.ts b/src/db/tables/contacts.ts index 00923f5..b684505 100644 --- a/src/db/tables/contacts.ts +++ b/src/db/tables/contacts.ts @@ -7,5 +7,5 @@ export interface Contact { } export const ContactsSchema = { - contacts: "++did, name, publicKeyBase64, registered, seesMe", + contacts: "&did, name, publicKeyBase64, registered, seesMe", }; diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts index 58f149d..a228533 100644 --- a/src/db/tables/settings.ts +++ b/src/db/tables/settings.ts @@ -13,7 +13,6 @@ export type BoundingBox = { */ export type Settings = { id: number; // Only one entry using MASTER_SETTINGS_KEY - activeDid?: string; // Active Decentralized ID apiServer?: string; // API server URL firstName?: string; // User's first name @@ -22,6 +21,7 @@ export type Settings = { lastNotifiedClaimId?: string; // Last notified claim ID // Array of named search boxes defined by bounding boxes + searchBoxes?: Array<{ name: string; bbox: BoundingBox; diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts index b3600b4..be8ccef 100644 --- a/src/libs/crypto/index.ts +++ b/src/libs/crypto/index.ts @@ -1,5 +1,4 @@ import { IIdentifier } from "@veramo/core"; -import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup"; import { getRandomBytesSync } from "ethereum-cryptography/random"; import { entropyToMnemonic } from "ethereum-cryptography/bip39"; 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 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 { 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; +}; diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index ce7093a..628d5ba 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -6,7 +6,12 @@ import { Axios, AxiosResponse } from "axios"; import { Contact } from "@/db/tables/contacts"; export const SCHEMA_ORG_CONTEXT = "https://schema.org"; +// the object in RegisterAction claims 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 { "@context": string; @@ -111,6 +116,8 @@ export function isHiddenDid(did: string) { /** always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY + + Similar logic is found in endorser-mobile. **/ export function didInfo( did: string, @@ -118,14 +125,14 @@ export function didInfo( allMyDids: string[], contacts: Contact[], ): string { + if (!did) return "Someone Anonymous"; + const myId = R.find(R.equals(did), allMyDids); if (myId) return `You${myId !== activeDid ? " (Alt ID)" : ""}`; const contact = R.find((c) => c.did === did, contacts); return contact - ? contact.name || "Someone Unnamed in Contacts" - : !did - ? "Unspecified Person" + ? contact.name || "Contact With No Name" : isHiddenDid(did) ? "Someone Not In Network" : "Someone Not In Contacts"; diff --git a/src/libs/veramo/setup.ts b/src/libs/veramo/setup.ts index f9dcfda..5f6b17b 100644 --- a/src/libs/veramo/setup.ts +++ b/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) { return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName); } -//const NETWORK_NAMES = ["mainnet", "rinkeby"]; - -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({ plugins: allPlugins }) +export const DEFAULT_DID_PROVIDER_NAME = didProviderName("mainnet"); diff --git a/src/main.ts b/src/main.ts index 980b0e8..7bcaade 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,7 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faArrowLeft, faArrowRight, + faBan, faBurst, faCalendar, faChevronLeft, @@ -59,6 +60,7 @@ import { library.add( faArrowLeft, faArrowRight, + faBan, faBurst, faCalendar, faChevronLeft, diff --git a/src/router/index.ts b/src/router/index.ts index e541d07..331d314 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -33,7 +33,6 @@ const routes: Array = [ name: "home", component: () => import(/* webpackChunkName: "home" */ "../views/HomeView.vue"), - beforeEnter: enterOrStart, }, { path: "/account", @@ -58,6 +57,14 @@ const routes: Array = [ /* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue" ), }, + { + path: "/contact-gives", + name: "contact-gives", + component: () => + import( + /* webpackChunkName: "contact-gives" */ "../views/ContactGiftingView.vue" + ), + }, { path: "/contact-qr", name: "contact-qr", @@ -71,15 +78,6 @@ const routes: Array = [ name: "contacts", component: () => import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"), - beforeEnter: enterOrStart, - }, - { - path: "/scan-contact", - name: "scan-contact", - component: () => - import( - /* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue" - ), }, { path: "/discover", @@ -93,6 +91,14 @@ const routes: Array = [ component: () => import(/* webpackChunkName: "help" */ "../views/HelpView.vue"), }, + { + path: "/identity-switcher", + name: "identity-switcher", + component: () => + import( + /* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue" + ), + }, { path: "/import-account", name: "import-account", @@ -141,14 +147,6 @@ const routes: Array = [ /* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue" ), }, - { - path: "/identity-switcher", - name: "identity-switcher", - component: () => - import( - /* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue" - ), - }, { path: "/project", name: "project", @@ -162,6 +160,14 @@ const routes: Array = [ import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"), beforeEnter: enterOrStart, }, + { + path: "/scan-contact", + name: "scan-contact", + component: () => + import( + /* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue" + ), + }, { path: "/seed-backup", name: "seed-backup", @@ -185,12 +191,10 @@ const routes: Array = [ ), }, { - path: "/contact-gives", - name: "contact-gives", + path: "/test", + name: "test", component: () => - import( - /* webpackChunkName: "contact-gives" */ "../views/ContactGiftingView.vue" - ), + import(/* webpackChunkName: "test" */ "../views/TestView.vue"), }, ]; diff --git a/src/test/index.ts b/src/test/index.ts index 4f622b3..1c2f5f4 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -2,7 +2,7 @@ import axios from "axios"; import * as didJwt from "did-jwt"; import { AppString } from "@/constants/app"; import { db } from "../db"; -import { SERVICE_ID } from "../libs/veramo/setup"; +import { SERVICE_ID } from "../libs/endorserServer"; import { deriveAddress, newIdentifier } from "../libs/crypto"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index a46e9ff..f222fc3 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -52,7 +52,17 @@
-

{{ firstName }} {{ lastName }}

+

+ {{ givenName }} +

+ + + (set name) + +
ID
@@ -67,53 +77,11 @@ Copied!
- -
Public Key (base 64)
-
- {{ publicBase64 }} - - Copied! -
- -
Public Key (hex)
-
- {{ publicHex }} - - Copied! -
- -
Derivation Path
-
- {{ derivationPath }} - - Copied! -
Edit Identity @@ -132,8 +100,10 @@ ) " > + +
App Notifications
-
+
@@ -143,8 +113,6 @@ class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition" >
- -
App Notifications
@@ -192,32 +160,13 @@ class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" @click="exportDatabase()" > - Download Settings & Contacts (excluding Identifier Data) + Download Settings & Contacts +
+ (excluding Identifier Data) - - -
- ID: did:peer:kl45kj41lk451kl3 -
- - - - -
- -
+
@@ -244,20 +193,84 @@
+

Advanced

+
+ +

+ Deep Identity Details +

+
+
Public Key (base 64)
+
+ {{ publicBase64 }} + + Copied! +
+ +
Public Key (hex)
+
+ {{ publicHex }} + + Copied! +
+ +
Derivation Path
+
+ {{ derivationPath }} + + Copied! +
+
+
- - Switch Identity / No Identity - +
- Claim Server + +
+ +
+

Claim Server

- -
- -
@@ -339,11 +351,11 @@ import "dexie-export-import"; import { Component, Vue } from "vue-facing-decorator"; import { useClipboard } from "@vueuse/core"; +import QuickNav from "@/components/QuickNav.vue"; import { AppString } from "@/constants/app"; import { db, accountsDB } from "@/db/index"; -import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; +import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { accessToken } from "@/libs/crypto"; -import QuickNav from "@/components/QuickNav.vue"; import { IIdentifier } from "@veramo/core"; import { ErrorResponse, RateLimits } from "@/libs/endorserServer"; @@ -364,14 +376,6 @@ interface IAccount { derivationPath: string; } -interface SettingsType { - activeDid?: string; - apiServer?: string; - firstName?: string; - lastName?: string; - showContactGivesInline?: boolean; -} - @Component({ components: { QuickNav } }) export default class AccountViewView extends Vue { $notify!: (notification: Notification, timeout?: number) => void; @@ -382,8 +386,8 @@ export default class AccountViewView extends Vue { apiServer = ""; apiServerInput = ""; derivationPath = ""; - firstName = ""; - lastName = ""; + givenName = ""; + isRegistered = false; numAccounts = 0; publicHex = ""; publicBase64 = ""; @@ -398,8 +402,6 @@ export default class AccountViewView extends Vue { showPubCopy = false; showAdvanced = false; - alertMessage = ""; - alertTitle = ""; public async getIdentity(activeDid: string): Promise { try { @@ -424,7 +426,7 @@ export default class AccountViewView extends Vue { } // Return parsed identity or null if not found - return JSON.parse(account?.identity || "null"); + return JSON.parse((account?.identity as string) || "null"); } /** @@ -505,12 +507,14 @@ export default class AccountViewView extends Vue { * Initializes component state with values from the database or defaults. * @param {SettingsType} settings - Object containing settings from the database. */ - initializeState(settings: SettingsType | undefined) { - this.activeDid = settings?.activeDid || ""; - this.apiServer = settings?.apiServer || ""; - this.apiServerInput = settings?.apiServer || ""; - this.firstName = settings?.firstName || ""; - this.lastName = settings?.lastName || ""; + initializeState(settings: Settings | undefined) { + this.activeDid = (settings?.activeDid as string) || ""; + this.apiServer = (settings?.apiServer as string) || ""; + this.apiServerInput = (settings?.apiServer as string) || ""; + this.givenName = + (settings?.firstName || "") + + (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3 + this.isRegistered = !!settings?.isRegistered; this.showContactGives = !!settings?.showContactGivesInline; } @@ -527,7 +531,7 @@ export default class AccountViewView extends Vue { ) { this.publicHex = identity.keys[0].publicKeyHex; this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); - this.derivationPath = identity.keys[0].meta.derivationPath; + this.derivationPath = identity.keys[0].meta.derivationPath as string; db.settings.update(MASTER_SETTINGS_KEY, { activeDid: identity.did, @@ -697,6 +701,27 @@ export default class AccountViewView extends Vue { const resp = await this.fetchRateLimits(identity); if (resp.status === 200) { this.limits = resp.data; + if (!this.isRegistered) { + // the user is not known to be registered, but they are so let's record it + try { + await db.open(); + db.settings.update(MASTER_SETTINGS_KEY, { + isRegistered: true, + }); + this.isRegistered = true; + } catch (err) { + console.log("Got an error updating settings:", err); + this.$notify( + { + group: "alert", + type: "warning", + title: "Update Error", + text: "Unable to update your settings. Check claim limits again.", + }, + -1, + ); + } + } } } catch (error) { this.handleRateLimitsError(error); @@ -725,8 +750,13 @@ export default class AccountViewView extends Vue { private handleRateLimitsError(error: unknown) { if (error instanceof AxiosError) { const data = error.response?.data as ErrorResponse; - this.limitsMessage = data?.error?.message || "Bad server response."; - console.error("Bad response retrieving limits:", error); + this.limitsMessage = + (data?.error?.message as string) || "Bad server response."; + console.log( + "Got bad response retrieving limits, which usually means user isn't registered. Server says:", + this.limitsMessage, + //error, + ); } else if ( error instanceof Error && error.message === diff --git a/src/views/ContactGiftingView.vue b/src/views/ContactGiftingView.vue index cd8a9de..828a30e 100644 --- a/src/views/ContactGiftingView.vue +++ b/src/views/ContactGiftingView.vue @@ -16,10 +16,6 @@
- - - - - - + @@ -83,16 +74,10 @@ import { Component, Vue } from "vue-facing-decorator"; import GiftedDialog from "@/components/GiftedDialog.vue"; import { db, accountsDB } from "@/db/index"; -import { AccountsSchema } from "@/db/tables/accounts"; -import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; +import { Account, AccountsSchema } from "@/db/tables/accounts"; +import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { accessToken } from "@/libs/crypto"; -import { - createAndSubmitGive, - CreateAndSubmitGiveResult, - ErrorResult, - GiverInputInfo, - GiverOutputInfo, -} from "@/libs/endorserServer"; +import { GiverInputInfo } from "@/libs/endorserServer"; import { Contact } from "@/db/tables/contacts"; import QuickNav from "@/components/QuickNav.vue"; import EntityIcon from "@/components/EntityIcon.vue"; @@ -124,10 +109,10 @@ export default class ContactGiftingView extends Vue { public async getIdentity(activeDid: string) { await accountsDB.open(); - const account = await accountsDB.accounts + const account = (await accountsDB.accounts .where("did") .equals(activeDid) - .first(); + .first()) as Account; const identity = JSON.parse(account?.identity || "null"); if (!identity) { @@ -150,7 +135,7 @@ export default class ContactGiftingView extends Vue { async created() { try { await db.open(); - const settings = await db.settings.get(MASTER_SETTINGS_KEY); + const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; this.apiServer = settings?.apiServer || ""; this.activeDid = settings?.activeDid || ""; this.allContacts = await db.contacts.toArray(); @@ -173,123 +158,5 @@ export default class ContactGiftingView extends Vue { openDialog(giver: GiverInputInfo) { (this.$refs.customDialog as GiftedDialog).open(giver); } - - handleDialogResult(result: GiverOutputInfo) { - if (result.action === "confirm") { - return new Promise((resolve) => { - this.recordGive( - result.giver?.did, - result.description, - result.hours, - ).then(() => { - resolve(null); - }); - }); - } else { - // action was "cancel" so do nothing - } - } - - /** - * - * @param giverDid may be null - * @param description may be an empty string - * @param hours may be 0 - */ - public async recordGive( - giverDid?: string, - description?: string, - hours?: number, - ) { - if (!this.activeDid) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "You must select an identity before you can record a give.", - }, - -1, - ); - return; - } - - if (!description && !hours) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "You must enter a description or some number of hours.", - }, - -1, - ); - return; - } - - try { - const identity = await this.getIdentity(this.activeDid); - const result = await createAndSubmitGive( - this.axios, - this.apiServer, - identity, - giverDid, - this.activeDid, - description, - hours, - ); - - if (this.isGiveCreationError(result)) { - const errorMessage = this.getGiveCreationErrorMessage(result); - console.log("Error with give result:", result); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: errorMessage || "There was an error recording the give.", - }, - -1, - ); - } else { - this.$notify( - { - group: "alert", - type: "success", - title: "Success", - text: "That gift was recorded.", - }, - -1, - ); - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - console.log("Error with give caught:", error); - - const message = - error.userMessage || - error.response?.data?.error?.message || - "There was an error recording the Give."; - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: message, - }, - -1, - ); - } - } - - // Helper functions for readability - - isGiveCreationError(result: CreateAndSubmitGiveResult) { - return result.type == "error"; - } - - getGiveCreationErrorMessage(result: CreateAndSubmitGiveResult) { - return (result as ErrorResult).error?.userMessage; - } } diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 54cfd4e..21a924e 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -3,7 +3,7 @@
-

+

Your Contact Info

@@ -17,12 +17,17 @@ :dotsOptions="{ type: 'square' }" class="flex justify-center" /> + +

Scan Contact Info

+
diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index f7e4301..fc3d33f 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -20,6 +20,11 @@
+ + + + + {{ contact.name || "(no name)" }} +
{{ contact.did }}
@@ -204,6 +219,31 @@

This identity has no contacts.

+ +
+
+

Edit Name

+ + + + +
+
@@ -211,11 +251,17 @@ import { AxiosError } from "axios"; import * as didJwt from "did-jwt"; import * as R from "ramda"; + +import { NotificationIface } from "@/constants/app"; import { IIdentifier } from "@veramo/core"; import { accountsDB, db } from "@/db/index"; import { Contact } from "@/db/tables/contacts"; -import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; -import { accessToken, SimpleSigner } from "@/libs/crypto"; +import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; +import { + accessToken, + getContactPayloadFromJwtUrl, + SimpleSigner, +} from "@/libs/crypto"; import { GiveServerRecord, GiveVerifiableCredential, @@ -225,27 +271,24 @@ import { import { Component, Vue } from "vue-facing-decorator"; import QuickNav from "@/components/QuickNav.vue"; import EntityIcon from "@/components/EntityIcon.vue"; +import { Account } from "@/db/tables/accounts"; // eslint-disable-next-line @typescript-eslint/no-var-requires const Buffer = require("buffer/").Buffer; -interface Notification { - group: string; - type: string; - title: string; - text: string; -} - @Component({ components: { QuickNav, EntityIcon }, }) export default class ContactsView extends Vue { - $notify!: (notification: Notification, timeout?: number) => void; + $notify!: (notification: NotificationIface, timeout?: number) => void; activeDid = ""; apiServer = ""; contacts: Array = []; + contactEndorserUrl = localStorage.getItem("contactEndorserUrl") || ""; contactInput = ""; + contactEdit: Contact | null = null; + contactNewName = ""; // { "did:...": concatenated-descriptions } entry for each contact givenByMeDescriptions: Record = {}; // { "did:...": amount } entry for each contact @@ -266,7 +309,7 @@ export default class ContactsView extends Vue { async created() { await db.open(); - const settings = await db.settings.get(MASTER_SETTINGS_KEY); + const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; this.activeDid = settings?.activeDid || ""; this.apiServer = settings?.apiServer || ""; @@ -279,12 +322,18 @@ export default class ContactsView extends Vue { (a: Contact, b) => (a.name || "").localeCompare(b.name || ""), allContacts, ); + + if (this.contactEndorserUrl) { + await this.newContactFromScan(this.contactEndorserUrl); + localStorage.removeItem("contactEndorserUrl"); + this.contactEndorserUrl = ""; + } } public async getIdentity(activeDid: string) { await accountsDB.open(); const accounts = await accountsDB.accounts.toArray(); - const account = R.find((acc) => acc.did === activeDid, accounts); + const account = R.find((acc) => acc.did === activeDid, accounts) as Account; const identity = JSON.parse(account?.identity || "null"); if (!identity) { @@ -346,7 +395,7 @@ export default class ContactsView extends Vue { { group: "alert", type: "danger", - title: "Error With Server", + title: "Server Error", text: "Got an error retrieving your " + (useRecipient ? "given" : "received") + @@ -405,7 +454,7 @@ export default class ContactsView extends Vue { { group: "alert", type: "danger", - title: "Error With Server", + title: "Server Error", text: error as string, }, -1, @@ -414,6 +463,18 @@ export default class ContactsView extends Vue { } async onClickNewContact(): Promise { + 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 name, publicKeyBase64; const commaPos1 = this.contactInput.indexOf(","); @@ -432,12 +493,74 @@ export default class ContactsView extends Vue { publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64"); } const newContact = { did, name, publicKeyBase64 }; - await db.contacts.add(newContact); - const allContacts = this.contacts.concat([newContact]); - this.contacts = R.sort( - (a: Contact, b) => (a.name || "").localeCompare(b.name || ""), - allContacts, - ); + return this.addContact(newContact); + } + + async newContactFromScan(url: string): Promise { + 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) { @@ -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 vcClaim: RegisterVerifiableCredential = { @@ -549,7 +682,7 @@ export default class ContactsView extends Vue { { group: "alert", type: "danger", - title: "Error With Server", + title: "Server Error", text: userMessage, }, -1, @@ -574,36 +707,30 @@ export default class ContactsView extends Vue { contact.seesMe = visibility; db.contacts.update(contact.did, { seesMe: visibility }); } else { - console.error("Bad response setting visibility: ", resp.data); - if (resp.data.error?.message) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Error With Server", - text: resp.data.error?.message, - }, - -1, - ); - } else { - this.$notify( - { - group: "alert", - type: "danger", - title: "Error With Server", - text: "Bad server response of " + resp.status, - }, - -1, - ); - } + console.error( + "Got some bad server response when setting visibility: ", + resp, + ); + const message = + resp.data.error?.message || "Bad server response of " + resp.status; + this.$notify( + { + group: "alert", + type: "danger", + title: "Server Error", + text: message, + }, + -1, + ); } } catch (err) { + console.error("Got some server error when setting visibility:", err); this.$notify( { group: "alert", type: "danger", - title: "Error With Server", - text: err as string, + title: "Server Error", + text: "Check connectivity and try again.", }, -1, ); @@ -628,7 +755,7 @@ export default class ContactsView extends Vue { this.$notify( { group: "alert", - type: "toast", + type: "info", title: "Refreshed", text: this.nameForContact(contact, true) + @@ -636,38 +763,29 @@ export default class ContactsView extends Vue { (visibility ? "" : "not ") + "see your activity.", }, - 5000, + -1, ); } else { - if (resp.data.error?.message) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Error With Server", - text: resp.data.error?.message, - }, - -1, - ); - } else { - this.$notify( - { - group: "alert", - type: "danger", - title: "Error With Server", - text: "Bad server response of " + resp.status, - }, - -1, - ); - } + console.log("Got bad server response when checking visibility: ", resp); + const message = resp.data.error?.message || "Got bad server response."; + this.$notify( + { + group: "alert", + type: "danger", + title: "Server Error", + text: message, + }, + -1, + ); } } catch (err) { + console.log("Caught error from server request to check visibility:", err); this.$notify( { group: "alert", type: "danger", - title: "Error With Server", - text: err as string, + title: "Server Error", + text: "Check connectivity and try again.", }, -1, ); @@ -867,7 +985,7 @@ export default class ContactsView extends Vue { { group: "alert", type: "danger", - title: "Error With Server", + title: "Server Error", text: userMessage, }, -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() { if (this.showGiveTotals) { this.showGiveTotals = false; @@ -900,6 +1030,26 @@ export default class ContactsView extends Vue {