forked from trent_larson/crowd-funder-for-time-pwa
Merge fixes
This commit is contained in:
30
CHANGELOG.md
30
CHANGELOG.md
@@ -6,9 +6,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
|
## [0.3.49] - 2025.01.09 - 36301ed238ff84df25bb11a8d44a295ee7eaf0f8
|
||||||
|
### Changed
|
||||||
|
- Make all external contact links direct to the contact-import page.
|
||||||
|
- Handle all new-single-contact JWTs in the contacts page, and multiple-contact JWTs in the contacts-import page.
|
||||||
|
|
||||||
|
|
||||||
|
## [0.3.48] - 2025.01.08 - 398f3e64a376789f7eb1c400cd886f5a2cacd588 (but app shows 07c4e58)
|
||||||
|
### Added
|
||||||
|
- More sanity-checks on contact-import JWT
|
||||||
|
|
||||||
|
|
||||||
|
## [0.3.47] - 2025.01.06 - 5bf6dd1ee32ca7cc46d39bd7afca58365b422f93
|
||||||
|
### Added
|
||||||
|
- Notes on contacts page with new contact-edit page
|
||||||
|
- Contact methods (only on contact-edit page and under DID details)
|
||||||
|
- DID view with no DID shows user's info.
|
||||||
|
### Changed
|
||||||
|
- URL for user's contact info is now URL to this app (not endorser.ch).
|
||||||
|
- Extended details (eg. full claim) is beneath details link on claim page.
|
||||||
|
|
||||||
|
|
||||||
|
## [0.3.46] - 2025.01.03 - 9e7056616b5e5acc51e5a8cf7354d408029fefb3
|
||||||
|
### Added
|
||||||
|
- More action-oriented questions for the gift prompts
|
||||||
|
### Fixed
|
||||||
|
- Contact-list import set visibility for all, even if not chosen.
|
||||||
|
|
||||||
|
|
||||||
## [0.3.45] - 2025.01.01 - 65402dc68ce69ccc6cb9aa8d2e7a9249bf4298e0
|
## [0.3.45] - 2025.01.01 - 65402dc68ce69ccc6cb9aa8d2e7a9249bf4298e0
|
||||||
### Fixed
|
### Fixed
|
||||||
- Previous project links stayed when following a link
|
- Previous project links stayed when following a link.
|
||||||
|
|
||||||
|
|
||||||
## [0.3.44] - 2024.12.31 - 694b22987b05482e4527c2478bbe15e6b6f3b532
|
## [0.3.44] - 2024.12.31 - 694b22987b05482e4527c2478bbe15e6b6f3b532
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -33,13 +33,10 @@ npm run serve
|
|||||||
npm run lint
|
npm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run all important tests
|
### Run all UI tests
|
||||||
|
|
||||||
... including automated UI tests (see below for details)
|
Look below for the "test-all" instructions.
|
||||||
|
|
||||||
```
|
|
||||||
npm run test-all
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and minify for test & production
|
### Compile and minify for test & production
|
||||||
|
|
||||||
@@ -92,11 +89,20 @@ Use the locally running Endorser server:
|
|||||||
|
|
||||||
* Clone and set up https://github.com/trentlarson/endorser-ch and run the following in that directory:
|
* Clone and set up https://github.com/trentlarson/endorser-ch and run the following in that directory:
|
||||||
```
|
```
|
||||||
|
npm install
|
||||||
test/test.sh
|
test/test.sh
|
||||||
|
cp .env.local .env
|
||||||
NODE_ENV=test-local npm run dev
|
NODE_ENV=test-local npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
* Now run the local tests:
|
If that fails, go to the README.md in the endorser-ch directory and follow the instructions there.
|
||||||
|
|
||||||
|
* Install playwright browsers:
|
||||||
|
```
|
||||||
|
npx playwright install
|
||||||
|
```
|
||||||
|
|
||||||
|
* Now you can run the local tests:
|
||||||
```
|
```
|
||||||
npm run test-all
|
npm run test-all
|
||||||
```
|
```
|
||||||
|
|||||||
714
package-lock.json
generated
714
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari",
|
||||||
"version": "0.3.46-beta",
|
"version": "0.3.51-beta",
|
||||||
"description": "A cross-platform app for managing time-based crowdfunding.",
|
"description": "A cross-platform app for managing time-based crowdfunding.",
|
||||||
"author": "Your Name <your.email@example.com>",
|
"author": "Your Name <your.email@example.com>",
|
||||||
"main": "src/electron/main.js",
|
"main": "src/electron/main.js",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default defineConfig({
|
|||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
baseURL: "http://localhost:8080",
|
baseURL: "http://localhost:8081",
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: "on-first-retry",
|
trace: "on-first-retry",
|
||||||
@@ -91,8 +91,8 @@ export default defineConfig({
|
|||||||
*/
|
*/
|
||||||
webServer: {
|
webServer: {
|
||||||
command:
|
command:
|
||||||
"VITE_APP_SERVER=http://localhost:8080 VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 VITE_PASSKEYS_ENABLED=true npm run dev",
|
"VITE_APP_SERVER=http://localhost:8081 VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 VITE_PASSKEYS_ENABLED=true npm run dev -- --port=8081",
|
||||||
url: "http://localhost:8080",
|
url: "http://localhost:8081",
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
15
src/App.vue
15
src/App.vue
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
|
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
|
||||||
<span class="font-semibold">{{ notification.title }}</span>
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
<p class="text-sm">{{ notification.text }}</p>
|
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="close(notification.id)"
|
@click="close(notification.id)"
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
|
|
||||||
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
|
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
|
||||||
<span class="font-semibold">{{ notification.title }}</span>
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
<p class="text-sm">{{ notification.text }}</p>
|
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="close(notification.id)"
|
@click="close(notification.id)"
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
|
|
||||||
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
|
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
|
||||||
<span class="font-semibold">{{ notification.title }}</span>
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
<p class="text-sm">{{ notification.text }}</p>
|
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="close(notification.id)"
|
@click="close(notification.id)"
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
|
|
||||||
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
|
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
|
||||||
<span class="font-semibold">{{ notification.title }}</span>
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
<p class="text-sm">{{ notification.text }}</p>
|
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="close(notification.id)"
|
@click="close(notification.id)"
|
||||||
@@ -340,6 +340,13 @@ export default class App extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
truncateLongWords(sentence: string) {
|
||||||
|
return sentence
|
||||||
|
.split(" ")
|
||||||
|
.map((word) => (word.length > 30 ? word.slice(0, 30) + "..." : word))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
beforeCreate() {
|
beforeCreate() {
|
||||||
console.log("Component beforeCreate: Instance initialized.");
|
console.log("Component beforeCreate: Instance initialized.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<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 relative">
|
<h1 class="text-xl font-bold text-center relative">
|
||||||
Here's one:
|
Here's one:
|
||||||
<div
|
<div
|
||||||
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
|
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
|
||||||
@@ -10,8 +10,9 @@
|
|||||||
<fa icon="xmark" class="w-[1em]"></fa>
|
<fa icon="xmark" class="w-[1em]"></fa>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<span class="flex justify-between">
|
<span class="mt-2 flex justify-between">
|
||||||
<span
|
<span
|
||||||
|
v-if="currentCategory === CATEGORY_IDEAS"
|
||||||
class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex"
|
class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex"
|
||||||
@click="prevIdea()"
|
@click="prevIdea()"
|
||||||
>
|
>
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
|
|
||||||
<div class="m-2">
|
<div class="m-2">
|
||||||
<span v-if="currentCategory === CATEGORY_IDEAS">
|
<span v-if="currentCategory === CATEGORY_IDEAS">
|
||||||
<p class="text-center text-lg font-bold">
|
<p class="text-center text-lg">
|
||||||
{{ IDEAS[currentIdeaIndex] }}
|
{{ IDEAS[currentIdeaIndex] }}
|
||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
@@ -28,12 +29,12 @@
|
|||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<span
|
<span
|
||||||
v-if="currentContact == null"
|
v-if="currentContact == null"
|
||||||
class="text-orange-500 text-lg font-bold"
|
class="text-orange-500 text-lg"
|
||||||
>
|
>
|
||||||
That's all your contacts.
|
That's all your contacts.
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<span class="text-lg font-bold">
|
<span class="text-lg">
|
||||||
Did {{ currentContact.name || AppString.NO_CONTACT_NAME }}
|
Did {{ currentContact.name || AppString.NO_CONTACT_NAME }}
|
||||||
<br />
|
<br />
|
||||||
or someone near them do anything – maybe a while ago?
|
or someone near them do anything – maybe a while ago?
|
||||||
@@ -85,21 +86,22 @@ export default class GivenPrompts extends Vue {
|
|||||||
CATEGORY_CONTACTS = 1;
|
CATEGORY_CONTACTS = 1;
|
||||||
CATEGORY_IDEAS = 0;
|
CATEGORY_IDEAS = 0;
|
||||||
IDEAS = [
|
IDEAS = [
|
||||||
"What food did someone fix for you?",
|
"What food did someone make? (How did it free up your time for something? Was something doable because it eased your stress?)",
|
||||||
"What did a family member do for you?",
|
"What did a family member do? (How did you take better action because it made you feel loved?)",
|
||||||
"What compliment did someone give you?",
|
"What compliment did someone give you? (What task could you tackle because it boosted your confidence?)",
|
||||||
"Who is someone you can always rely on, and how did they demonstrate that?",
|
"Who is someone you can always rely on, and how did they demonstrate that? (What project tasks were enabled because you could depend on them?)",
|
||||||
"What did you see someone give to someone else?",
|
"What did you see someone give to someone else? (What is the effect of the positivity you gained from seeing that?)",
|
||||||
"What is a way that someone helped you even though you have never met?",
|
"What is a way that someone helped you even though you have never met? (What different action did you take due to that newfound perspective or inspiration?)",
|
||||||
"How did a musician or author or artist inspire you?",
|
"How did a musician or author or artist inspire you? (What were you motivated to do more creatively because of that?)",
|
||||||
"What inspiration did you get from someone who handled tragedy well?",
|
"What inspiration did you get from someone who handled tragedy well? (What could you accomplish with better grace or resilience after seeing their example?)",
|
||||||
"What is something worth respect that an organization gave you?",
|
"What is something worth respect that an organization gave you? (How did their contribution improve the situation or enable new activities?)",
|
||||||
"Who last gave you a good laugh?",
|
"Who last gave you a good laugh? (What kind of bond or revitalization did that bring to a situation?)",
|
||||||
"What do you recall someone giving you while you were young?",
|
"What do you recall someone giving you while you were young? (How did it bring excitement or teach a skill or ignite a passion that resulted in improvements in your life?)",
|
||||||
"Who forgave you or overlooked a mistake?",
|
"Who forgave you or overlooked a mistake? (How did that free you or build trust that enabled better relationships?)",
|
||||||
"What is a way an ancestor contributed to your life?",
|
"What is a way an ancestor contributed to your life? (What in your life is now possible because of their efforts? What challenges are you undertaking knowing of their lives?)",
|
||||||
"What kind of help did someone at work give you?",
|
"What kind of help did someone at work give you? (How did that help with team progress? How did that lift your professional growth?)",
|
||||||
"How did a teacher or mentor or great example help you?",
|
"How did a teacher or mentor or great example help you? (How did their guidance enhance your attitude or actions?)",
|
||||||
|
"What is a surprise gift you received? (What extra possibilities did it give you?)",
|
||||||
];
|
];
|
||||||
|
|
||||||
callbackOnFullGiftInfo?: (
|
callbackOnFullGiftInfo?: (
|
||||||
@@ -116,9 +118,9 @@ export default class GivenPrompts extends Vue {
|
|||||||
AppString = AppString;
|
AppString = AppString;
|
||||||
|
|
||||||
async open(
|
async open(
|
||||||
callbackOnFullGiftInfo: (
|
callbackOnFullGiftInfo?: (
|
||||||
contactInfo: GiverReceiverInputInfo,
|
contactInfo?: GiverReceiverInputInfo,
|
||||||
description: string,
|
description?: string,
|
||||||
) => void,
|
) => void,
|
||||||
) {
|
) {
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="text-center mt-8">
|
<div class="text-center mt-8">
|
||||||
<div class>
|
<div>
|
||||||
<fa
|
<fa
|
||||||
icon="camera"
|
icon="camera"
|
||||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
|
export interface ContactMethod {
|
||||||
|
label: string;
|
||||||
|
type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Contact {
|
export interface Contact {
|
||||||
|
//
|
||||||
|
// When adding a property, consider whether it should be added when exporting & sharing contacts.
|
||||||
|
|
||||||
did: string;
|
did: string;
|
||||||
|
contactMethods?: Array<ContactMethod>;
|
||||||
name?: string;
|
name?: string;
|
||||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
||||||
|
notes?: string;
|
||||||
profileImageUrl?: string;
|
profileImageUrl?: string;
|
||||||
publicKeyBase64?: string;
|
publicKeyBase64?: string;
|
||||||
seesMe?: boolean; // cached value of the server setting
|
seesMe?: boolean; // cached value of the server setting
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
|
|||||||
import { HDNode } from "@ethersproject/hdnode";
|
import { HDNode } from "@ethersproject/hdnode";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
createEndorserJwtForDid,
|
createEndorserJwtForDid,
|
||||||
ENDORSER_JWT_URL_LOCATION,
|
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
||||||
|
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
||||||
} from "../../libs/endorserServer";
|
} from "../../libs/endorserServer";
|
||||||
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
||||||
import { decodeEndorserJwt } from "../../libs/crypto/vc";
|
|
||||||
|
|
||||||
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
|
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
|
||||||
|
|
||||||
@@ -101,24 +102,34 @@ export const accessToken = async (did?: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@return results of uportJwtPayload:
|
@return payload of JWT pulled out of any recognized URL path (if any)
|
||||||
{ 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) => {
|
export const getContactJwtFromJwtUrl = (jwtUrlText: string) => {
|
||||||
let jwtText = jwtUrlText;
|
let jwtText = jwtUrlText;
|
||||||
const endorserContextLoc = jwtText.indexOf(ENDORSER_JWT_URL_LOCATION);
|
const appImportConfirmUrlLoc = jwtText.indexOf(
|
||||||
if (endorserContextLoc > -1) {
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
|
);
|
||||||
|
if (appImportConfirmUrlLoc > -1) {
|
||||||
jwtText = jwtText.substring(
|
jwtText = jwtText.substring(
|
||||||
endorserContextLoc + ENDORSER_JWT_URL_LOCATION.length,
|
appImportConfirmUrlLoc +
|
||||||
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI.length,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const appImportOneUrlLoc = jwtText.indexOf(
|
||||||
// JWT format: { header, payload, signature, data }
|
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
||||||
const jwt = decodeEndorserJwt(jwtText);
|
);
|
||||||
|
if (appImportOneUrlLoc > -1) {
|
||||||
return jwt.payload;
|
jwtText = jwtText.substring(
|
||||||
|
appImportOneUrlLoc + CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const endorserUrlPathLoc = jwtText.indexOf(CONTACT_URL_PATH_ENDORSER_CH_OLD);
|
||||||
|
if (endorserUrlPathLoc > -1) {
|
||||||
|
jwtText = jwtText.substring(
|
||||||
|
endorserUrlPathLoc + CONTACT_URL_PATH_ENDORSER_CH_OLD.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return jwtText;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const nextDerivationPath = (origDerivPath: string) => {
|
export const nextDerivationPath = (origDerivPath: string) => {
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ function bytesToHex(b: Uint8Array): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We should be calling 'verify' in more places, showing warnings if it fails.
|
// We should be calling 'verify' in more places, showing warnings if it fails.
|
||||||
// @returns JWTDecoded with { header: JWTHeader, payload: string, signature: string, data: string } (but doesn't verify the signature)
|
// @returns JWTDecoded with { header: JWTHeader, payload: any, signature: string, data: string } (but doesn't verify the signature)
|
||||||
export function decodeEndorserJwt(jwt: string) {
|
export function decodeEndorserJwt(jwt: string) {
|
||||||
return didJwt.decodeJWT(jwt);
|
return didJwt.decodeJWT(jwt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
|||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import { accessToken, deriveAddress, nextDerivationPath } from "../libs/crypto";
|
import { accessToken, deriveAddress, nextDerivationPath } from "../libs/crypto";
|
||||||
import { logConsoleAndDb, NonsensitiveDexie } from "../db/index";
|
import { logConsoleAndDb, NonsensitiveDexie } from "../db/index";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
retrieveAccountMetadata,
|
retrieveAccountMetadata,
|
||||||
retrieveFullyDecryptedAccount,
|
retrieveFullyDecryptedAccount,
|
||||||
@@ -22,10 +23,14 @@ export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
|||||||
export const SERVICE_ID = "endorser.ch";
|
export const SERVICE_ID = "endorser.ch";
|
||||||
// the header line for contacts exported via Endorser Mobile
|
// the header line for contacts exported via Endorser Mobile
|
||||||
export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered";
|
export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered";
|
||||||
// the prefix for the contact URL
|
// the suffix for the contact URL in this app where they are confirmed before import
|
||||||
export const CONTACT_URL_PREFIX = "https://endorser.ch";
|
export const CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI = "/contact-import/";
|
||||||
// the suffix for the contact URL
|
// the suffix for the contact URL in this app where a single one gets imported automatically
|
||||||
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt=";
|
export const CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI = "/contacts?contactJwt=";
|
||||||
|
// the suffix for the old contact URL -- deprecated Jan 2025, though "endorser.ch/contact?jwt=" shows data on endorser.ch server
|
||||||
|
export const CONTACT_URL_PATH_ENDORSER_CH_OLD = "/contact?jwt=";
|
||||||
|
// unused now that we match on the URL path; just note that it was used for a while to create URLs that showed at endorser.ch
|
||||||
|
//export const CONTACT_URL_PREFIX_ENDORSER_CH_OLD = "https://endorser.ch";
|
||||||
// the prefix for handle IDs, the permanent ID for claims on Endorser
|
// the prefix for handle IDs, the permanent ID for claims on Endorser
|
||||||
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
|
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
|
||||||
|
|
||||||
@@ -286,7 +291,12 @@ export interface ErrorResult extends ResultWithType {
|
|||||||
|
|
||||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is similar to Contact but it grew up in different logic paths.
|
||||||
|
* We may want to change this to be a Contact.
|
||||||
|
*/
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
|
did: string;
|
||||||
name: string;
|
name: string;
|
||||||
publicEncKey: string;
|
publicEncKey: string;
|
||||||
registered: boolean;
|
registered: boolean;
|
||||||
@@ -601,7 +611,17 @@ const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
|
|||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function errorStringForLog(error: any) {
|
export function errorStringForLog(error: any) {
|
||||||
let fullError = "" + error + " - JSON: " + JSON.stringify(error);
|
let stringifiedError = "" + error;
|
||||||
|
try {
|
||||||
|
stringifiedError = JSON.stringify(error);
|
||||||
|
} catch (e) {
|
||||||
|
// can happen with Dexie, eg:
|
||||||
|
// TypeError: Converting circular structure to JSON
|
||||||
|
// --> starting at object with constructor 'DexieError2'
|
||||||
|
// | property '_promise' -> object with constructor 'DexiePromise'
|
||||||
|
// --- property '_value' closes the circle
|
||||||
|
}
|
||||||
|
let fullError = "" + error + " - JSON: " + stringifiedError;
|
||||||
const errorResponseText = JSON.stringify(error.response);
|
const errorResponseText = JSON.stringify(error.response);
|
||||||
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
|
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
|
||||||
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
|
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
|
||||||
@@ -692,7 +712,6 @@ export async function getNewOffersToUser(
|
|||||||
url += "&beforeId=" + beforeOfferJwtId;
|
url += "&beforeId=" + beforeOfferJwtId;
|
||||||
}
|
}
|
||||||
const headers = await getHeaders(activeDid);
|
const headers = await getHeaders(activeDid);
|
||||||
console.log("Using headers: ", headers);
|
|
||||||
const response = await axios.get(url, { headers });
|
const response = await axios.get(url, { headers });
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
@@ -1090,7 +1109,7 @@ export async function createAndSubmitClaim(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateEndorserJwtForAccount(
|
export async function generateEndorserJwtUrlForAccount(
|
||||||
account: Account,
|
account: Account,
|
||||||
isRegistered?: boolean,
|
isRegistered?: boolean,
|
||||||
name?: string,
|
name?: string,
|
||||||
@@ -1105,6 +1124,7 @@ export async function generateEndorserJwtForAccount(
|
|||||||
iat: Date.now(),
|
iat: Date.now(),
|
||||||
iss: account.did,
|
iss: account.did,
|
||||||
own: {
|
own: {
|
||||||
|
did: account.did,
|
||||||
name: name ?? "",
|
name: name ?? "",
|
||||||
publicEncKey,
|
publicEncKey,
|
||||||
registered: !!isRegistered,
|
registered: !!isRegistered,
|
||||||
@@ -1130,7 +1150,7 @@ export async function generateEndorserJwtForAccount(
|
|||||||
|
|
||||||
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
|
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
|
||||||
|
|
||||||
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
|
const viewPrefix = APP_SERVER + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
|
||||||
return viewPrefix + vcJwt;
|
return viewPrefix + vcJwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
faBurst,
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faCamera,
|
faCamera,
|
||||||
|
faCaretDown,
|
||||||
faCheck,
|
faCheck,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
@@ -98,6 +99,7 @@ library.add(
|
|||||||
faBurst,
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faCamera,
|
faCamera,
|
||||||
|
faCaretDown,
|
||||||
faCheck,
|
faCheck,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "contact-amounts",
|
name: "contact-amounts",
|
||||||
component: () => import("../views/ContactAmountsView.vue"),
|
component: () => import("../views/ContactAmountsView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/contact-edit/:did",
|
||||||
|
name: "contact-edit",
|
||||||
|
component: () => import("../views/ContactEditView.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/contact-gift",
|
path: "/contact-gift",
|
||||||
name: "contact-gift",
|
name: "contact-gift",
|
||||||
|
|||||||
@@ -936,7 +936,7 @@ export default class AccountViewView extends Vue {
|
|||||||
title: "Error Loading Profile",
|
title: "Error Loading Profile",
|
||||||
text: "See the Help page about errors with your personal data.",
|
text: "See the Help page about errors with your personal data.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1339,7 +1339,7 @@ export default class AccountViewView extends Vue {
|
|||||||
title: "Export Error",
|
title: "Export Error",
|
||||||
text: "There was an error exporting the data.",
|
text: "There was an error exporting the data.",
|
||||||
},
|
},
|
||||||
-1,
|
3000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1475,7 +1475,7 @@ export default class AccountViewView extends Vue {
|
|||||||
title: "Update Error",
|
title: "Update Error",
|
||||||
text: "Unable to update your settings. Check claim limits again.",
|
text: "Unable to update your settings. Check claim limits again.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1540,7 +1540,7 @@ export default class AccountViewView extends Vue {
|
|||||||
title: "Reload",
|
title: "Reload",
|
||||||
text: "Now reload the app to get a new VAPID to use with this push server.",
|
text: "Now reload the app to get a new VAPID to use with this push server.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1598,7 +1598,7 @@ export default class AccountViewView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "There was a problem deleting the image. Contact support if you want it removed from the servers.",
|
text: "There was a problem deleting the image. Contact support if you want it removed from the servers.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
// keep the imageUrl in localStorage so the user can try again if they want
|
// keep the imageUrl in localStorage so the user can try again if they want
|
||||||
}
|
}
|
||||||
@@ -1630,7 +1630,7 @@ export default class AccountViewView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "There was an error deleting the image.",
|
text: "There was an error deleting the image.",
|
||||||
},
|
},
|
||||||
5000,
|
3000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export default class ClaimAddRawView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "There was a problem submitting the claim.",
|
text: "There was a problem submitting the claim.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,7 +181,6 @@ export default class ClaimCertificateView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw claim issuer
|
// Draw claim issuer
|
||||||
console.log("claimData.issuer", claimData.issuer);
|
|
||||||
if (
|
if (
|
||||||
claimData.issuer == null ||
|
claimData.issuer == null ||
|
||||||
serverUtil.isHiddenDid(claimData.issuer) ||
|
serverUtil.isHiddenDid(claimData.issuer) ||
|
||||||
|
|||||||
@@ -345,8 +345,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Note that a similar section is found in ConfirmGiftView.vue -->
|
<!-- Note that a similar section is found in ConfirmGiftView.vue -->
|
||||||
<div>
|
<h2
|
||||||
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Details</h2>
|
class="font-bold uppercase text-xl text-blue-500 mt-8 cursor-pointer"
|
||||||
|
@click="showVeriClaimDump = !showVeriClaimDump"
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
<fa v-if="showVeriClaimDump" icon="chevron-up" />
|
||||||
|
<fa v-else icon="chevron-right" />
|
||||||
|
</h2>
|
||||||
|
<div v-if="showVeriClaimDump">
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
serverUtil.containsHiddenDid(veriClaim) &&
|
serverUtil.containsHiddenDid(veriClaim) &&
|
||||||
@@ -448,50 +455,48 @@
|
|||||||
This record is an edited version. The latest version is here.
|
This record is an edited version. The latest version is here.
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<button @click="showVeriClaimDump = !showVeriClaimDump" class="ml-2">
|
|
||||||
Details
|
|
||||||
<fa v-if="showVeriClaimDump" icon="chevron-up" class="text-blue-400" />
|
|
||||||
<fa v-else icon="chevron-down" class="text-blue-400" />
|
|
||||||
</button>
|
|
||||||
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
||||||
<pre
|
<pre
|
||||||
v-if="showVeriClaimDump"
|
v-if="showVeriClaimDump"
|
||||||
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
|
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
|
||||||
>{{ veriClaimDump }}</pre
|
>{{ veriClaimDump }}</pre
|
||||||
>
|
>
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-xl mt-8 mb-2">Full Claim</h2>
|
<h2 class="text-xl mt-8 mb-2">Full Claim</h2>
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
The full claim includes the claim as it was originally issued, including
|
The full claim includes the claim as it was originally issued, including
|
||||||
the signature (ie. the proof of issuance by that person).
|
the signature (ie. the proof of issuance by that person).
|
||||||
</p>
|
|
||||||
<div v-if="!fullClaim">
|
|
||||||
<p v-if="fullClaimMessage" class="mb-4">
|
|
||||||
{{ fullClaimMessage }}
|
|
||||||
</p>
|
</p>
|
||||||
<button
|
<div v-if="!fullClaim">
|
||||||
v-else
|
<p v-if="fullClaimMessage" class="mb-4">
|
||||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2"
|
{{ fullClaimMessage }}
|
||||||
@click="showFullClaim(veriClaim.id as string)"
|
</p>
|
||||||
>
|
<button
|
||||||
Load Full Claim Details
|
v-else
|
||||||
</button>
|
class="text-blue-500 cursor-pointer"
|
||||||
</div>
|
@click="showFullClaim(veriClaim.id as string)"
|
||||||
<div v-else>
|
>
|
||||||
<pre
|
<fa icon="file-lines" class="fa-fw" />
|
||||||
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
|
Load Full Claim Details
|
||||||
>{{ fullClaimDump }}</pre
|
</button>
|
||||||
>
|
</div>
|
||||||
</div>
|
<div v-else>
|
||||||
|
<pre
|
||||||
|
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
|
||||||
|
>{{ fullClaimDump }}</pre
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
:href="apiServer + '/api/claim/' + veriClaim.id"
|
:href="apiServer + '/api/claim/' + veriClaim.id"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2"
|
class="text-blue-500 cursor-pointer"
|
||||||
>
|
>
|
||||||
View on the Public Server
|
<fa icon="file-lines" class="fa-fw" />
|
||||||
</a>
|
<fa icon="arrow-up-right-from-square" class="ml-1 fa-fw" />
|
||||||
|
View on the Public Server
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -601,7 +606,7 @@ export default class ClaimView extends Vue {
|
|||||||
title: "Error Loading Profile",
|
title: "Error Loading Profile",
|
||||||
text: "See the Help page for problems with your personal data.",
|
text: "See the Help page for problems with your personal data.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,7 +623,7 @@ export default class ClaimView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "No claim ID was provided.",
|
text: "No claim ID was provided.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,7 +685,7 @@ export default class ClaimView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "There was a problem retrieving that claim.",
|
text: "There was a problem retrieving that claim.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -727,7 +732,7 @@ export default class ClaimView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Got error retrieving linked provider data.",
|
text: "Got error retrieving linked provider data.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (this.veriClaim.claimType === "Offer") {
|
} else if (this.veriClaim.claimType === "Offer") {
|
||||||
@@ -750,7 +755,7 @@ export default class ClaimView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Got error retrieving linked offer data.",
|
text: "Got error retrieving linked offer data.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -805,7 +810,7 @@ export default class ClaimView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "There was a problem getting that claim.",
|
text: "There was a problem getting that claim.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@@ -849,7 +854,7 @@ export default class ClaimView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Something went wrong retrieving that claim.",
|
text: "Something went wrong retrieving that claim.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -912,7 +917,7 @@ export default class ClaimView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "There was a problem submitting the confirmation.",
|
text: "There was a problem submitting the confirmation.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,12 +256,12 @@
|
|||||||
|
|
||||||
<!-- Note that a similar section is found in ClaimView.vue -->
|
<!-- Note that a similar section is found in ClaimView.vue -->
|
||||||
<h2
|
<h2
|
||||||
class="font-bold uppercase text-xl text-blue-500 mt-8 mb-2 cursor-pointer"
|
class="font-bold uppercase text-xl text-blue-500 mt-8 cursor-pointer"
|
||||||
@click="showVeriClaimDump = !showVeriClaimDump"
|
@click="showVeriClaimDump = !showVeriClaimDump"
|
||||||
>
|
>
|
||||||
Details
|
Details
|
||||||
<span v-if="!showVeriClaimDump"><fa icon="chevron-down" /></span>
|
<fa v-if="showVeriClaimDump" icon="chevron-up" />
|
||||||
<span v-else><fa icon="chevron-up" /></span>
|
<fa v-else icon="chevron-right" />
|
||||||
</h2>
|
</h2>
|
||||||
<div v-if="showVeriClaimDump">
|
<div v-if="showVeriClaimDump">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
err.userMessage ||
|
err.userMessage ||
|
||||||
"There was an error retrieving your settings or contacts or gives.",
|
"There was an error retrieving your settings or contacts or gives.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,7 +196,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
title: "Error With Server",
|
title: "Error With Server",
|
||||||
text: "Got an error retrieving your given time from the server.",
|
text: "Got an error retrieving your given time from the server.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
title: "Error With Server",
|
title: "Error With Server",
|
||||||
text: "Got an error retrieving your given time from the server.",
|
text: "Got an error retrieving your given time from the server.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
title: "Error With Server",
|
title: "Error With Server",
|
||||||
text: error as string,
|
text: error as string,
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
title: "Error With Server",
|
title: "Error With Server",
|
||||||
text: userMessage,
|
text: userMessage,
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,7 +310,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
title: "Not Allowed",
|
title: "Not Allowed",
|
||||||
text: "Only the recipient can confirm final receipt.",
|
text: "Only the recipient can confirm final receipt.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
238
src/views/ContactEditView.vue
Normal file
238
src/views/ContactEditView.vue
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav selected="Contacts" />
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
|
<section id="ContactEdit" class="p-6 max-w-3xl mx-auto">
|
||||||
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
|
<h1 class="text-4xl text-center font-light relative px-7">
|
||||||
|
<!-- Back -->
|
||||||
|
<button
|
||||||
|
@click="$router.go(-1)"
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Name -->
|
||||||
|
<div class="mt-4 flex" data-testId="contactName">
|
||||||
|
<label
|
||||||
|
for="contactName"
|
||||||
|
class="block text-sm font-medium text-gray-700 mt-2"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-full ml-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
v-model="contactName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Notes -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="contactNotes" class="block text-sm font-medium text-gray-700">
|
||||||
|
Notes
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="contactNotes"
|
||||||
|
rows="4"
|
||||||
|
class="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
v-model="contactNotes"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Methods -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<h2 class="text-lg font-medium text-gray-700">Contact Methods</h2>
|
||||||
|
<div
|
||||||
|
v-for="(method, index) in contactMethods"
|
||||||
|
:key="index"
|
||||||
|
class="flex mt-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="method.label"
|
||||||
|
class="block w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
placeholder="Label"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="method.type"
|
||||||
|
class="block ml-2 w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
placeholder="Type"
|
||||||
|
/>
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
@click="toggleDropdown(index)"
|
||||||
|
class="px-2 py-1 bg-gray-200 rounded-md"
|
||||||
|
>
|
||||||
|
<fa icon="caret-down" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="dropdownIndex === index"
|
||||||
|
class="absolute bg-white border border-gray-300 rounded-md mt-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
@click="setMethodType(index, 'CELL')"
|
||||||
|
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
CELL
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
@click="setMethodType(index, 'EMAIL')"
|
||||||
|
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
EMAIL
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
@click="setMethodType(index, 'WHATSAPP')"
|
||||||
|
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
WHATSAPP
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="method.value"
|
||||||
|
class="block ml-2 w-1/2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
placeholder="Number, email, etc."
|
||||||
|
/>
|
||||||
|
<button @click="removeContactMethod(index)" class="ml-2 text-red-500">
|
||||||
|
<fa icon="trash-can" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="addContactMethod" class="mt-2">
|
||||||
|
<fa
|
||||||
|
icon="plus"
|
||||||
|
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Save Button -->
|
||||||
|
<div class="mt-8 flex justify-between">
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 bg-blue-500 text-white rounded-md"
|
||||||
|
@click="saveEdit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ml-4 px-4 py-2 bg-slate-500 text-white rounded-md"
|
||||||
|
@click="$router.go(-1)"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import * as R from "ramda";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { RouteLocation, Router } from "vue-router";
|
||||||
|
|
||||||
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
|
import { AppString, NotificationIface } from "../constants/app";
|
||||||
|
import { db } from "../db/index";
|
||||||
|
import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
QuickNav,
|
||||||
|
TopMessage,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class ContactEditView extends Vue {
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
contact: Contact = {
|
||||||
|
did: "",
|
||||||
|
name: "",
|
||||||
|
notes: "",
|
||||||
|
};
|
||||||
|
contactName = "";
|
||||||
|
contactNotes = "";
|
||||||
|
contactMethods: Array<ContactMethod> = [];
|
||||||
|
dropdownIndex: number | null = null;
|
||||||
|
|
||||||
|
AppString = AppString;
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
const contactDid = (this.$route as RouteLocation).params.did;
|
||||||
|
const contact = await db.contacts.get(contactDid || "");
|
||||||
|
if (contact) {
|
||||||
|
this.contact = contact;
|
||||||
|
this.contactName = contact.name || "";
|
||||||
|
this.contactNotes = contact.notes || "";
|
||||||
|
this.contactMethods = contact.contactMethods || [];
|
||||||
|
} else {
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Contact Not Found",
|
||||||
|
text: "There is no contact with DID " + contactDid,
|
||||||
|
});
|
||||||
|
(this.$router as Router).push({ path: "/contacts" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addContactMethod() {
|
||||||
|
this.contactMethods.push({ label: "", type: "", value: "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
removeContactMethod(index: number) {
|
||||||
|
this.contactMethods.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDropdown(index: number) {
|
||||||
|
this.dropdownIndex = this.dropdownIndex === index ? null : index;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMethodType(index: number, type: string) {
|
||||||
|
this.contactMethods[index].type = type;
|
||||||
|
this.dropdownIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveEdit() {
|
||||||
|
// without this conversion, "Failed to execute 'put' on 'IDBObjectStore': [object Array] could not be cloned."
|
||||||
|
const contactMethodsObj = JSON.parse(JSON.stringify(this.contactMethods));
|
||||||
|
const contactMethods = contactMethodsObj.map((method: ContactMethod) =>
|
||||||
|
R.set(R.lensProp("type"), method.type.toUpperCase(), method),
|
||||||
|
);
|
||||||
|
if (!R.equals(contactMethodsObj, contactMethods)) {
|
||||||
|
this.contactMethods = contactMethods;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Contact Methods Updated",
|
||||||
|
text: "Note that some methods have been updated, such as uppercasing 'email' to 'EMAIL'. Save again if the changes are acceptable.",
|
||||||
|
},
|
||||||
|
15000,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await db.contacts.update(this.contact.did, {
|
||||||
|
name: this.contactName,
|
||||||
|
notes: this.contactNotes,
|
||||||
|
contactMethods: contactMethods,
|
||||||
|
});
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Contact Saved",
|
||||||
|
text: "The contact info has been updated successfully.",
|
||||||
|
});
|
||||||
|
(this.$router as Router).push({
|
||||||
|
path: "/did/" + encodeURIComponent(this.contact.did),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -16,96 +16,134 @@
|
|||||||
Contact Import
|
Contact Import
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<span
|
<div v-if="checkingImports" class="text-center">
|
||||||
v-if="contactsImporting.length > sameCount"
|
<fa icon="spinner" class="animate-spin" />
|
||||||
class="flex justify-center"
|
|
||||||
>
|
|
||||||
<input type="checkbox" v-model="makeVisible" class="mr-2" />
|
|
||||||
Make my activity visible to these contacts.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div v-if="sameCount > 0">
|
|
||||||
<span v-if="sameCount == 1"
|
|
||||||
>One contact is the same as an existing contact</span
|
|
||||||
>
|
|
||||||
<span v-else
|
|
||||||
>{{ sameCount }} contacts are the same as existing contacts</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<span
|
||||||
|
v-if="contactsImporting.length > sameCount"
|
||||||
|
class="flex justify-center"
|
||||||
|
>
|
||||||
|
<input type="checkbox" v-model="makeVisible" class="mr-2" />
|
||||||
|
Make my activity visible to these contacts.
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- Results List -->
|
<div v-if="sameCount > 0">
|
||||||
<ul
|
<span v-if="sameCount == 1"
|
||||||
v-if="contactsImporting.length > sameCount"
|
>One contact is the same as an existing contact</span
|
||||||
class="border-t border-slate-300"
|
|
||||||
>
|
|
||||||
<li v-for="(contact, index) in contactsImporting" :key="contact.did">
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
!contactsExisting[contact.did] ||
|
|
||||||
!R.isEmpty(contactDifferences[contact.did])
|
|
||||||
"
|
|
||||||
class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4"
|
|
||||||
>
|
>
|
||||||
<h2 class="text-base font-semibold">
|
<span v-else
|
||||||
<input type="checkbox" v-model="contactsSelected[index]" />
|
>{{ sameCount }} contacts are the same as existing contacts</span
|
||||||
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
>
|
||||||
-
|
</div>
|
||||||
<span v-if="contactsExisting[contact.did]" class="text-orange-500"
|
|
||||||
>Existing</span
|
<!-- Results List -->
|
||||||
>
|
<ul
|
||||||
<span v-else class="text-green-500">New</span>
|
v-if="contactsImporting.length > sameCount"
|
||||||
</h2>
|
class="border-t border-slate-300"
|
||||||
<div class="text-sm truncate">
|
>
|
||||||
{{ contact.did }}
|
<li v-for="(contact, index) in contactsImporting" :key="contact.did">
|
||||||
</div>
|
<div
|
||||||
<div v-if="contactDifferences[contact.did]">
|
v-if="
|
||||||
<div>
|
!contactsExisting[contact.did] ||
|
||||||
<div class="grid grid-cols-3 gap-2">
|
!R.isEmpty(contactDifferences[contact.did])
|
||||||
<div class="font-bold">Field</div>
|
"
|
||||||
<div class="font-bold">Old Value</div>
|
class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4"
|
||||||
<div class="font-bold">New Value</div>
|
>
|
||||||
</div>
|
<h2 class="text-base font-semibold">
|
||||||
<div
|
<input type="checkbox" v-model="contactsSelected[index]" />
|
||||||
v-for="(value, contactField) in contactDifferences[contact.did]"
|
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
||||||
:key="contactField"
|
-
|
||||||
class="grid grid-cols-3 border"
|
<span v-if="contactsExisting[contact.did]" class="text-orange-500"
|
||||||
|
>Existing</span
|
||||||
>
|
>
|
||||||
<div class="border p-1">{{ contactField }}</div>
|
<span v-else class="text-green-500">New</span>
|
||||||
<div class="border p-1">{{ value.old }}</div>
|
</h2>
|
||||||
<div class="border p-1">{{ value.new }}</div>
|
<div class="text-sm truncate">
|
||||||
|
{{ contact.did }}
|
||||||
|
</div>
|
||||||
|
<div v-if="contactDifferences[contact.did]">
|
||||||
|
<div>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<div></div>
|
||||||
|
<div class="font-bold">Old Value</div>
|
||||||
|
<div class="font-bold">New Value</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(value, contactField) in contactDifferences[
|
||||||
|
contact.did
|
||||||
|
]"
|
||||||
|
:key="contactField"
|
||||||
|
class="grid grid-cols-3 border"
|
||||||
|
>
|
||||||
|
<div class="border font-bold p-1">
|
||||||
|
{{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
|
||||||
|
</div>
|
||||||
|
<div class="border p-1">{{ value.old }}</div>
|
||||||
|
<div class="border p-1">{{ value.new }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</li>
|
||||||
|
<button
|
||||||
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-sm text-white mt-2 px-2 py-1.5 rounded"
|
||||||
|
@click="importContacts"
|
||||||
|
>
|
||||||
|
Import Selected Contacts
|
||||||
|
</button>
|
||||||
|
</ul>
|
||||||
|
<p v-else-if="contactsImporting.length > 0">
|
||||||
|
All those contacts are already in your list with the same information.
|
||||||
|
</p>
|
||||||
|
<div v-else>
|
||||||
|
There are no contacts in that import. If some were sent, try again to
|
||||||
|
get the full text and paste it. (Note that iOS cuts off data in text
|
||||||
|
messages.) Ask the person to send the data a different way, eg. email.
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<textarea
|
||||||
|
v-model="inputJwt"
|
||||||
|
placeholder="Contact-import data"
|
||||||
|
class="mt-4 border-2 border-gray-300 p-2 rounded"
|
||||||
|
cols="30"
|
||||||
|
@input="() => checkContactJwt(inputJwt)"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<button
|
||||||
|
@click="() => processContactJwt(inputJwt)"
|
||||||
|
class="ml-2 p-2 bg-blue-500 text-white rounded"
|
||||||
|
>
|
||||||
|
Check Import
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
<fa icon="spinner" v-if="importing" class="animate-spin" />
|
</div>
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-sm text-white mt-2 px-2 py-1.5 rounded"
|
|
||||||
@click="importContacts"
|
|
||||||
>
|
|
||||||
Import Selected Contacts
|
|
||||||
</button>
|
|
||||||
</ul>
|
|
||||||
<p v-else>There are no contacts to import.</p>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { JWTVerified } from "did-jwt";
|
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
|
|
||||||
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 OfferDialog from "../components/OfferDialog.vue";
|
import OfferDialog from "../components/OfferDialog.vue";
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import {
|
||||||
import { Contact } from "../db/tables/contacts";
|
db,
|
||||||
|
logConsoleAndDb,
|
||||||
|
retrieveSettingsForActiveAccount,
|
||||||
|
} from "../db/index";
|
||||||
|
import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import { decodeAndVerifyJwt } from "../libs/crypto/vc/index";
|
import {
|
||||||
import { setVisibilityUtil } from "../libs/endorserServer";
|
capitalizeAndInsertSpacesBeforeCaps,
|
||||||
|
errorStringForLog,
|
||||||
|
setVisibilityUtil,
|
||||||
|
} from "../libs/endorserServer";
|
||||||
|
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
||||||
|
import { decodeEndorserJwt } from "../libs/crypto/vc";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { EntityIcon, OfferDialog, QuickNav },
|
components: { EntityIcon, OfferDialog, QuickNav },
|
||||||
@@ -114,6 +152,7 @@ export default class ContactImportView extends Vue {
|
|||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
AppString = AppString;
|
AppString = AppString;
|
||||||
|
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
R = R;
|
R = R;
|
||||||
|
|
||||||
@@ -124,9 +163,16 @@ export default class ContactImportView extends Vue {
|
|||||||
contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected
|
contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected
|
||||||
contactDifferences: Record<
|
contactDifferences: Record<
|
||||||
string,
|
string,
|
||||||
Record<string, { new: string; old: string }>
|
Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
new: string | boolean | Array<ContactMethod> | undefined;
|
||||||
|
old: string | boolean | Array<ContactMethod> | undefined;
|
||||||
|
}
|
||||||
|
>
|
||||||
> = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key
|
> = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key
|
||||||
importing = false;
|
checkingImports = false;
|
||||||
|
inputJwt: string = "";
|
||||||
makeVisible = true;
|
makeVisible = true;
|
||||||
sameCount = 0;
|
sameCount = 0;
|
||||||
|
|
||||||
@@ -135,23 +181,48 @@ export default class ContactImportView extends Vue {
|
|||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
|
|
||||||
// Retrieve the imported contacts from the query parameter
|
// look for any imported contact array from the query parameter
|
||||||
const importedContacts = (this.$route as Router).query[
|
const importedContacts = (this.$route as RouteLocationNormalizedLoaded)
|
||||||
"contacts"
|
.query["contacts"] as string;
|
||||||
] as string;
|
|
||||||
if (importedContacts) {
|
if (importedContacts) {
|
||||||
await this.setContactsSelected(JSON.parse(importedContacts));
|
await this.setContactsSelected(JSON.parse(importedContacts));
|
||||||
}
|
}
|
||||||
|
|
||||||
// match everything after /contact-import/ in the window.location.pathname
|
// look for a JWT after /contact-import/ in the window.location.pathname
|
||||||
const jwt = window.location.pathname.match(
|
const jwt = window.location.pathname.match(
|
||||||
/\/contact-import\/(ey.+)$/,
|
/\/contact-import\/(ey.+)$/,
|
||||||
)?.[1];
|
)?.[1];
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
// decode the JWT
|
// would prefer to validate but we've got an error with JWTs on QR codes generated in the future
|
||||||
// eslint-disable-next-line prettier/prettier
|
// eslint-disable-next-line prettier/prettier
|
||||||
const parsedJwt: Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt"> = await decodeAndVerifyJwt(jwt);
|
// const parsedJwt: Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt"> = await decodeAndVerifyJwt(jwt);
|
||||||
await this.setContactsSelected(parsedJwt.payload.contacts as Contact[]);
|
// decode the JWT
|
||||||
|
const parsedJwt = decodeEndorserJwt(jwt);
|
||||||
|
|
||||||
|
const contacts: Array<Contact> =
|
||||||
|
parsedJwt.payload.contacts || // someday this will be the only payload sent to this page
|
||||||
|
(Array.isArray(parsedJwt.payload) ? parsedJwt.payload : undefined);
|
||||||
|
if (!contacts && parsedJwt.payload.own) {
|
||||||
|
// handle this single-contact JWT in the contacts page, better suited to single additions
|
||||||
|
(this.$router as Router).push({
|
||||||
|
name: "contacts",
|
||||||
|
query: { contactJwt: jwt },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (contacts) {
|
||||||
|
await this.setContactsSelected(contacts);
|
||||||
|
} else {
|
||||||
|
// no contacts found so default message should be OK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.contactsImporting.length === 1 &&
|
||||||
|
R.isEmpty(this.contactsExisting)
|
||||||
|
) {
|
||||||
|
// if there is only one contact and it's new, then we will automatically import it
|
||||||
|
this.contactsSelected[0] = true;
|
||||||
|
this.importContacts(); // ... which routes to the contacts list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,12 +241,19 @@ export default class ContactImportView extends Vue {
|
|||||||
if (existingContact) {
|
if (existingContact) {
|
||||||
this.contactsExisting[contactIn.did] = existingContact;
|
this.contactsExisting[contactIn.did] = existingContact;
|
||||||
|
|
||||||
const differences: Record<string, { new: string; old: string }> = {};
|
const differences: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
new: string | boolean | Array<ContactMethod> | undefined;
|
||||||
|
old: string | boolean | Array<ContactMethod> | undefined;
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
Object.keys(contactIn).forEach((key) => {
|
Object.keys(contactIn).forEach((key) => {
|
||||||
if (contactIn[key] !== existingContact[key]) {
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
if (!R.equals(contactIn[key as keyof Contact], existingContact[key as keyof Contact])) {
|
||||||
differences[key] = {
|
differences[key] = {
|
||||||
old: existingContact[key],
|
old: existingContact[key as keyof Contact],
|
||||||
new: contactIn[key],
|
new: contactIn[key as keyof Contact],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -190,8 +268,59 @@ export default class ContactImportView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check the contact-import JWT
|
||||||
|
async checkContactJwt(jwtInput: string) {
|
||||||
|
if (
|
||||||
|
jwtInput.endsWith(APP_SERVER) ||
|
||||||
|
jwtInput.endsWith(APP_SERVER + "/") ||
|
||||||
|
jwtInput.endsWith("contact-import") ||
|
||||||
|
jwtInput.endsWith("contact-import/")
|
||||||
|
) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "That is only part of the contact-import data; it's missing data at the end. Try another way to get the full data.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// process the invite JWT and/or text message containing the URL with the JWT
|
||||||
|
async processContactJwt(jwtInput: string) {
|
||||||
|
this.checkingImports = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// (For another approach used with invites, see InviteOneAcceptView.processInvite)
|
||||||
|
const jwt: string = getContactJwtFromJwtUrl(jwtInput);
|
||||||
|
// JWT format: { header, payload, signature, data }
|
||||||
|
const payload = decodeEndorserJwt(jwt).payload;
|
||||||
|
|
||||||
|
if (Array.isArray(payload.contacts)) {
|
||||||
|
await this.setContactsSelected(payload.contacts);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid contact-import JWT or URL: " + jwtInput);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const fullError = "Error importing contacts: " + errorStringForLog(error);
|
||||||
|
logConsoleAndDb(fullError, true);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "There was an error processing the contact-import data.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.checkingImports = false;
|
||||||
|
}
|
||||||
|
|
||||||
async importContacts() {
|
async importContacts() {
|
||||||
this.importing = true;
|
this.checkingImports = true;
|
||||||
let importedCount = 0,
|
let importedCount = 0,
|
||||||
updatedCount = 0;
|
updatedCount = 0;
|
||||||
for (let i = 0; i < this.contactsImporting.length; i++) {
|
for (let i = 0; i < this.contactsImporting.length; i++) {
|
||||||
@@ -203,6 +332,7 @@ export default class ContactImportView extends Vue {
|
|||||||
updatedCount++;
|
updatedCount++;
|
||||||
} else {
|
} else {
|
||||||
// without explicit clone on the Proxy, we get: DataCloneError: Failed to execute 'add' on 'IDBObjectStore': #<Object> could not be cloned.
|
// without explicit clone on the Proxy, we get: DataCloneError: Failed to execute 'add' on 'IDBObjectStore': #<Object> could not be cloned.
|
||||||
|
// DataError: Failed to execute 'add' on 'IDBObjectStore': Evaluating the object store's key path yielded a value that is not a valid key.
|
||||||
await db.contacts.add(R.clone(contact));
|
await db.contacts.add(R.clone(contact));
|
||||||
importedCount++;
|
importedCount++;
|
||||||
}
|
}
|
||||||
@@ -211,22 +341,24 @@ export default class ContactImportView extends Vue {
|
|||||||
if (this.makeVisible) {
|
if (this.makeVisible) {
|
||||||
const failedVisibileToContacts = [];
|
const failedVisibileToContacts = [];
|
||||||
for (let i = 0; i < this.contactsImporting.length; i++) {
|
for (let i = 0; i < this.contactsImporting.length; i++) {
|
||||||
const contact = this.contactsImporting[i];
|
if (this.contactsSelected[i]) {
|
||||||
if (contact) {
|
const contact = this.contactsImporting[i];
|
||||||
const visResult = await setVisibilityUtil(
|
if (contact) {
|
||||||
this.activeDid,
|
const visResult = await setVisibilityUtil(
|
||||||
this.apiServer,
|
this.activeDid,
|
||||||
this.axios,
|
this.apiServer,
|
||||||
db,
|
this.axios,
|
||||||
contact,
|
db,
|
||||||
true,
|
contact,
|
||||||
);
|
true,
|
||||||
if (!visResult.success) {
|
);
|
||||||
failedVisibileToContacts.push(contact);
|
if (!visResult.success) {
|
||||||
|
failedVisibileToContacts.push(contact);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (failedVisibileToContacts.length) {
|
if (failedVisibileToContacts.length > 0) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -241,7 +373,7 @@ export default class ContactImportView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.importing = false;
|
this.checkingImports = false;
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -101,14 +101,14 @@ import { NotificationIface } from "../constants/app";
|
|||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } 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 } from "../db/tables/settings";
|
||||||
import { getContactPayloadFromJwtUrl } from "../libs/crypto";
|
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
||||||
import {
|
import {
|
||||||
generateEndorserJwtForAccount,
|
generateEndorserJwtUrlForAccount,
|
||||||
isDid,
|
isDid,
|
||||||
register,
|
register,
|
||||||
setVisibilityUtil,
|
setVisibilityUtil,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import { ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
||||||
import { retrieveAccountMetadata } from "../libs/util";
|
import { retrieveAccountMetadata } from "../libs/util";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -146,7 +146,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
(settings.firstName || "") +
|
(settings.firstName || "") +
|
||||||
(settings.lastName ? ` ${settings.lastName}` : ""); // lastName is deprecated, pre v 0.1.3
|
(settings.lastName ? ` ${settings.lastName}` : ""); // lastName is deprecated, pre v 0.1.3
|
||||||
|
|
||||||
this.qrValue = await generateEndorserJwtForAccount(
|
this.qrValue = await generateEndorserJwtUrlForAccount(
|
||||||
account,
|
account,
|
||||||
!!settings.isRegistered,
|
!!settings.isRegistered,
|
||||||
name,
|
name,
|
||||||
@@ -179,8 +179,8 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
if (url) {
|
if (url) {
|
||||||
let newContact: Contact;
|
let newContact: Contact;
|
||||||
try {
|
try {
|
||||||
const payload = getContactPayloadFromJwtUrl(url);
|
const jwt = getContactJwtFromJwtUrl(url);
|
||||||
if (!payload) {
|
if (!jwt) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -192,8 +192,9 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { payload } = decodeEndorserJwt(jwt);
|
||||||
newContact = {
|
newContact = {
|
||||||
did: payload.iss as string,
|
did: payload.own.did || payload.iss, // ".own.did" is reliable as of v 0.3.49
|
||||||
name: payload.own.name,
|
name: payload.own.name,
|
||||||
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
|
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
|
||||||
profileImageUrl: payload.own.profileImageUrl,
|
profileImageUrl: payload.own.profileImageUrl,
|
||||||
@@ -405,7 +406,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(this.qrValue)
|
.copy(this.qrValue)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log("Contact URL:", this.qrValue);
|
// console.log("Contact URL:", this.qrValue);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
|
|||||||
@@ -69,32 +69,36 @@
|
|||||||
|
|
||||||
<div class="flex justify-between" v-if="contacts.length > 0">
|
<div class="flex justify-between" v-if="contacts.length > 0">
|
||||||
<div class="w-full text-left">
|
<div class="w-full text-left">
|
||||||
<input
|
<div v-if="!showGiveNumbers">
|
||||||
type="checkbox"
|
<input
|
||||||
v-if="!showGiveNumbers"
|
type="checkbox"
|
||||||
:checked="contactsSelected.length === contacts.length"
|
:checked="contactsSelected.length === contacts.length"
|
||||||
@click="
|
@click="
|
||||||
contactsSelected.length === contacts.length
|
contactsSelected.length === contacts.length
|
||||||
? (contactsSelected = [])
|
? (contactsSelected = [])
|
||||||
: (contactsSelected = contacts.map((contact) => contact.did))
|
: (contactsSelected = contacts.map((contact) => contact.did))
|
||||||
"
|
"
|
||||||
class="align-middle ml-2 h-6 w-6"
|
class="align-middle ml-2 h-6 w-6"
|
||||||
data-testId="contactCheckAllTop"
|
data-testId="contactCheckAllTop"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
href=""
|
href=""
|
||||||
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
|
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
|
||||||
:style="
|
:style="
|
||||||
contactsSelected.length > 0
|
contactsSelected.length > 0
|
||||||
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
||||||
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
||||||
"
|
"
|
||||||
@click="copySelectedContacts()"
|
@click="copySelectedContacts()"
|
||||||
v-if="!showGiveNumbers"
|
v-if="!showGiveNumbers"
|
||||||
data-testId="copySelectedContactsButtonTop"
|
data-testId="copySelectedContactsButtonTop"
|
||||||
>
|
>
|
||||||
Copy Selections
|
Copy Selections
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="showCopySelectionsInfo()">
|
||||||
|
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full text-right">
|
<div class="w-full text-right">
|
||||||
@@ -170,27 +174,35 @@
|
|||||||
)
|
)
|
||||||
: contactsSelected.push(contact.did)
|
: contactsSelected.push(contact.did)
|
||||||
"
|
"
|
||||||
class="ml-2 h-6 w-6"
|
class="ml-2 h-6 w-6 flex-shrink-0"
|
||||||
data-testId="contactCheckOne"
|
data-testId="contactCheckOne"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="text-base font-semibold ml-2">
|
<h2
|
||||||
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
class="text-base font-semibold ml-2 w-1/3 truncate flex-shrink-0"
|
||||||
|
>
|
||||||
|
{{ contactNameNonBreakingSpace(contact.name) }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<router-link
|
<span>
|
||||||
:to="{
|
<div class="flex items-center">
|
||||||
path: '/did/' + encodeURIComponent(contact.did),
|
<router-link
|
||||||
}"
|
:to="{
|
||||||
title="See more about this person"
|
path: '/did/' + encodeURIComponent(contact.did),
|
||||||
>
|
}"
|
||||||
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
|
title="See more about this person"
|
||||||
</router-link>
|
>
|
||||||
|
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
|
||||||
|
</router-link>
|
||||||
|
|
||||||
<span class="ml-4 text-sm overflow-hidden">{{
|
<span class="ml-4 text-sm overflow-hidden">{{
|
||||||
shortDid(contact.did)
|
shortDid(contact.did)
|
||||||
}}</span
|
}}</span>
|
||||||
><!-- The first 18 characters of did:peer are the same. -->
|
</div>
|
||||||
|
<div class="ml-4 text-sm">
|
||||||
|
{{ contact.notes }}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
||||||
<div
|
<div
|
||||||
@@ -332,11 +344,10 @@ import {
|
|||||||
updateDefaultSettings,
|
updateDefaultSettings,
|
||||||
} from "../db/index";
|
} from "../db/index";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import { getContactPayloadFromJwtUrl } from "../libs/crypto";
|
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
||||||
import { decodeEndorserJwt } from "../libs/crypto/vc";
|
import { decodeEndorserJwt } from "../libs/crypto/vc";
|
||||||
import {
|
import {
|
||||||
CONTACT_CSV_HEADER,
|
CONTACT_CSV_HEADER,
|
||||||
CONTACT_URL_PREFIX,
|
|
||||||
createEndorserJwtForDid,
|
createEndorserJwtForDid,
|
||||||
errorStringForLog,
|
errorStringForLog,
|
||||||
GiveSummaryRecord,
|
GiveSummaryRecord,
|
||||||
@@ -346,6 +357,9 @@ import {
|
|||||||
setVisibilityUtil,
|
setVisibilityUtil,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
VerifiableCredential,
|
VerifiableCredential,
|
||||||
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
|
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
||||||
|
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import { generateSaveAndActivateIdentity } from "../libs/util";
|
import { generateSaveAndActivateIdentity } from "../libs/util";
|
||||||
@@ -402,6 +416,11 @@ export default class ContactsView extends Vue {
|
|||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
|
|
||||||
|
// if these detect a query parameter, they can and then redirect to this URL without a query parameter
|
||||||
|
// to avoid problems when they reload or they go forward & back and it tries to reprocess
|
||||||
|
await this.processContactJwt();
|
||||||
|
await this.processInviteJwt();
|
||||||
|
|
||||||
this.showGiveNumbers = !!settings.showContactGivesInline;
|
this.showGiveNumbers = !!settings.showContactGivesInline;
|
||||||
this.hideRegisterPromptOnNewContact =
|
this.hideRegisterPromptOnNewContact =
|
||||||
!!settings.hideRegisterPromptOnNewContact;
|
!!settings.hideRegisterPromptOnNewContact;
|
||||||
@@ -416,9 +435,13 @@ export default class ContactsView extends Vue {
|
|||||||
this.contacts = baseContacts.sort((a, b) =>
|
this.contacts = baseContacts.sort((a, b) =>
|
||||||
(a.name || "").localeCompare(b.name || ""),
|
(a.name || "").localeCompare(b.name || ""),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processContactJwt() {
|
||||||
// handle a contact sent via URL
|
// handle a contact sent via URL
|
||||||
// @deprecated: use /contact-import/:jwt with a JWT that has an array of contacts
|
//
|
||||||
|
// For external links, use /contact-import/:jwt with a JWT that has an array of contacts
|
||||||
|
// because that will do better error checking for things like missing data on iOS platforms.
|
||||||
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
|
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
|
||||||
.query["contactJwt"] as string;
|
.query["contactJwt"] as string;
|
||||||
if (importedContactJwt) {
|
if (importedContactJwt) {
|
||||||
@@ -426,21 +449,25 @@ export default class ContactsView extends Vue {
|
|||||||
const { payload } = decodeEndorserJwt(importedContactJwt);
|
const { payload } = decodeEndorserJwt(importedContactJwt);
|
||||||
const userInfo = payload["own"] as UserInfo;
|
const userInfo = payload["own"] as UserInfo;
|
||||||
const newContact = {
|
const newContact = {
|
||||||
did: payload["iss"],
|
did: userInfo.did || payload["iss"], // ".did" is reliable as of v 0.3.49
|
||||||
name: userInfo.name,
|
name: userInfo.name,
|
||||||
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
|
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
|
||||||
profileImageUrl: userInfo.profileImageUrl,
|
profileImageUrl: userInfo.profileImageUrl,
|
||||||
publicKeyBase64: userInfo.publicEncKey,
|
publicKeyBase64: userInfo.publicEncKey,
|
||||||
registered: userInfo.registered,
|
registered: userInfo.registered,
|
||||||
} as Contact;
|
} as Contact;
|
||||||
this.addContact(newContact);
|
await this.addContact(newContact);
|
||||||
|
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
||||||
|
(this.$router as Router).push({ path: "/contacts" });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processInviteJwt() {
|
||||||
// handle an invite JWT sent via URL
|
// handle an invite JWT sent via URL
|
||||||
const importedInviteJwt = (this.$route as RouteLocationNormalizedLoaded)
|
const importedInviteJwt = (this.$route as RouteLocationNormalizedLoaded)
|
||||||
.query["inviteJwt"] as string;
|
.query["inviteJwt"] as string;
|
||||||
if (importedInviteJwt === "") {
|
if (importedInviteJwt === "") {
|
||||||
// this happens when a platform (usually iOS) doesn't include anything after the "=" in a shared link.
|
// this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link.
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -539,9 +566,15 @@ export default class ContactsView extends Vue {
|
|||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
||||||
|
(this.$router as Router).push({ path: "/contacts" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private contactNameNonBreakingSpace(contactName?: string) {
|
||||||
|
return (contactName || AppString.NO_CONTACT_NAME).replace(/\s/g, "\u00A0");
|
||||||
|
}
|
||||||
|
|
||||||
private danger(message: string, title: string = "Error", timeout = 5000) {
|
private danger(message: string, title: string = "Error", timeout = 5000) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -624,7 +657,7 @@ export default class ContactsView extends Vue {
|
|||||||
(useRecipient ? "given" : "received") +
|
(useRecipient ? "given" : "received") +
|
||||||
" data from the server.",
|
" data from the server.",
|
||||||
},
|
},
|
||||||
5000,
|
3000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -682,7 +715,7 @@ export default class ContactsView extends Vue {
|
|||||||
title: "Load Error",
|
title: "Load Error",
|
||||||
text: "Got an error loading your gives.",
|
text: "Got an error loading your gives.",
|
||||||
},
|
},
|
||||||
5000,
|
3000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -697,8 +730,30 @@ export default class ContactsView extends Vue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contactInput.startsWith(CONTACT_URL_PREFIX)) {
|
if (contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
|
||||||
await this.addContactFromScan(contactInput);
|
const jwt = getContactJwtFromJwtUrl(contactInput);
|
||||||
|
(this.$router as Router).push({
|
||||||
|
path: "/contact-import/" + jwt,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
contactInput.includes(CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI) ||
|
||||||
|
contactInput.includes(CONTACT_URL_PATH_ENDORSER_CH_OLD)
|
||||||
|
) {
|
||||||
|
const jwt = getContactJwtFromJwtUrl(contactInput);
|
||||||
|
const { payload } = decodeEndorserJwt(jwt);
|
||||||
|
const userInfo = payload["own"] as UserInfo;
|
||||||
|
const newContact = {
|
||||||
|
did: userInfo.did || payload["iss"], // "did" is reliable as of v 0.3.49
|
||||||
|
name: userInfo.name,
|
||||||
|
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
|
||||||
|
profileImageUrl: userInfo.profileImageUrl,
|
||||||
|
publicKeyBase64: userInfo.publicEncKey,
|
||||||
|
registered: userInfo.registered,
|
||||||
|
} as Contact;
|
||||||
|
await this.addContact(newContact);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -846,31 +901,6 @@ export default class ContactsView extends Vue {
|
|||||||
return db.contacts.add(newContact);
|
return db.contacts.add(newContact);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async addContactFromScan(url: string): Promise<void> {
|
|
||||||
const payload = getContactPayloadFromJwtUrl(url);
|
|
||||||
if (!payload) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "No Contact Info",
|
|
||||||
text: "The contact info could not be parsed.",
|
|
||||||
},
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
return this.addContact({
|
|
||||||
did: payload.iss,
|
|
||||||
name: payload.own.name,
|
|
||||||
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
|
|
||||||
profileImageUrl: payload.own.profileImageUrl,
|
|
||||||
publicKeyBase64: payload.own.publicEncKey,
|
|
||||||
registered: payload.own.registered,
|
|
||||||
} as Contact);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async addContact(newContact: Contact) {
|
private async addContact(newContact: Contact) {
|
||||||
if (!newContact.did) {
|
if (!newContact.did) {
|
||||||
this.danger("Cannot add a contact without a DID.", "Incomplete Contact");
|
this.danger("Cannot add a contact without a DID.", "Incomplete Contact");
|
||||||
@@ -930,7 +960,7 @@ export default class ContactsView extends Vue {
|
|||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}, 500);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -958,7 +988,7 @@ export default class ContactsView extends Vue {
|
|||||||
message +=
|
message +=
|
||||||
" Check that the contact doesn't conflict with any you already have.";
|
" Check that the contact doesn't conflict with any you already have.";
|
||||||
}
|
}
|
||||||
this.danger(message, "Contact Not Added", -1);
|
this.danger(message, "Contact Not Added", 5000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1007,7 +1037,7 @@ export default class ContactsView extends Vue {
|
|||||||
text:
|
text:
|
||||||
(contact.name || "That unnamed person") + " has been registered.",
|
(contact.name || "That unnamed person") + " has been registered.",
|
||||||
},
|
},
|
||||||
5000,
|
3000,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -1266,13 +1296,29 @@ export default class ContactsView extends Vue {
|
|||||||
this.danger("You must select contacts to copy.");
|
this.danger("You must select contacts to copy.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedContacts = this.contacts.filter((c) =>
|
const selectedContactsFull = this.contacts.filter((c) =>
|
||||||
this.contactsSelected.includes(c.did),
|
this.contactsSelected.includes(c.did),
|
||||||
);
|
);
|
||||||
console.log(
|
const selectedContacts: Array<Contact> = selectedContactsFull.map((c) => {
|
||||||
"Array of selected contacts:",
|
const contact: Contact = {
|
||||||
JSON.stringify(selectedContacts),
|
did: c.did,
|
||||||
);
|
name: c.name,
|
||||||
|
};
|
||||||
|
if (c.nextPubKeyHashB64) {
|
||||||
|
contact.nextPubKeyHashB64 = c.nextPubKeyHashB64;
|
||||||
|
}
|
||||||
|
if (c.profileImageUrl) {
|
||||||
|
contact.profileImageUrl = c.profileImageUrl;
|
||||||
|
}
|
||||||
|
if (c.publicKeyBase64) {
|
||||||
|
contact.publicKeyBase64 = c.publicKeyBase64;
|
||||||
|
}
|
||||||
|
return contact;
|
||||||
|
});
|
||||||
|
// console.log(
|
||||||
|
// "Array of selected contacts:",
|
||||||
|
// JSON.stringify(selectedContacts),
|
||||||
|
// );
|
||||||
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
||||||
contacts: selectedContacts,
|
contacts: selectedContacts,
|
||||||
});
|
});
|
||||||
@@ -1287,7 +1333,7 @@ export default class ContactsView extends Vue {
|
|||||||
title: "Copied",
|
title: "Copied",
|
||||||
text: "The link for those contacts is now in the clipboard.",
|
text: "The link for those contacts is now in the clipboard.",
|
||||||
},
|
},
|
||||||
5000,
|
3000,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1306,5 +1352,17 @@ export default class ContactsView extends Vue {
|
|||||||
return did.substring(0, did.indexOf(":", 4) + 7) + "...";
|
return did.substring(0, did.indexOf(":", 4) + 7) + "...";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private showCopySelectionsInfo() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "Copying Contacts",
|
||||||
|
text: "Contact info will include name, ID, profile image, and public key.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -26,15 +26,11 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
{{ contactFromDid?.name || "(no name)" }}
|
{{ contactFromDid?.name || "(no name)" }}
|
||||||
<button
|
<router-link
|
||||||
@click="
|
:to="{ name: 'contact-edit', params: { did: contactFromDid?.did } }"
|
||||||
contactEdit = true;
|
|
||||||
contactNewName = (contactFromDid?.name as string) || '';
|
|
||||||
"
|
|
||||||
title="Edit"
|
|
||||||
>
|
>
|
||||||
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||||
</button>
|
</router-link>
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
@click="showDidDetails = !showDidDetails"
|
@click="showDidDetails = !showDidDetails"
|
||||||
@@ -163,34 +159,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit Name Dialog, maybe should be replaced by ContactNameDialog -->
|
|
||||||
<div v-if="contactEdit" class="dialog-overlay">
|
|
||||||
<div class="dialog">
|
|
||||||
<h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
|
||||||
placeholder="Name"
|
|
||||||
v-model="contactNewName"
|
|
||||||
/>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<button
|
|
||||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
|
||||||
@click="onClickSaveName(contactNewName)"
|
|
||||||
>
|
|
||||||
<fa icon="save" />
|
|
||||||
</button>
|
|
||||||
<span class="inline-block w-2" />
|
|
||||||
<button
|
|
||||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
|
||||||
@click="onClickCancelName()"
|
|
||||||
>
|
|
||||||
<fa icon="ban" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading Animation -->
|
<!-- Loading Animation -->
|
||||||
<div
|
<div
|
||||||
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||||
@@ -290,8 +258,6 @@ export default class DIDView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
|
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
|
||||||
contactFromDid?: Contact;
|
contactFromDid?: Contact;
|
||||||
contactEdit = false;
|
|
||||||
contactNewName: string = "";
|
|
||||||
contactYaml = "";
|
contactYaml = "";
|
||||||
hitEnd = false;
|
hitEnd = false;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
@@ -312,8 +278,23 @@ export default class DIDView extends Vue {
|
|||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
|
|
||||||
const pathParam = window.location.pathname.substring("/did/".length);
|
const pathParam = window.location.pathname.substring("/did/".length);
|
||||||
if (pathParam) {
|
let showDid = pathParam;
|
||||||
this.viewingDid = decodeURIComponent(pathParam);
|
if (!showDid) {
|
||||||
|
showDid = this.activeDid;
|
||||||
|
if (showDid) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
title: "Your Info",
|
||||||
|
text: "No user was specified so showing your info.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showDid) {
|
||||||
|
this.viewingDid = decodeURIComponent(showDid);
|
||||||
this.contactFromDid = await db.contacts.get(this.viewingDid);
|
this.contactFromDid = await db.contacts.get(this.viewingDid);
|
||||||
if (this.contactFromDid) {
|
if (this.contactFromDid) {
|
||||||
this.contactYaml = yaml.dump(this.contactFromDid);
|
this.contactYaml = yaml.dump(this.contactFromDid);
|
||||||
@@ -513,7 +494,7 @@ export default class DIDView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: e.userMessage || "There was a problem retrieving claims.",
|
text: e.userMessage || "There was a problem retrieving claims.",
|
||||||
},
|
},
|
||||||
-1,
|
3000,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@@ -559,29 +540,6 @@ export default class DIDView extends Vue {
|
|||||||
return claim.claim.name || claim.claim.description || "";
|
return claim.claim.name || claim.claim.description || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onClickCancelName() {
|
|
||||||
this.contactEdit = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async onClickSaveName(newName: string) {
|
|
||||||
if (!this.contactFromDid) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Not A Contact",
|
|
||||||
text: "First add this on the contact page, then you can edit here.",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.contactFromDid.name = newName;
|
|
||||||
return db.contacts
|
|
||||||
.update(this.contactFromDid.did, { name: newName })
|
|
||||||
.then(() => (this.contactEdit = false));
|
|
||||||
}
|
|
||||||
|
|
||||||
// note that this is also in ContactView.vue
|
// note that this is also in ContactView.vue
|
||||||
async confirmSetVisibility(contact: Contact, visibility: boolean) {
|
async confirmSetVisibility(contact: Contact, visibility: boolean) {
|
||||||
const visibilityPrompt = visibility
|
const visibilityPrompt = visibility
|
||||||
|
|||||||
@@ -337,9 +337,9 @@ export default class DiscoverView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: `There was a problem accessing the server. Try again later.`,
|
text: `There was a problem accessing the server.`,
|
||||||
},
|
},
|
||||||
-1,
|
3000,
|
||||||
);
|
);
|
||||||
|
|
||||||
throw details;
|
throw details;
|
||||||
@@ -376,7 +376,7 @@ export default class DiscoverView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: e.userMessage || "There was a problem retrieving projects.",
|
text: e.userMessage || "There was a problem retrieving projects.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@@ -433,9 +433,9 @@ export default class DiscoverView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "There was a problem accessing the server. Try again later.",
|
text: "There was a problem accessing the server.",
|
||||||
},
|
},
|
||||||
-1,
|
3000,
|
||||||
);
|
);
|
||||||
throw await response.text();
|
throw await response.text();
|
||||||
}
|
}
|
||||||
@@ -472,7 +472,7 @@ export default class DiscoverView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: e.userMessage || "There was a problem retrieving projects.",
|
text: e.userMessage || "There was a problem retrieving projects.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|||||||
@@ -818,7 +818,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: errorMessage || "There was an error creating the give.",
|
text: errorMessage || "There was an error creating the give.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -828,7 +828,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
title: "Success",
|
title: "Success",
|
||||||
text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`,
|
text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`,
|
||||||
},
|
},
|
||||||
5000,
|
3000,
|
||||||
);
|
);
|
||||||
localStorage.removeItem("imageUrl");
|
localStorage.removeItem("imageUrl");
|
||||||
if (this.destinationPathAfter) {
|
if (this.destinationPathAfter) {
|
||||||
@@ -851,7 +851,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: errorMessage,
|
text: errorMessage,
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -912,7 +912,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
title: "Data Sharing",
|
title: "Data Sharing",
|
||||||
text: libsUtil.PRIVACY_MESSAGE,
|
text: libsUtil.PRIVACY_MESSAGE,
|
||||||
},
|
},
|
||||||
-1,
|
7000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,10 +331,10 @@ export default class HelpNotificationsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
alertWebPushSubscription() {
|
alertWebPushSubscription() {
|
||||||
console.log(
|
// console.log(
|
||||||
"Web push subscription:",
|
// "Web push subscription:",
|
||||||
JSON.parse(JSON.stringify(this.subscriptionJSON)), // gives more info than plain console logging
|
// JSON.parse(JSON.stringify(this.subscriptionJSON)), // gives more info than plain console logging
|
||||||
);
|
// );
|
||||||
alert(JSON.stringify(this.subscriptionJSON));
|
alert(JSON.stringify(this.subscriptionJSON));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ export default class HelpNotificationsView extends Vue {
|
|||||||
// Note that this exact verbiage shows in help text.
|
// Note that this exact verbiage shows in help text.
|
||||||
text: "You must enable notifications before testing the web push.",
|
text: "You must enable notifications before testing the web push.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -365,7 +365,7 @@ export default class HelpNotificationsView extends Vue {
|
|||||||
"Check your device for the test web push message" +
|
"Check your device for the test web push message" +
|
||||||
(skipFilter ? "." : " if there are new items in your feed."),
|
(skipFilter ? "." : " if there are new items in your feed."),
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Got an error sending test notification:", error);
|
console.error("Got an error sending test notification:", error);
|
||||||
@@ -376,7 +376,7 @@ export default class HelpNotificationsView extends Vue {
|
|||||||
title: "Error Sending Test",
|
title: "Error Sending Test",
|
||||||
text: "Got an error sending the test web push notification.",
|
text: "Got an error sending the test web push notification.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,7 +409,7 @@ export default class HelpNotificationsView extends Vue {
|
|||||||
title: "Failed",
|
title: "Failed",
|
||||||
text: "Got an error sending a notification.",
|
text: "Got an error sending a notification.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -557,7 +557,6 @@ export default class HomeView extends Vue {
|
|||||||
this.activeDid,
|
this.activeDid,
|
||||||
this.lastAckedOfferToUserJwtId,
|
this.lastAckedOfferToUserJwtId,
|
||||||
);
|
);
|
||||||
console.log("offersToUserData", offersToUserData);
|
|
||||||
this.numNewOffersToUser = offersToUserData.data.length;
|
this.numNewOffersToUser = offersToUserData.data.length;
|
||||||
this.newOffersToUserHitLimit = offersToUserData.hitLimit;
|
this.newOffersToUserHitLimit = offersToUserData.hitLimit;
|
||||||
}
|
}
|
||||||
@@ -585,7 +584,7 @@ export default class HomeView extends Vue {
|
|||||||
err.userMessage ||
|
err.userMessage ||
|
||||||
"There was an error retrieving your settings or the latest activity.",
|
"There was an error retrieving your settings or the latest activity.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
title: "Error Loading Accounts",
|
title: "Error Loading Accounts",
|
||||||
text: "Clear your cache and start over (after data backup).",
|
text: "Clear your cache and start over (after data backup).",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
console.error("Telling user to clear cache at page create because:", err);
|
console.error("Telling user to clear cache at page create because:", err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export default class ImportAccountView extends Vue {
|
|||||||
title: "Invalid Mnemonic",
|
title: "Invalid Mnemonic",
|
||||||
text: "Please check your mnemonic and try again.",
|
text: "Please check your mnemonic and try again.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -192,7 +192,7 @@ export default class ImportAccountView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Got an error creating that identifier.",
|
text: "Got an error creating that identifier.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Invite" />
|
<QuickNav selected="Invite" />
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<div v-if="acceptInput" class="text-center mt-4">
|
<div
|
||||||
|
v-if="checkingInvite"
|
||||||
|
class="text-lg text-center font-light relative px-7"
|
||||||
|
>
|
||||||
|
<fa icon="spinner" class="fa-spin-pulse" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center mt-4">
|
||||||
<p>That invitation did not work.</p>
|
<p>That invitation did not work.</p>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
Go back to your invite message and copy the entire text, then paste it
|
Go back to your invite message and copy the entire text, then paste it
|
||||||
here.
|
here.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
If the link looks correct, try Chrome. (For example, iOS may have cut
|
If the data looks correct, try Chrome. (For example, iOS may have cut
|
||||||
off the invite data, or it may have shown a preview that stole your
|
off the invite data, or it may have shown a preview that stole your
|
||||||
invite.) If it still complains, you may need the person who invited you
|
invite.) If it still complains, you may need the person who invited you
|
||||||
to send a new one.
|
to send a new one.
|
||||||
@@ -25,16 +31,9 @@
|
|||||||
@click="() => processInvite(inputJwt, true)"
|
@click="() => processInvite(inputJwt, true)"
|
||||||
class="ml-2 p-2 bg-blue-500 text-white rounded"
|
class="ml-2 p-2 bg-blue-500 text-white rounded"
|
||||||
>
|
>
|
||||||
Submit
|
Accept
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="checkingInvite"
|
|
||||||
class="text-lg text-center font-light relative px-7"
|
|
||||||
>
|
|
||||||
<fa icon="spinner" class="fa-spin-pulse" />
|
|
||||||
Loading…
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
logConsoleAndDb,
|
logConsoleAndDb,
|
||||||
@@ -57,7 +56,6 @@ import { generateSaveAndActivateIdentity } from "../libs/util";
|
|||||||
export default class InviteOneAcceptView extends Vue {
|
export default class InviteOneAcceptView extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
acceptInput: boolean = false;
|
|
||||||
activeDid: string = "";
|
activeDid: string = "";
|
||||||
apiServer: string = "";
|
apiServer: string = "";
|
||||||
checkingInvite: boolean = true;
|
checkingInvite: boolean = true;
|
||||||
@@ -91,6 +89,7 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
|
|
||||||
// parse the string: extract the URL or JWT if surrounded by spaces
|
// parse the string: extract the URL or JWT if surrounded by spaces
|
||||||
// and then extract the JWT from the URL
|
// and then extract the JWT from the URL
|
||||||
|
// (For another approach used with contacts, see getContactPayloadFromJwtUrl)
|
||||||
const urlMatch = jwtInput.match(/(https?:\/\/[^\s]+)/);
|
const urlMatch = jwtInput.match(/(https?:\/\/[^\s]+)/);
|
||||||
if (urlMatch && urlMatch[1]) {
|
if (urlMatch && urlMatch[1]) {
|
||||||
// extract the JWT from the URL, meaning any character except "?"
|
// extract the JWT from the URL, meaning any character except "?"
|
||||||
@@ -112,13 +111,12 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Missing invite",
|
title: "Missing Invite",
|
||||||
text: "There was no invite. Paste the entire text that has the link.",
|
text: "There was no invite. Paste the entire text that has the data.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.acceptInput = true;
|
|
||||||
} else {
|
} else {
|
||||||
//const payload: JWTPayload =
|
//const payload: JWTPayload =
|
||||||
decodeEndorserJwt(jwt);
|
decodeEndorserJwt(jwt);
|
||||||
@@ -144,7 +142,6 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
3000,
|
3000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.acceptInput = true;
|
|
||||||
}
|
}
|
||||||
this.checkingInvite = false;
|
this.checkingInvite = false;
|
||||||
}
|
}
|
||||||
@@ -152,6 +149,8 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
// check the invite JWT
|
// check the invite JWT
|
||||||
async checkInvite(jwtInput: string) {
|
async checkInvite(jwtInput: string) {
|
||||||
if (
|
if (
|
||||||
|
jwtInput.endsWith(APP_SERVER) ||
|
||||||
|
jwtInput.endsWith(APP_SERVER + "/") ||
|
||||||
jwtInput.endsWith("invite-one-accept") ||
|
jwtInput.endsWith("invite-one-accept") ||
|
||||||
jwtInput.endsWith("invite-one-accept/")
|
jwtInput.endsWith("invite-one-accept/")
|
||||||
) {
|
) {
|
||||||
@@ -160,7 +159,7 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "That is only part of the invite link; it's missing data at the end. Try another way to get the full link.",
|
text: "That is only part of the invite data; it's missing some at the end. Try another way to get the full data.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
>
|
>
|
||||||
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
|
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
|
||||||
</router-link>
|
</router-link>
|
||||||
<!-- New line that appears on hover -->
|
<!-- New line that appears on hover or when the offer is clicked -->
|
||||||
<div
|
<div
|
||||||
@click="markOffersAsReadStartingWith(offer.jwtId)"
|
@click="markOffersAsReadStartingWith(offer.jwtId)"
|
||||||
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
|
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
|
||||||
@@ -271,7 +271,7 @@ export default class NewActivityView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Marked as Unread",
|
title: "Marked as Unread",
|
||||||
text: "All offers above that one are marked as unread.",
|
text: "All offers above that line are marked as unread.",
|
||||||
},
|
},
|
||||||
3000,
|
3000,
|
||||||
);
|
);
|
||||||
@@ -292,7 +292,7 @@ export default class NewActivityView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Marked as Read",
|
title: "Marked as Read",
|
||||||
text: "The offers are marked as viewed. Click in the list to keep them as new.",
|
text: "The offers are now marked as viewed. Click in the list to keep them as new.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
@@ -321,7 +321,7 @@ export default class NewActivityView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Marked as Unread",
|
title: "Marked as Unread",
|
||||||
text: "All offers above that one are marked as unread.",
|
text: "All offers above that line are marked as unread.",
|
||||||
},
|
},
|
||||||
3000,
|
3000,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ export default class OfferDetailsView extends Vue {
|
|||||||
title: "Retrieval Error",
|
title: "Retrieval Error",
|
||||||
text: "The previous record isn't available for editing. If you submit, you'll create a new record.",
|
text: "The previous record isn't available for editing. If you submit, you'll create a new record.",
|
||||||
},
|
},
|
||||||
6000,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ export default class OfferDetailsView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: err.message || "There was an error retrieving your settings.",
|
text: err.message || "There was an error retrieving your settings.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,7 +530,7 @@ export default class OfferDetailsView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: errorMessage || "There was an error creating the offer.",
|
text: errorMessage || "There was an error creating the offer.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -563,7 +563,7 @@ export default class OfferDetailsView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: errorMessage,
|
text: errorMessage,
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -621,7 +621,7 @@ export default class OfferDetailsView extends Vue {
|
|||||||
title: "Data Sharing",
|
title: "Data Sharing",
|
||||||
text: libsUtil.PRIVACY_MESSAGE,
|
text: libsUtil.PRIVACY_MESSAGE,
|
||||||
},
|
},
|
||||||
-1,
|
7000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
title: "Error Loading Profile",
|
title: "Error Loading Profile",
|
||||||
text: "See the Help page to fix problems with your personal data.",
|
text: "See the Help page to fix problems with your personal data.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -475,9 +475,9 @@ export default class ProjectsView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Failed to get offers from the server. Try again later.",
|
text: "Failed to get offers from the server.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -490,7 +490,7 @@ export default class ProjectsView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Got an error loading offers.",
|
text: "Got an error loading offers.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export default class SearchAreaView extends Vue {
|
|||||||
title: "Error Updating Search Settings",
|
title: "Error Updating Search Settings",
|
||||||
text: "Try going to a different page and then coming back.",
|
text: "Try going to a different page and then coming back.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
console.error(
|
console.error(
|
||||||
"Telling user to retry the location search setting because:",
|
"Telling user to retry the location search setting because:",
|
||||||
@@ -243,7 +243,7 @@ export default class SearchAreaView extends Vue {
|
|||||||
title: "No Location Selected",
|
title: "No Location Selected",
|
||||||
text: "Select a location on the map.",
|
text: "Select a location on the map.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,7 +271,7 @@ export default class SearchAreaView extends Vue {
|
|||||||
title: "Error Updating Search Settings",
|
title: "Error Updating Search Settings",
|
||||||
text: "Try going to a different page and then coming back.",
|
text: "Try going to a different page and then coming back.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
console.error(
|
console.error(
|
||||||
"Telling user to retry the location search setting because:",
|
"Telling user to retry the location search setting because:",
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export default class SeedBackupView extends Vue {
|
|||||||
title: "Error Loading Profile",
|
title: "Error Loading Profile",
|
||||||
text: "Got an error loading your seed data.",
|
text: "Got an error loading your seed data.",
|
||||||
},
|
},
|
||||||
-1,
|
3000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,13 +44,12 @@
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { generateEndorserJwtForAccount } from "../libs/endorserServer";
|
|
||||||
import { retrieveAccountMetadata } from "../libs/util";
|
import { retrieveAccountMetadata } from "../libs/util";
|
||||||
|
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { QuickNav, TopMessage },
|
components: { QuickNav, TopMessage },
|
||||||
@@ -70,7 +69,7 @@ export default class ShareMyContactInfoView extends Vue {
|
|||||||
const numContacts = await db.contacts.count();
|
const numContacts = await db.contacts.count();
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
const message = await generateEndorserJwtForAccount(
|
const message = await generateEndorserJwtUrlForAccount(
|
||||||
account,
|
account,
|
||||||
isRegistered,
|
isRegistered,
|
||||||
givenName,
|
givenName,
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export default class SharedPhotoView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Got an error loading this data.",
|
text: "Got an error loading this data.",
|
||||||
},
|
},
|
||||||
-1,
|
3000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export default class StatisticsView extends Vue {
|
|||||||
title: "Mounting Error",
|
title: "Mounting Error",
|
||||||
text: error.message,
|
text: error.message,
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
title: 'Information Alert',
|
title: 'Information Alert',
|
||||||
text: 'Just wanted you to know.',
|
text: 'Just wanted you to know.',
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
title: 'Success Alert',
|
title: 'Success Alert',
|
||||||
text: 'Congratulations!',
|
text: 'Congratulations!',
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="font-bold uppercase bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
|
class="font-bold uppercase bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
title: 'Warning Alert',
|
title: 'Warning Alert',
|
||||||
text: 'You might wanna look at this.',
|
text: 'You might wanna look at this.',
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="font-bold uppercase bg-amber-600 text-white px-3 py-2 rounded-md mr-2"
|
class="font-bold uppercase bg-amber-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
title: 'Danger Alert',
|
title: 'Danger Alert',
|
||||||
text: 'Something terrible has happened!',
|
text: 'Something terrible has happened!',
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="font-bold uppercase bg-rose-600 text-white px-3 py-2 rounded-md mr-2"
|
class="font-bold uppercase bg-rose-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { deleteContact, generateNewEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
|
import { deleteContact, generateNewEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
|
||||||
|
|
||||||
test('Check User 0 can invite someone', async ({ page }) => {
|
test('Check User 0 can invite someone', async ({ page }) => {
|
||||||
const newDid = await generateNewEthrUser(page);
|
|
||||||
|
|
||||||
await importUser(page, '00');
|
await importUser(page, '00');
|
||||||
await page.goto('./invite-one');
|
await page.goto('./invite-one');
|
||||||
await page.locator('button > svg.fa-plus').click();
|
await page.locator('button > svg.fa-plus').click();
|
||||||
@@ -23,6 +21,7 @@ test('Check User 0 can invite someone', async ({ page }) => {
|
|||||||
expect(inviteLink).not.toBeNull();
|
expect(inviteLink).not.toBeNull();
|
||||||
|
|
||||||
// become the new user and accept the invite
|
// become the new user and accept the invite
|
||||||
|
const newDid = await generateNewEthrUser(page);
|
||||||
await switchToUser(page, newDid);
|
await switchToUser(page, newDid);
|
||||||
await page.goto(inviteLink as string);
|
await page.goto(inviteLink as string);
|
||||||
await page.getByPlaceholder('Name', { exact: true }).fill(`My pal User #0`);
|
await page.getByPlaceholder('Name', { exact: true }).fill(`My pal User #0`);
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ test('Record something given', async ({ page }) => {
|
|||||||
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
||||||
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
||||||
const page1Promise = page.waitForEvent('popup');
|
const page1Promise = page.waitForEvent('popup');
|
||||||
|
// expand the Details section to see the extended details
|
||||||
|
await page.getByRole('heading', { name: 'Details', exact: true }).click();
|
||||||
await page.getByRole('link', { name: 'View on the Public Server' }).click();
|
await page.getByRole('link', { name: 'View on the Public Server' }).click();
|
||||||
const page1 = await page1Promise;
|
const page1 = await page1Promise;
|
||||||
});
|
});
|
||||||
@@ -21,15 +21,15 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
|||||||
// Combine title prefix with the random string
|
// Combine title prefix with the random string
|
||||||
const finalTitle = standardTitle + finalRandomString;
|
const finalTitle = standardTitle + finalRandomString;
|
||||||
|
|
||||||
// Contact name
|
|
||||||
const contactName = 'Contact #000 renamed';
|
const contactName = 'Contact #000 renamed';
|
||||||
|
const userName = 'User #000';
|
||||||
|
|
||||||
// Import user 01
|
// Import user 01
|
||||||
await importUser(page, '01');
|
await importUser(page, '01');
|
||||||
|
|
||||||
// Add new contact
|
// Add new contact
|
||||||
await page.goto('./contacts');
|
await page.goto('./contacts');
|
||||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F, User #000');
|
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F, ' + userName);
|
||||||
await page.locator('button > svg.fa-plus').click();
|
await page.locator('button > svg.fa-plus').click();
|
||||||
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();
|
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();
|
||||||
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
||||||
@@ -37,15 +37,19 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
|||||||
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
||||||
|
|
||||||
// Verify added contact
|
// Verify added contact
|
||||||
await expect(page.locator('li.border-b')).toContainText('User #000');
|
await expect(page.locator('li.border-b')).toContainText(userName);
|
||||||
|
|
||||||
// Rename contact
|
// Rename contact
|
||||||
await page.locator('li.border-b div div > a[title="See more about this person"]').click();
|
await page.locator(`li[data-testid="contactListItem"] h2:has-text("${userName}") + span svg.fa-circle-info`).click();
|
||||||
await page.locator('h2 > button > svg.fa-pen').click();
|
// now on the DID view page
|
||||||
await expect(page.locator('div.dialog-overlay > div.dialog').filter({ hasText: 'Edit Name' })).toBeVisible();
|
await page.locator('h2 svg.fa-pen').click();
|
||||||
await page.getByPlaceholder('Name', { exact: true }).fill(contactName);
|
// now on the contact edit page
|
||||||
await page.locator('.dialog > .flex > button').first().click();
|
await expect(page.getByTestId('contactName').locator('input')).toBeVisible();
|
||||||
// await page.locator('.dialog > .flex > button').first().click(); // close alert
|
// check that the input field has userName
|
||||||
|
await expect(page.getByTestId('contactName').locator('input')).toHaveValue(userName);
|
||||||
|
await page.getByTestId('contactName').locator('input').fill(contactName);
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await expect(page.locator('h2', { hasText: contactName })).toBeVisible();
|
||||||
|
|
||||||
// Confirm that home shows contact in "Record Something…"
|
// Confirm that home shows contact in "Record Something…"
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
@@ -145,10 +149,7 @@ test('Add contact, copy details, delete, and import from paste & from file', asy
|
|||||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
|
||||||
await expect(page.locator('div[role="alert"]')).toBeHidden();
|
await expect(page.locator('div[role="alert"]')).toBeHidden();
|
||||||
// I would prefer to copy from the clipboard, but the recommended approaches don't work.
|
// I would prefer to copy from the clipboard, but the recommended approaches don't work.
|
||||||
// this seems to fail in non-chromium browsers
|
// See a different clipboard solution below.
|
||||||
//await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
|
||||||
// this seems to fail in chromium (at least) where clipboard is undefined
|
|
||||||
//const contactData = await navigator.clipboard.readText();
|
|
||||||
|
|
||||||
// see contact details on the second contact
|
// see contact details on the second contact
|
||||||
await page.getByTestId('contactListItem').nth(1).locator('a').click();
|
await page.getByTestId('contactListItem').nth(1).locator('a').click();
|
||||||
@@ -185,7 +186,6 @@ test('Add contact, copy details, delete, and import from paste & from file', asy
|
|||||||
await page.goto('./account');
|
await page.goto('./account');
|
||||||
await page.getByRole('heading', { name: 'Advanced' }).click();
|
await page.getByRole('heading', { name: 'Advanced' }).click();
|
||||||
const fileSelect = await page.locator('input[type="file"]')
|
const fileSelect = await page.locator('input[type="file"]')
|
||||||
//fileSelect.click();
|
|
||||||
fileSelect.setInputFiles('./test-playwright/exported-data.json');
|
fileSelect.setInputFiles('./test-playwright/exported-data.json');
|
||||||
await page.locator('button', { hasText: 'Import Only Contacts' }).click();
|
await page.locator('button', { hasText: 'Import Only Contacts' }).click();
|
||||||
// we're on the contact-import page
|
// we're on the contact-import page
|
||||||
@@ -199,3 +199,64 @@ test('Add contact, copy details, delete, and import from paste & from file', asy
|
|||||||
// But it should only show that one, for User #000.
|
// But it should only show that one, for User #000.
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Copy contact to clipboard, then import ', async ({ page, context }, testInfo) => {
|
||||||
|
await importUser(page, '00');
|
||||||
|
|
||||||
|
await page.goto('./account');
|
||||||
|
await page.getByRole('heading', { name: 'Advanced' }).click();
|
||||||
|
const fileSelect = await page.locator('input[type="file"]')
|
||||||
|
fileSelect.setInputFiles('./test-playwright/exported-data.json');
|
||||||
|
await page.locator('button', { hasText: 'Import Only Contacts' }).click();
|
||||||
|
// we're on the contact-import page
|
||||||
|
await expect(page.getByRole('heading', { name: "Contact Import" })).toBeVisible();
|
||||||
|
await page.locator('button', { hasText: 'Import' }).click();
|
||||||
|
|
||||||
|
await page.goto('./contacts');
|
||||||
|
// Copy contact details
|
||||||
|
await page.getByTestId('contactCheckAllTop').click();
|
||||||
|
|
||||||
|
// // There's a crazy amount of overlap in all the userAgent values. Ug.
|
||||||
|
// const agent = await page.evaluate(() => {
|
||||||
|
// return navigator.userAgent;
|
||||||
|
// });
|
||||||
|
// console.log("agent: ", agent);
|
||||||
|
|
||||||
|
const isFirefox = await page.evaluate(() => {
|
||||||
|
return navigator.userAgent.includes('Firefox');
|
||||||
|
});
|
||||||
|
if (isFirefox) {
|
||||||
|
// Firefox doesn't grant permissions like this but it works anyway.
|
||||||
|
} else {
|
||||||
|
await context.grantPermissions(['clipboard-read']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWebkit = await page.evaluate(() => {
|
||||||
|
return navigator.userAgent.includes('Macintosh') || navigator.userAgent.includes('iPhone');
|
||||||
|
});
|
||||||
|
if (isWebkit) {
|
||||||
|
console.log("Haven't found a way to access clipboard text in Webkit. Skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Running test that copies contact details to clipboard.");
|
||||||
|
await page.getByTestId('copySelectedContactsButtonTop').click();
|
||||||
|
const clipboardText = await page.evaluate(async () => {
|
||||||
|
return navigator.clipboard.readText();
|
||||||
|
});
|
||||||
|
|
||||||
|
// look into the playwright.config file for the server URL
|
||||||
|
const webServer = testInfo.config.webServer;
|
||||||
|
const clientServerUrl = webServer?.url;
|
||||||
|
|
||||||
|
const PATH_PART = clientServerUrl + "/contact-import/";
|
||||||
|
expect(clipboardText).toContain(PATH_PART);
|
||||||
|
|
||||||
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
|
||||||
|
await expect(page.locator('div[role="alert"]')).toBeHidden();
|
||||||
|
|
||||||
|
await page.goto(clipboardText);
|
||||||
|
// we're on the contact-import page
|
||||||
|
await expect(page.getByRole('heading', { name: "Contact Import" })).toBeVisible();
|
||||||
|
await expect(page.locator('span', { hasText: '4 contacts are the same' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ test('Record an offer', async ({ page }) => {
|
|||||||
await expect(page.getByText('Offered to a bigger plan')).toBeVisible();
|
await expect(page.getByText('Offered to a bigger plan')).toBeVisible();
|
||||||
|
|
||||||
const serverPagePromise = page.waitForEvent('popup');
|
const serverPagePromise = page.waitForEvent('popup');
|
||||||
|
// expand the Details section to see the extended details
|
||||||
|
await page.getByRole('heading', { name: 'Details', exact: true }).click();
|
||||||
await page.getByRole('link', { name: 'View on the Public Server' }).click();
|
await page.getByRole('link', { name: 'View on the Public Server' }).click();
|
||||||
const serverPage = await serverPagePromise;
|
const serverPage = await serverPagePromise;
|
||||||
await expect(serverPage.getByText(description)).toBeVisible();
|
await expect(serverPage.getByText(description)).toBeVisible();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { importUser, generateNewEthrUser, switchToUser } from './testUtils';
|
|||||||
test('New offers for another user', async ({ page }) => {
|
test('New offers for another user', async ({ page }) => {
|
||||||
const user01Did = await generateNewEthrUser(page);
|
const user01Did = await generateNewEthrUser(page);
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
|
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||||
await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden();
|
await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden();
|
||||||
|
|
||||||
await importUser(page, '00');
|
await importUser(page, '00');
|
||||||
@@ -43,7 +44,7 @@ test('New offers for another user', async ({ page }) => {
|
|||||||
// as user 1, go to the home page and check that two offers are shown as new
|
// as user 1, go to the home page and check that two offers are shown as new
|
||||||
await switchToUser(page, user01Did);
|
await switchToUser(page, user01Did);
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
// await page.getByTestId('closeOnboardingAndFinish').click();
|
||||||
let offerNumElem = page.getByTestId('newDirectOffersActivityNumber');
|
let offerNumElem = page.getByTestId('newDirectOffersActivityNumber');
|
||||||
await expect(offerNumElem).toHaveText('2');
|
await expect(offerNumElem).toHaveText('2');
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export async function deleteContact(page: Page, did: string): Promise<void> {
|
|||||||
await page.goto('./contacts');
|
await page.goto('./contacts');
|
||||||
const contactName = createContactName(did);
|
const contactName = createContactName(did);
|
||||||
// go to the detail page for this contact
|
// go to the detail page for this contact
|
||||||
await page.locator(`li[data-testid="contactListItem"] h2:has-text("${contactName}") + a`).click();
|
await page.locator(`li[data-testid="contactListItem"] h2:has-text("${contactName}") + span svg.fa-circle-info`).click();
|
||||||
// delete the contact
|
// delete the contact
|
||||||
await page.locator('button > svg.fa-trash-can').click();
|
await page.locator('button > svg.fa-trash-can').click();
|
||||||
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
||||||
|
|||||||
Reference in New Issue
Block a user