Merge branch 'master' into test-playwright
This commit is contained in:
@@ -6,9 +6,10 @@ 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.21] - 2024.08.24
|
||||||
### Added
|
### Added
|
||||||
- Send list of contacts to someone
|
- Send list of contacts to someone.
|
||||||
|
- Prompt for name in pop-up, and send to different contact-sharing screens.
|
||||||
### Changed
|
### Changed
|
||||||
- Moved contact actions from list onto detail page
|
- Moved contact actions from list onto detail page
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari",
|
||||||
"version": "0.3.21-beta",
|
"version": "0.3.21",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari",
|
||||||
"version": "0.3.21-beta",
|
"version": "0.3.21",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.1",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari",
|
||||||
"version": "0.3.21-beta",
|
"version": "0.3.21",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
/* Configure global timeout; default is 30000 milliseconds */
|
/* Configure global timeout; default is 30000 milliseconds */
|
||||||
// the image upload will often not succeed at 5 seconds
|
// the image upload will often not succeed at 5 seconds
|
||||||
timeout: 20000,
|
// timeout: 5000,
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -180,8 +180,9 @@
|
|||||||
"
|
"
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
Yes
|
Yes{{
|
||||||
{{ notification.yesText ? ", " + notification.yesText : "" }}
|
notification.yesText ? ", " + notification.yesText : ""
|
||||||
|
}}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -193,7 +194,7 @@
|
|||||||
"
|
"
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
No {{ notification.noText ? ", " + notification.noText : "" }}
|
No{{ notification.noText ? ", " + notification.noText : "" }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
id="ViewHeading"
|
id="ViewHeading"
|
||||||
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-0.5 bg-black/50 text-white leading-none"
|
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-0.5 bg-black/50 text-white leading-none"
|
||||||
>
|
>
|
||||||
Camera or Other?
|
Add Photo
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
|
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
|
||||||
|
|||||||
96
src/components/UserNameDialog.vue
Normal file
96
src/components/UserNameDialog.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="dialog-overlay">
|
||||||
|
<div class="dialog">
|
||||||
|
<h1 class="text-xl font-bold text-center mb-4">Set Your Name</h1>
|
||||||
|
|
||||||
|
Note that this is not sent to servers. It is only shared with people when
|
||||||
|
you choose to send it to them.
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
|
v-model="givenName"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="block w-full text-center text-lg font-bold 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-2 py-3 rounded-md mb-2"
|
||||||
|
@click="onClickSaveChanges()"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<!-- SHOW ME instead while processing saving changes -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
|
||||||
|
@click="onClickCancel()"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component } from "vue-facing-decorator";
|
||||||
|
|
||||||
|
import { NotificationIface } from "@/constants/app";
|
||||||
|
import { db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class UserNameDialog extends Vue {
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
callback: (string?) => void = () => {};
|
||||||
|
givenName = "";
|
||||||
|
visible = false;
|
||||||
|
|
||||||
|
async open(aCallback?: (name?: string) => void) {
|
||||||
|
this.callback = aCallback || this.callback;
|
||||||
|
await db.open();
|
||||||
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
|
this.givenName = settings?.firstName || "";
|
||||||
|
this.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickSaveChanges() {
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
firstName: this.givenName,
|
||||||
|
});
|
||||||
|
this.visible = false;
|
||||||
|
this.callback(this.givenName);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickCancel() {
|
||||||
|
this.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background-color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -49,8 +49,8 @@ export interface NotificationIface {
|
|||||||
title: string;
|
title: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
noText?: string;
|
noText?: string;
|
||||||
onCancel?: (stopAsking: boolean) => Promise<void>;
|
onCancel?: (stopAsking?: boolean) => Promise<void>;
|
||||||
onNo?: (stopAsking: boolean) => Promise<void>;
|
onNo?: (stopAsking?: boolean) => Promise<void>;
|
||||||
onYes?: () => Promise<void>;
|
onYes?: () => Promise<void>;
|
||||||
promptToStopAsking?: boolean;
|
promptToStopAsking?: boolean;
|
||||||
yesText?: string;
|
yesText?: string;
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
|
import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
|
import { Buffer } from "buffer";
|
||||||
|
import { sha256 } from "ethereum-cryptography/sha256";
|
||||||
import { LRUCache } from "lru-cache";
|
import { LRUCache } from "lru-cache";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
|
|
||||||
import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
|
import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken, deriveAddress, nextDerivationPath } from "@/libs/crypto";
|
||||||
import { NonsensitiveDexie } from "@/db/index";
|
import { NonsensitiveDexie } from "@/db/index";
|
||||||
import { getAccount, getPasskeyExpirationSeconds } from "@/libs/util";
|
import { getAccount, getPasskeyExpirationSeconds } from "@/libs/util";
|
||||||
import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc";
|
import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc";
|
||||||
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
|
||||||
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
||||||
// the object in RegisterAction claims
|
// the object in RegisterAction claims
|
||||||
@@ -925,6 +928,53 @@ export async function createAndSubmitClaim(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateEndorserJwtForAccount(
|
||||||
|
account: Account,
|
||||||
|
isRegistered?: boolean,
|
||||||
|
name?: string,
|
||||||
|
profileImageUrl?: string,
|
||||||
|
) {
|
||||||
|
const publicKeyHex = account.publicKeyHex;
|
||||||
|
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
||||||
|
|
||||||
|
interface UserInfo {
|
||||||
|
name: string;
|
||||||
|
publicEncKey: string;
|
||||||
|
registered: boolean;
|
||||||
|
profileImageUrl?: string;
|
||||||
|
nextPublicEncKeyHash?: string;
|
||||||
|
}
|
||||||
|
const contactInfo = {
|
||||||
|
iat: Date.now(),
|
||||||
|
iss: account.did,
|
||||||
|
own: {
|
||||||
|
name: name ?? "",
|
||||||
|
publicEncKey,
|
||||||
|
registered: !!isRegistered,
|
||||||
|
} as UserInfo,
|
||||||
|
};
|
||||||
|
if (profileImageUrl) {
|
||||||
|
contactInfo.own.profileImageUrl = profileImageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account?.mnemonic && account?.derivationPath) {
|
||||||
|
const newDerivPath = nextDerivationPath(account.derivationPath as string);
|
||||||
|
const nextPublicHex = deriveAddress(
|
||||||
|
account.mnemonic as string,
|
||||||
|
newDerivPath,
|
||||||
|
)[2];
|
||||||
|
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
|
||||||
|
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
|
||||||
|
const nextPublicEncKeyHashBase64 =
|
||||||
|
Buffer.from(nextPublicEncKeyHash).toString("base64");
|
||||||
|
contactInfo.own.nextPublicEncKeyHash = nextPublicEncKeyHashBase64;
|
||||||
|
}
|
||||||
|
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
|
||||||
|
|
||||||
|
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
|
||||||
|
return viewPrefix + vcJwt;
|
||||||
|
}
|
||||||
|
|
||||||
export async function createEndorserJwtForDid(
|
export async function createEndorserJwtForDid(
|
||||||
issuerDid: string,
|
issuerDid: string,
|
||||||
payload: object,
|
payload: object,
|
||||||
|
|||||||
@@ -189,6 +189,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "seed-backup",
|
name: "seed-backup",
|
||||||
component: () => import("../views/SeedBackupView.vue"),
|
component: () => import("../views/SeedBackupView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/share-my-contact-info",
|
||||||
|
name: "share-my-contact-info",
|
||||||
|
component: () => import("@/views/ShareMyContactInfoView.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/shared-photo",
|
path: "/shared-photo",
|
||||||
name: "shared-photo",
|
name: "shared-photo",
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<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">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
||||||
Your Identity
|
Your Identity
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between mb-2 mt-4">
|
||||||
<span />
|
<span />
|
||||||
<span class="whitespace-nowrap">
|
<span class="whitespace-nowrap">
|
||||||
<router-link
|
<router-link
|
||||||
@@ -55,14 +55,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="block w-full text-center text-md bg-amber-200 text-blue-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md bg-amber-200 border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
<router-link
|
<button
|
||||||
:to="{ name: 'new-edit-account' }"
|
@click="
|
||||||
class="inline-block text-md 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-4 py-2 rounded-md"
|
() => $refs.userNameDialog.open((name) => (this.givenName = name))
|
||||||
|
"
|
||||||
|
class="inline-block 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-4 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Set Your Name
|
Set Your Name
|
||||||
</router-link>
|
</button>
|
||||||
|
<UserNameDialog ref="userNameDialog" />
|
||||||
</span>
|
</span>
|
||||||
<div class="flex justify-center mt-4">
|
<div class="flex justify-center mt-4">
|
||||||
<span v-if="profileImageUrl" class="flex justify-between">
|
<span v-if="profileImageUrl" class="flex justify-between">
|
||||||
@@ -129,7 +132,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-slate-500 text-sm font-bold">ID</div>
|
<div class="text-slate-500 text-sm font-bold">ID</div>
|
||||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
<div
|
||||||
|
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
||||||
|
data-testId="didWrapper"
|
||||||
|
>
|
||||||
<code class="truncate">{{ activeDid }}</code>
|
<code class="truncate">{{ activeDid }}</code>
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
@@ -717,6 +723,7 @@ import EntityIcon from "@/components/EntityIcon.vue";
|
|||||||
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
|
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
|
||||||
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 UserNameDialog from "@/components/UserNameDialog.vue";
|
||||||
import {
|
import {
|
||||||
AppString,
|
AppString,
|
||||||
DEFAULT_IMAGE_API_SERVER,
|
DEFAULT_IMAGE_API_SERVER,
|
||||||
@@ -747,7 +754,13 @@ import { getAccount } from "@/libs/util";
|
|||||||
const inputImportFileNameRef = ref<Blob>();
|
const inputImportFileNameRef = ref<Blob>();
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { EntityIcon, ImageMethodDialog, QuickNav, TopMessage },
|
components: {
|
||||||
|
EntityIcon,
|
||||||
|
ImageMethodDialog,
|
||||||
|
QuickNav,
|
||||||
|
TopMessage,
|
||||||
|
UserNameDialog,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class AccountViewView extends Vue {
|
export default class AccountViewView extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Profile"></QuickNav>
|
<QuickNav selected="Profile" />
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<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">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
@@ -25,13 +25,16 @@
|
|||||||
<span class="text-red">Beware!</span>
|
<span class="text-red">Beware!</span>
|
||||||
You aren't sharing your name, so quickly
|
You aren't sharing your name, so quickly
|
||||||
<br />
|
<br />
|
||||||
<router-link
|
<span
|
||||||
:to="{ name: 'new-edit-account' }"
|
@click="
|
||||||
|
() => $refs.userNameDialog.open((name) => (this.givenName = name))
|
||||||
|
"
|
||||||
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-1.5 py-1 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-1.5 py-1 rounded-md"
|
||||||
>
|
>
|
||||||
click here to set it for them.
|
click here to set it for them.
|
||||||
</router-link>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
<UserNameDialog ref="userNameDialog" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
class="flex justify-center"
|
class="flex justify-center"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
Click this or QR code to copy your contact URL to your clipboard.
|
Click the QR code to copy your contact info to your clipboard.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="activeDid" class="text-center">
|
<div v-else-if="activeDid" class="text-center">
|
||||||
@@ -96,6 +99,7 @@ import { QrcodeStream } from "vue-qrcode-reader";
|
|||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import UserNameDialog from "@/components/UserNameDialog.vue";
|
||||||
import { NotificationIface } from "@/constants/app";
|
import { NotificationIface } from "@/constants/app";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
@@ -109,6 +113,7 @@ import {
|
|||||||
CONTACT_URL_PREFIX,
|
CONTACT_URL_PREFIX,
|
||||||
createEndorserJwtForDid,
|
createEndorserJwtForDid,
|
||||||
ENDORSER_JWT_URL_LOCATION,
|
ENDORSER_JWT_URL_LOCATION,
|
||||||
|
generateEndorserJwtForAccount,
|
||||||
isDid,
|
isDid,
|
||||||
register,
|
register,
|
||||||
setVisibilityUtil,
|
setVisibilityUtil,
|
||||||
@@ -120,6 +125,7 @@ import { ETHR_DID_PREFIX } from "@/libs/crypto/vc";
|
|||||||
QrcodeStream,
|
QrcodeStream,
|
||||||
QRCodeVue3,
|
QRCodeVue3,
|
||||||
QuickNav,
|
QuickNav,
|
||||||
|
UserNameDialog,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ContactQRScanShow extends Vue {
|
export default class ContactQRScanShow extends Vue {
|
||||||
@@ -157,7 +163,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
own: {
|
own: {
|
||||||
name:
|
name:
|
||||||
(settings?.firstName || "") +
|
(settings?.firstName || "") +
|
||||||
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
|
(settings?.lastName ? ` ${settings.lastName}` : ""), // lastName is deprecated, pre v 0.1.3
|
||||||
publicEncKey,
|
publicEncKey,
|
||||||
profileImageUrl: settings?.profileImageUrl,
|
profileImageUrl: settings?.profileImageUrl,
|
||||||
registered: settings?.isRegistered,
|
registered: settings?.isRegistered,
|
||||||
@@ -182,7 +188,18 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
const vcJwt = await createEndorserJwtForDid(this.activeDid, contactInfo);
|
const vcJwt = await createEndorserJwtForDid(this.activeDid, contactInfo);
|
||||||
|
|
||||||
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
|
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
|
||||||
this.qrValue = viewPrefix + vcJwt;
|
viewPrefix + vcJwt;
|
||||||
|
|
||||||
|
const name =
|
||||||
|
(settings?.firstName || "") +
|
||||||
|
(settings?.lastName ? ` ${settings.lastName}` : ""); // lastName is deprecated, pre v 0.1.3
|
||||||
|
|
||||||
|
this.qrValue = await generateEndorserJwtForAccount(
|
||||||
|
account,
|
||||||
|
!!settings?.isRegistered,
|
||||||
|
name,
|
||||||
|
settings?.profileImageUrl as string,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
<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">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
||||||
Your Contacts
|
Your Contacts
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="flex justify-between py-2">
|
<div class="flex justify-between py-2 mt-8">
|
||||||
<span />
|
<span />
|
||||||
<span>
|
<span>
|
||||||
<a
|
<a
|
||||||
@@ -1128,8 +1128,8 @@ export default class ContactsView extends Vue {
|
|||||||
this.contactsSelected.includes(c.did),
|
this.contactsSelected.includes(c.did),
|
||||||
);
|
);
|
||||||
const message =
|
const message =
|
||||||
"To add contacts, paste this into the box on the 'People' screen.\n\n" +
|
"To add contacts, paste this into the box on the 'Contacts' screen.\n\n" +
|
||||||
JSON.stringify(selectedContacts, null, 2);
|
JSON.stringify(selectedContacts);
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(message)
|
.copy(message)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -1138,7 +1138,7 @@ export default class ContactsView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Copied",
|
title: "Copied",
|
||||||
text: "Those contacts were copied to the clipboard. Have them paste it in the box on their 'People' screen.",
|
text: "Those contacts were copied to the clipboard. Have them paste it in the box on their 'Contacts' screen.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Discover"></QuickNav>
|
<QuickNav selected="Discover" />
|
||||||
<TopMessage />
|
<TopMessage />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<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">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
||||||
Discover Projects
|
Discover Projects
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Quick Search -->
|
<!-- Quick Search -->
|
||||||
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchSelected()">
|
<div
|
||||||
|
id="QuickSearch"
|
||||||
|
class="mt-8 mb-4 flex"
|
||||||
|
v-on:keyup.enter="searchSelected()"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="searchTerms"
|
v-model="searchTerms"
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
|
||||||
{{ AppString.APP_NAME }}
|
{{ AppString.APP_NAME }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- prompt to install notifications -->
|
<!-- prompt to install notifications with notificationsSupported, which we're making an advanced feature -->
|
||||||
<div class="mb-8">
|
<div class="mb-8 mt-8">
|
||||||
<div
|
<div
|
||||||
v-if="!notificationsSupported()"
|
v-if="false"
|
||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||||
>
|
>
|
||||||
<p style="display: inline; align-items: center">
|
<p style="display: inline; align-items: center">
|
||||||
@@ -86,13 +86,16 @@
|
|||||||
>
|
>
|
||||||
<!-- activeDid && !isRegistered -->
|
<!-- activeDid && !isRegistered -->
|
||||||
To share, someone must register you.
|
To share, someone must register you.
|
||||||
<router-link
|
<div class="block text-center">
|
||||||
:to="{ name: 'contact-qr' }"
|
<button
|
||||||
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
@click="showNameDialog()"
|
||||||
>
|
class="text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
||||||
Show Them {{ PASSKEYS_ENABLED ? "Default" : "Your" }} Identifier
|
>
|
||||||
Info
|
Show them {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
|
||||||
</router-link>
|
info
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<UserNameDialog ref="userNameDialog" />
|
||||||
<div v-if="PASSKEYS_ENABLED" class="flex justify-end w-full">
|
<div v-if="PASSKEYS_ENABLED" class="flex justify-end w-full">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'start' }"
|
:to="{ name: 'start' }"
|
||||||
@@ -107,12 +110,20 @@
|
|||||||
<!-- activeDid && isRegistered -->
|
<!-- activeDid && isRegistered -->
|
||||||
|
|
||||||
<!-- show the actions for recognizing a give -->
|
<!-- show the actions for recognizing a give -->
|
||||||
<div class="mb-4">
|
<div class="flex justify-between">
|
||||||
<h2 class="text-xl font-bold">Record Something Given By:</h2>
|
<h2 class="text-xl font-bold">Record Something Given By:</h2>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
@click="openGiftedPrompts()"
|
||||||
|
class="block text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Ideas...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5"
|
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mt-4"
|
||||||
>
|
>
|
||||||
<li @click="openDialog()">
|
<li @click="openDialog()">
|
||||||
<img
|
<img
|
||||||
@@ -126,7 +137,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-for="contact in allContacts.slice(0, 7)"
|
v-for="contact in allContacts.slice(0, 6)"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
@click="openDialog(contact)"
|
@click="openDialog(contact)"
|
||||||
>
|
>
|
||||||
@@ -141,23 +152,16 @@
|
|||||||
{{ contact.name || contact.did }}
|
{{ contact.name || contact.did }}
|
||||||
</h3>
|
</h3>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<router-link
|
||||||
|
v-if="allContacts.length >= 6"
|
||||||
|
:to="{ name: 'contact-gift' }"
|
||||||
|
class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
||||||
|
>
|
||||||
|
Choose From All Contacts
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<router-link
|
|
||||||
v-if="allContacts.length >= 7"
|
|
||||||
:to="{ name: 'contact-gift' }"
|
|
||||||
class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
|
||||||
>
|
|
||||||
Choose From All Contacts
|
|
||||||
</router-link>
|
|
||||||
<button
|
|
||||||
@click="openGiftedPrompts()"
|
|
||||||
class="block text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
|
||||||
>
|
|
||||||
Ideas...
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +172,7 @@
|
|||||||
<FeedFilters ref="feedFilters" />
|
<FeedFilters ref="feedFilters" />
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4 mb-4">
|
||||||
<div class="flex items-center mb-4">
|
<div class="flex items-center mb-4">
|
||||||
<h2 class="text-xl font-bold">Latest Activity</h2>
|
<h2 class="text-xl font-bold">Latest Activity</h2>
|
||||||
<button @click="openFeedFilters()" class="block text-center ml-auto">
|
<button @click="openFeedFilters()" class="block text-center ml-auto">
|
||||||
@@ -312,6 +316,7 @@ import FeedFilters from "@/components/FeedFilters.vue";
|
|||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
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 UserNameDialog from "@/components/UserNameDialog.vue";
|
||||||
import {
|
import {
|
||||||
AppString,
|
AppString,
|
||||||
NotificationIface,
|
NotificationIface,
|
||||||
@@ -369,6 +374,7 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
|||||||
EntityIcon,
|
EntityIcon,
|
||||||
InfiniteScroll,
|
InfiniteScroll,
|
||||||
TopMessage,
|
TopMessage,
|
||||||
|
UserNameDialog,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class HomeView extends Vue {
|
export default class HomeView extends Vue {
|
||||||
@@ -425,6 +431,7 @@ export default class HomeView extends Vue {
|
|||||||
this.showShortcutBvc = !!settings?.showShortcutBvc;
|
this.showShortcutBvc = !!settings?.showShortcutBvc;
|
||||||
|
|
||||||
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
|
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
|
||||||
|
console.log("getting through mounted");
|
||||||
|
|
||||||
// someone may have have registered after sharing contact info, so recheck
|
// someone may have have registered after sharing contact info, so recheck
|
||||||
if (!this.isRegistered && this.activeDid) {
|
if (!this.isRegistered && this.activeDid) {
|
||||||
@@ -448,7 +455,7 @@ export default class HomeView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this returns a Promise but we don't need to wait for it
|
// this returns a Promise but we don't need to wait for it
|
||||||
await this.updateAllFeed();
|
this.updateAllFeed();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -494,7 +501,7 @@ export default class HomeView extends Vue {
|
|||||||
|
|
||||||
this.feedData = [];
|
this.feedData = [];
|
||||||
this.feedPreviousOldestId = undefined;
|
this.feedPreviousOldestId = undefined;
|
||||||
this.updateAllFeed();
|
await this.updateAllFeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -506,7 +513,7 @@ export default class HomeView extends Vue {
|
|||||||
// and the InfiniteScroll component triggers a load before finished.
|
// and the InfiniteScroll component triggers a load before finished.
|
||||||
// One alternative is to totally separate the project link loading.
|
// One alternative is to totally separate the project link loading.
|
||||||
if (payload && !this.isFeedLoading) {
|
if (payload && !this.isFeedLoading) {
|
||||||
this.updateAllFeed();
|
await this.updateAllFeed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,6 +533,7 @@ export default class HomeView extends Vue {
|
|||||||
async updateAllFeed() {
|
async updateAllFeed() {
|
||||||
this.isFeedLoading = true;
|
this.isFeedLoading = true;
|
||||||
let endOfResults = true;
|
let endOfResults = true;
|
||||||
|
console.log("about to retrieveGives");
|
||||||
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
|
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
|
||||||
.then(async (results) => {
|
.then(async (results) => {
|
||||||
if (results.data.length > 0) {
|
if (results.data.length > 0) {
|
||||||
@@ -619,7 +627,7 @@ export default class HomeView extends Vue {
|
|||||||
});
|
});
|
||||||
if (this.feedData.length === 0 && !endOfResults) {
|
if (this.feedData.length === 0 && !endOfResults) {
|
||||||
// repeat until there's at least some data
|
// repeat until there's at least some data
|
||||||
this.updateAllFeed();
|
await this.updateAllFeed();
|
||||||
}
|
}
|
||||||
this.isFeedLoading = false;
|
this.isFeedLoading = false;
|
||||||
}
|
}
|
||||||
@@ -769,5 +777,36 @@ export default class HomeView extends Vue {
|
|||||||
computeKnownPersonIconStyleClassNames(known: boolean) {
|
computeKnownPersonIconStyleClassNames(known: boolean) {
|
||||||
return known ? "text-slate-500" : "text-slate-100";
|
return known ? "text-slate-500" : "text-slate-100";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showNameDialog() {
|
||||||
|
if (!this.givenName) {
|
||||||
|
(this.$refs.userNameDialog as UserNameDialog).open(() => {
|
||||||
|
this.promptForShareMethod();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.promptForShareMethod();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
promptForShareMethod() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "confirm",
|
||||||
|
title: "Are you nearby with cameras?",
|
||||||
|
text: "If so, we'll use those with QR codes to share.",
|
||||||
|
onCancel: async () => {},
|
||||||
|
onNo: async () => {
|
||||||
|
(this.$router as Router).push({ name: "share-my-contact-info" });
|
||||||
|
},
|
||||||
|
onYes: async () => {
|
||||||
|
(this.$router as Router).push({ name: "contact-qr" });
|
||||||
|
},
|
||||||
|
noText: "we will share another way",
|
||||||
|
yesText: "we are nearby with cameras",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center py-12">
|
<div class="flex justify-center py-12">
|
||||||
<span />
|
<div />
|
||||||
<span v-if="loading">
|
<div v-if="loading">
|
||||||
<span class="text-xl">Creating... </span>
|
<span class="text-xl">Creating... </span>
|
||||||
<fa
|
<fa
|
||||||
icon="spinner"
|
icon="spinner"
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
color="green"
|
color="green"
|
||||||
size="128"
|
size="128"
|
||||||
></fa>
|
></fa>
|
||||||
</span>
|
</div>
|
||||||
<span v-else>
|
<div v-else>
|
||||||
<span class="text-xl">Created!</span>
|
<span class="text-xl">Created!</span>
|
||||||
<fa
|
<fa
|
||||||
icon="burst"
|
icon="burst"
|
||||||
@@ -45,8 +45,8 @@
|
|||||||
--fa-beat-scale: 6;
|
--fa-beat-scale: 6;
|
||||||
"
|
"
|
||||||
></fa>
|
></fa>
|
||||||
</span>
|
</div>
|
||||||
<span />
|
<div />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Projects"></QuickNav>
|
<QuickNav selected="Projects" />
|
||||||
<TopMessage />
|
<TopMessage />
|
||||||
|
|
||||||
<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">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light">Your Ideas</h1>
|
||||||
Your Ideas
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Result Tabs -->
|
<!-- Result Tabs -->
|
||||||
<div class="text-center text-slate-500 border-b border-slate-300">
|
<div class="text-center text-slate-500 border-b border-slate-300 mt-8">
|
||||||
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
|
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
|
|||||||
123
src/views/ShareMyContactInfoView.vue
Normal file
123
src/views/ShareMyContactInfoView.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav />
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div>
|
||||||
|
<!-- Back -->
|
||||||
|
<div class="text-lg text-center font-light relative px-7">
|
||||||
|
<h1
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
@click="$router.back()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" class="fa-fw" />
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Heading -->
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
||||||
|
Share Your Contact Info
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center mt-8">
|
||||||
|
<button
|
||||||
|
class="block w-fit text-center text-lg font-bold 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-3 rounded-md"
|
||||||
|
@click="onClickShare()"
|
||||||
|
>
|
||||||
|
Copy to Clipboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="ml-12">
|
||||||
|
<div class="mt-8">Click to copy your info, then send it to them.</div>
|
||||||
|
<div>
|
||||||
|
They will paste it in the input box on the Contacts
|
||||||
|
<fa icon="users" /> screen.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import * as R from "ramda";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { Router } from "vue-router";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
|
import { NotificationIface } from "@/constants/app";
|
||||||
|
import { accountsDB, db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
|
import { generateEndorserJwtForAccount } from "@/libs/endorserServer";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { QuickNav, TopMessage },
|
||||||
|
})
|
||||||
|
export default class ShareMyContactInfoView extends Vue {
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
async onClickShare() {
|
||||||
|
await db.open();
|
||||||
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
|
const activeDid = settings?.activeDid || "";
|
||||||
|
const givenName = settings?.firstName || "";
|
||||||
|
const isRegistered = !!settings?.isRegistered;
|
||||||
|
const profileImageUrl = settings?.profileImageUrl || "";
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
const account = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
|
|
||||||
|
const numContacts = await db.contacts.count();
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
const message = await generateEndorserJwtForAccount(
|
||||||
|
account,
|
||||||
|
isRegistered,
|
||||||
|
givenName,
|
||||||
|
profileImageUrl,
|
||||||
|
);
|
||||||
|
useClipboard()
|
||||||
|
.copy(message)
|
||||||
|
.then(() => {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "Copied",
|
||||||
|
text: "Your contact info was copied to the clipboard. Have them paste it in the box on their 'Contacts' screen.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
if (numContacts > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Share Other Contacts",
|
||||||
|
text: "You may want to share some of your contacts with them. Select them below to copy and send.",
|
||||||
|
},
|
||||||
|
10000,
|
||||||
|
);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(this.$router as Router).push({ name: "contacts" });
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "error",
|
||||||
|
title: "Error",
|
||||||
|
text: "No account was found for the active DID.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -58,6 +58,7 @@
|
|||||||
<a
|
<a
|
||||||
@click="onClickNewSeed()"
|
@click="onClickNewSeed()"
|
||||||
class="block w-full text-center text-lg 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-2 py-3 rounded-md mb-2 cursor-pointer"
|
class="block w-full text-center text-lg 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-2 py-3 rounded-md mb-2 cursor-pointer"
|
||||||
|
data-testId="newSeed"
|
||||||
>
|
>
|
||||||
Generate one with a new seed
|
Generate one with a new seed
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { generateEthrUser, importUser } from './testUtils';
|
||||||
test('Confirm usage of test API (may fail if you are running your own Time Safari)', async ({ page }, testInfo) => {
|
|
||||||
// Load account view
|
|
||||||
await page.goto('./account');
|
|
||||||
await page.getByRole('heading', { name: 'Advanced' }).click();
|
|
||||||
|
|
||||||
// look into the config file: if it starts Time Safari, it might say which server it should set by default
|
|
||||||
const webServer = testInfo.config.webServer;
|
|
||||||
const endorserWords = webServer?.command.split(' ');
|
|
||||||
const ENDORSER_ENV_NAME = 'VITE_DEFAULT_ENDORSER_API_SERVER';
|
|
||||||
const endorserTerm = endorserWords?.find(word => word.startsWith(ENDORSER_ENV_NAME + '='));
|
|
||||||
const endorserTermInConfig = endorserTerm?.substring(ENDORSER_ENV_NAME.length + 1);
|
|
||||||
|
|
||||||
const endorserServer = endorserTermInConfig || 'https://test-api.endorser.ch';
|
|
||||||
await expect(page.getByRole('textbox').nth(1)).toHaveValue(endorserServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Check activity feed', async ({ page }) => {
|
test('Check activity feed', async ({ page }) => {
|
||||||
// Load app homepage
|
// Load app homepage
|
||||||
@@ -38,14 +23,6 @@ test('Check discover results', async ({ page }) => {
|
|||||||
await page.locator('ul#listDiscoverResults li.border-b:nth-child(20)').scrollIntoViewIfNeeded();
|
await page.locator('ul#listDiscoverResults li.border-b:nth-child(20)').scrollIntoViewIfNeeded();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Check no-ID messaging in homepage', async ({ page }) => {
|
|
||||||
// Load app homepage
|
|
||||||
await page.goto('./');
|
|
||||||
|
|
||||||
// Check 'someone must register you' notice
|
|
||||||
await expect(page.getByText('To share, someone must register you.')).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Check no-ID messaging in account', async ({ page }) => {
|
test('Check no-ID messaging in account', async ({ page }) => {
|
||||||
// Load account view
|
// Load account view
|
||||||
await page.goto('./account');
|
await page.goto('./account');
|
||||||
@@ -73,9 +50,66 @@ test('Check ID generation', async ({ page }) => {
|
|||||||
// Wait for activity feed to start loading, as a delay
|
// Wait for activity feed to start loading, as a delay
|
||||||
await expect(page.locator('ul#listLatestActivity li:nth-child(10)')).toBeVisible();
|
await expect(page.locator('ul#listLatestActivity li:nth-child(10)')).toBeVisible();
|
||||||
|
|
||||||
|
// Check 'someone must register you' notice
|
||||||
|
await expect(page.getByText('To share, someone must register you.')).toBeVisible();
|
||||||
|
|
||||||
// Go back to Account view
|
// Go back to Account view
|
||||||
await page.goto('./account');
|
await page.goto('./account');
|
||||||
|
|
||||||
// Check that ID is now generated
|
// Check that ID is now generated
|
||||||
await expect(page.locator('#sectionIdentityDetails code.truncate')).toContainText('did:ethr:');
|
await expect(page.locator('#sectionIdentityDetails code.truncate')).toContainText('did:ethr:');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('Check setting name & sharing info', async ({ page }) => {
|
||||||
|
// Load homepage to trigger ID generation (?)
|
||||||
|
await page.goto('./');
|
||||||
|
// Check 'someone must register you' notice
|
||||||
|
await expect(page.getByText('someone must register you.')).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: /Show them/}).click();
|
||||||
|
// fill in a name
|
||||||
|
await expect(page.getByText('Set Your Name')).toBeVisible();
|
||||||
|
await page.getByRole('textbox').fill('Me Test User');
|
||||||
|
await page.locator('button:has-text("Save")').click();
|
||||||
|
await expect(page.getByText('share another way')).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: /share another way/ }).click();
|
||||||
|
await expect(page.getByRole('button', { name: 'copy to clipboard' })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'copy to clipboard' }).click();
|
||||||
|
await expect(page.getByText('contact info was copied')).toBeVisible();
|
||||||
|
// dismiss alert and wait for it to go away
|
||||||
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click();
|
||||||
|
await expect(page.getByText('contact info was copied')).toBeHidden();
|
||||||
|
// check that they're on the Contacts screen
|
||||||
|
await expect(page.getByText('your contacts')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Confirm usage of test API (may fail if you are running your own Time Safari)', async ({ page }, testInfo) => {
|
||||||
|
// Load account view
|
||||||
|
await page.goto('./account');
|
||||||
|
await page.getByRole('heading', { name: 'Advanced' }).click();
|
||||||
|
|
||||||
|
// look into the config file: if it starts Time Safari, it might say which server it should set by default
|
||||||
|
const webServer = testInfo.config.webServer;
|
||||||
|
const endorserWords = webServer?.command.split(' ');
|
||||||
|
const ENDORSER_ENV_NAME = 'VITE_DEFAULT_ENDORSER_API_SERVER';
|
||||||
|
const endorserTerm = endorserWords?.find(word => word.startsWith(ENDORSER_ENV_NAME + '='));
|
||||||
|
const endorserTermInConfig = endorserTerm?.substring(ENDORSER_ENV_NAME.length + 1);
|
||||||
|
|
||||||
|
const endorserServer = endorserTermInConfig || 'https://test-api.endorser.ch';
|
||||||
|
await expect(page.getByRole('textbox').nth(1)).toHaveValue(endorserServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check User 0 can register a random person', async ({ page }) => {
|
||||||
|
await importUser(page, '00');
|
||||||
|
const newDid = await generateEthrUser(page);
|
||||||
|
expect(newDid).toContain('did:ethr:');
|
||||||
|
|
||||||
|
await page.goto('./');
|
||||||
|
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
|
||||||
|
await page.getByPlaceholder('What was given').fill('Access!');
|
||||||
|
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||||
|
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
||||||
|
// now ensure that alert goes away
|
||||||
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
|
||||||
|
await expect(page.getByText('That gift was recorded.')).toBeHidden();
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,12 +7,22 @@ test('Check usage limits', async ({ page }) => {
|
|||||||
await expect(page.locator('div.bg-slate-100.rounded-md').filter({ hasText: 'Usage Limits' })).toBeHidden();
|
await expect(page.locator('div.bg-slate-100.rounded-md').filter({ hasText: 'Usage Limits' })).toBeHidden();
|
||||||
|
|
||||||
// Import user 01
|
// Import user 01
|
||||||
await importUser(page, '01');
|
const did = await importUser(page, '01');
|
||||||
|
|
||||||
// Verify that "Usage Limits" section is visible
|
// Verify that "Usage Limits" section is visible
|
||||||
await expect(page.locator('#sectionUsageLimits')).toBeVisible();
|
await expect(page.locator('#sectionUsageLimits')).toBeVisible();
|
||||||
|
await expect(page.locator('#sectionUsageLimits')).toContainText('You have done');
|
||||||
|
await expect(page.locator('#sectionUsageLimits')).toContainText('You have uploaded');
|
||||||
|
|
||||||
await expect(page.getByText('Your claims counter resets')).toBeVisible();
|
await expect(page.getByText('Your claims counter resets')).toBeVisible();
|
||||||
await expect(page.getByText('Your registration counter resets')).toBeVisible();
|
await expect(page.getByText('Your registration counter resets')).toBeVisible();
|
||||||
await expect(page.getByText('Your image counter resets')).toBeVisible();
|
await expect(page.getByText('Your image counter resets')).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: 'Recheck Limits' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Recheck Limits' })).toBeVisible();
|
||||||
|
|
||||||
|
// Set name
|
||||||
|
await page.getByRole('button', { name: 'Set Your Name' }).click();
|
||||||
|
const name = 'User ' + did.slice(11, 14);
|
||||||
|
await page.getByPlaceholder('Name').fill(name);
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -2,6 +2,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { importUser } from './testUtils';
|
import { importUser } from './testUtils';
|
||||||
|
|
||||||
test('Add contact, record gift, confirm gift', async ({ page }) => {
|
test('Add contact, record gift, confirm gift', async ({ page }) => {
|
||||||
|
|
||||||
// Generate a random string of 16 characters
|
// Generate a random string of 16 characters
|
||||||
let randomString = Math.random().toString(36).substring(2, 18);
|
let randomString = Math.random().toString(36).substring(2, 18);
|
||||||
|
|
||||||
@@ -31,8 +32,9 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
|||||||
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, User #000');
|
||||||
await page.locator('button > svg.fa-plus').click();
|
await page.locator('button > svg.fa-plus').click();
|
||||||
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
||||||
|
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
||||||
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
|
||||||
|
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('User #000');
|
||||||
@@ -94,8 +96,8 @@ test('Add contact, copy details, delete, and import various ways', async ({ page
|
|||||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39, User #111');
|
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39, User #111');
|
||||||
await page.locator('button > svg.fa-plus').click();
|
await page.locator('button > svg.fa-plus').click();
|
||||||
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
||||||
await page.locator('div[role="alert"] button:has-text("No")').click();
|
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
||||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click();
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
|
||||||
// wait for the alert to disappear
|
// wait for the alert to disappear
|
||||||
await expect(page.locator('div[role="alert"]')).toBeHidden();
|
await expect(page.locator('div[role="alert"]')).toBeHidden();
|
||||||
|
|
||||||
@@ -103,8 +105,8 @@ test('Add contact, copy details, delete, and import various ways', async ({ page
|
|||||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b, User #222, asdf1234');
|
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b, User #222, asdf1234');
|
||||||
await page.locator('button > svg.fa-plus').click();
|
await page.locator('button > svg.fa-plus').click();
|
||||||
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
||||||
await page.locator('div[role="alert"] button:has-text("No")').click();
|
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
||||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click();
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
|
||||||
await expect(page.locator('div[role="alert"]')).toBeHidden();
|
await expect(page.locator('div[role="alert"]')).toBeHidden();
|
||||||
|
|
||||||
await expect(page.getByTestId('contactListItem')).toHaveCount(2);
|
await expect(page.getByTestId('contactListItem')).toHaveCount(2);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { expect, Page } from '@playwright/test';
|
import { expect, Page } from '@playwright/test';
|
||||||
|
|
||||||
export async function importUser(page: Page, id?: string): Promise<void> {
|
// Import the seed and switch to the user based on the ID.
|
||||||
|
// '01' -> 111
|
||||||
|
// otherwise -> 000
|
||||||
|
export async function importUser(page: Page, id?: string): Promise<string> {
|
||||||
let seedPhrase, userName, did;
|
let seedPhrase, userName, did;
|
||||||
|
|
||||||
// Set seed phrase and DID based on user ID
|
// Set seed phrase and DID based on user ID
|
||||||
@@ -21,14 +24,45 @@ export async function importUser(page: Page, id?: string): Promise<void> {
|
|||||||
await page.getByText('You have a seed').click();
|
await page.getByText('You have a seed').click();
|
||||||
await page.getByPlaceholder('Seed Phrase').fill(seedPhrase);
|
await page.getByPlaceholder('Seed Phrase').fill(seedPhrase);
|
||||||
await page.getByRole('button', { name: 'Import' }).click();
|
await page.getByRole('button', { name: 'Import' }).click();
|
||||||
await expect(page.locator('#sectionUsageLimits')).toContainText('You have done');
|
|
||||||
await expect(page.locator('#sectionUsageLimits')).toContainText('You have uploaded');
|
|
||||||
|
|
||||||
// Set name
|
|
||||||
await page.getByRole('link', { name: 'Set Your Name' }).click();
|
|
||||||
await page.getByPlaceholder('Name').fill(userName);
|
|
||||||
await page.getByRole('button', { name: 'Save Changes' }).click();
|
|
||||||
|
|
||||||
// Check DID
|
// Check DID
|
||||||
await expect(page.getByRole('code')).toContainText(did);
|
await expect(page.getByRole('code')).toContainText(did);
|
||||||
|
// ... and ensure the app retrieves the registration status
|
||||||
|
await expect(page.getByText('Your claims counter resets')).toBeVisible();
|
||||||
|
return did;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is to switch to someone already in the identity table. It doesn't include registration.
|
||||||
|
export async function switchToUser(page: Page, did: string): Promise<void> {
|
||||||
|
await page.goto('./account');
|
||||||
|
await page.getByRole('heading', { name: 'Advanced' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Switch Identifier' }).click();
|
||||||
|
await page.getByRole('code', { name: did }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new random user and register them.
|
||||||
|
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
|
||||||
|
export async function generateEthrUser(page: Page): Promise<string> {
|
||||||
|
await page.goto('./start');
|
||||||
|
await page.getByTestId('newSeed').click();
|
||||||
|
await expect(page.locator('span:has-text("Created")')).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto('./account');
|
||||||
|
// wait until the DID shows on the page in the 'did' element
|
||||||
|
const didElem = await page.getByTestId('didWrapper').locator('code:has-text("did:")');
|
||||||
|
const newDid = await didElem.innerText();
|
||||||
|
|
||||||
|
await importUser(page, '000'); // switch to user 000
|
||||||
|
|
||||||
|
await page.goto('./contacts');
|
||||||
|
const threeChars = newDid.slice(11, 14);
|
||||||
|
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(`${newDid}, User ${threeChars}`);
|
||||||
|
await page.locator('button > svg.fa-plus').click();
|
||||||
|
await page.locator('li', { hasText: threeChars }).click();
|
||||||
|
// register them
|
||||||
|
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
||||||
|
// wait for it to disappear because the next steps may depend on alerts being gone
|
||||||
|
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
|
||||||
|
|
||||||
|
return newDid;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user