finish the loading of an invite RegisterAction when clicking on a link
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
|
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
|
||||||
|
VITE_APP_SERVER=https://timesafari.app
|
||||||
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H
|
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H
|
||||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
|
|
||||||
## [0.3.29-beta]
|
## [0.3.29-beta]
|
||||||
|
### Added
|
||||||
|
- Invite for a contact to join immediately
|
||||||
### Changed
|
### Changed
|
||||||
- Send signed data to nostr endpoints to verify public key ownership.
|
- Send signed data to nostr endpoints to verify public key ownership.
|
||||||
### Changed in DB or environment
|
### Changed in DB or environment
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ npm run test-all
|
|||||||
```
|
```
|
||||||
# (Let's replace this with a .env.development or .env.staging file.)
|
# (Let's replace this with a .env.development or .env.staging file.)
|
||||||
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
|
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
|
||||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
|
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
* Production
|
* Production
|
||||||
|
|||||||
83
src/components/ContactNameDialog.vue
Normal file
83
src/components/ContactNameDialog.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<!-- similar to ContactNameDialog -->
|
||||||
|
<template>
|
||||||
|
<div v-if="visible" class="dialog-overlay">
|
||||||
|
<div class="dialog">
|
||||||
|
<h1 class="text-xl font-bold text-center mb-4">Inviter's Name</h1>
|
||||||
|
Note that their name is only stored on this device.
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
|
v-model="newText"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class UserNameDialog extends Vue {
|
||||||
|
callback: (name?: string) => void = () => {};
|
||||||
|
newText = "";
|
||||||
|
visible = false;
|
||||||
|
|
||||||
|
async open(aCallback?: (name?: string) => void) {
|
||||||
|
this.callback = aCallback || this.callback;
|
||||||
|
this.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickSaveChanges() {
|
||||||
|
this.visible = false;
|
||||||
|
this.callback(this.newText);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickCancel() {
|
||||||
|
this.visible = false;
|
||||||
|
this.callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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>
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
|
<!-- similar to ContactNameDialog -->
|
||||||
<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">Set Your Name</h1>
|
<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
|
This is not sent to servers. It is only shared with people when you send
|
||||||
you choose to send it to them.
|
it to them.
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
@@ -21,7 +22,6 @@
|
|||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<!-- SHOW ME instead while processing saving changes -->
|
|
||||||
<button
|
<button
|
||||||
type="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"
|
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"
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ export enum AppString {
|
|||||||
NO_CONTACT_NAME = "(no name)",
|
NO_CONTACT_NAME = "(no name)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const APP_SERVER =
|
||||||
|
import.meta.env.VITE_APP_SERVER || "https://timesafari.app";
|
||||||
|
|
||||||
export const DEFAULT_ENDORSER_API_SERVER =
|
export const DEFAULT_ENDORSER_API_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
|
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
|
||||||
AppString.TEST_ENDORSER_API_SERVER;
|
AppString.TEST_ENDORSER_API_SERVER;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export async function createEndorserJwtForKey(
|
|||||||
issuer: account.did,
|
issuer: account.did,
|
||||||
signer: signer,
|
signer: signer,
|
||||||
expiresIn: undefined as number | undefined,
|
expiresIn: undefined as number | undefined,
|
||||||
}
|
};
|
||||||
if (expiresIn) {
|
if (expiresIn) {
|
||||||
options.expiresIn = expiresIn;
|
options.expiresIn = expiresIn;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export interface ClaimResult {
|
|||||||
error: { code: string; message: string };
|
error: { code: string; message: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// similar to VerifiableCredentialSubject... maybe rename this
|
||||||
export interface GenericVerifiableCredential {
|
export interface GenericVerifiableCredential {
|
||||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||||
"@type": string;
|
"@type": string;
|
||||||
@@ -56,8 +57,6 @@ export interface GenericVerifiableCredential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
||||||
"@context": string;
|
|
||||||
"@type": string;
|
|
||||||
claim: T;
|
claim: T;
|
||||||
claimType?: string;
|
claimType?: string;
|
||||||
handleId: string;
|
handleId: string;
|
||||||
@@ -68,8 +67,6 @@ export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
|||||||
}
|
}
|
||||||
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
||||||
{
|
{
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
|
||||||
"@type": "",
|
|
||||||
claim: { "@type": "" },
|
claim: { "@type": "" },
|
||||||
handleId: "",
|
handleId: "",
|
||||||
id: "",
|
id: "",
|
||||||
@@ -223,11 +220,21 @@ export interface ImageRateLimits {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface VerifiableCredential {
|
export interface VerifiableCredential {
|
||||||
|
exp?: number;
|
||||||
|
iat: number;
|
||||||
|
iss: string;
|
||||||
|
vc: {
|
||||||
|
"@context": string[];
|
||||||
|
type: string[];
|
||||||
|
credentialSubject: VerifiableCredentialSubject;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// similar to GenericVerifiableCredential... maybe replace that one
|
||||||
|
export interface VerifiableCredentialSubject {
|
||||||
"@context": string;
|
"@context": string;
|
||||||
"@type": string;
|
"@type": string;
|
||||||
name: string;
|
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
description: string;
|
|
||||||
identifier?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorldProperties {
|
export interface WorldProperties {
|
||||||
@@ -235,13 +242,14 @@ export interface WorldProperties {
|
|||||||
endTime?: string;
|
endTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AKA Registration & RegisterAction
|
||||||
export interface RegisterVerifiableCredential {
|
export interface RegisterVerifiableCredential {
|
||||||
"@context": string;
|
"@context": typeof SCHEMA_ORG_CONTEXT;
|
||||||
"@type": string;
|
"@type": "RegisterAction";
|
||||||
agent: { identifier: string };
|
agent: { identifier: string };
|
||||||
identifier?: string;
|
identifier?: string; // used for invites (when participant ID isn't known)
|
||||||
object: string;
|
object: string;
|
||||||
participant?: { identifier: string };
|
participant?: { identifier: string }; // used when person is known (not an invite)
|
||||||
}
|
}
|
||||||
|
|
||||||
// now for some of the error & other wrapper types
|
// now for some of the error & other wrapper types
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await updateDefaultSettings({ activeDid: newId.did });
|
await updateDefaultSettings({ activeDid: newId.did });
|
||||||
console.log("Updated default settings in util");
|
//console.log("Updated default settings in util");
|
||||||
await updateAccountSettings(newId.did, { isRegistered: false });
|
await updateAccountSettings(newId.did, { isRegistered: false });
|
||||||
|
|
||||||
return newId.did;
|
return newId.did;
|
||||||
|
|||||||
@@ -254,10 +254,7 @@
|
|||||||
<b>{{ endorserLimits.doneRegistrationsThisMonth }} registrations</b>
|
<b>{{ endorserLimits.doneRegistrationsThisMonth }} registrations</b>
|
||||||
out of <b>{{ endorserLimits.maxRegistrationsPerMonth }}</b> for this
|
out of <b>{{ endorserLimits.maxRegistrationsPerMonth }}</b> for this
|
||||||
month.
|
month.
|
||||||
<i
|
<i>(You cannot register anyone else on your first day.)</i>
|
||||||
>(You can register nobody on your first day, and after that only one
|
|
||||||
a day in your first month.)</i
|
|
||||||
>
|
|
||||||
Your registration counter resets at
|
Your registration counter resets at
|
||||||
<b class="whitespace-nowrap">
|
<b class="whitespace-nowrap">
|
||||||
{{ readableDate(endorserLimits.nextMonthBeginDateTime) }}
|
{{ readableDate(endorserLimits.nextMonthBeginDateTime) }}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
@click="$router.back()"
|
@click="$router.back()"
|
||||||
>
|
>
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
<fa icon="chevron-left" class="fa-fw" />
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
click here to set it for them.
|
click here to set it for them.
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<UserNameDialog ref="userNameDialog" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<UserNameDialog ref="userNameDialog" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@click="onCopyUrlToClipboard()"
|
@click="onCopyUrlToClipboard()"
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
}"
|
}"
|
||||||
title="See more about this person"
|
title="See more about this person"
|
||||||
>
|
>
|
||||||
<fa icon="circle-info" class="text-blue-500 ml-4" />
|
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<span class="ml-4 text-sm overflow-hidden"
|
<span class="ml-4 text-sm overflow-hidden"
|
||||||
@@ -276,6 +276,7 @@
|
|||||||
|
|
||||||
<GiftedDialog ref="customGivenDialog" />
|
<GiftedDialog ref="customGivenDialog" />
|
||||||
<OfferDialog ref="customOfferDialog" />
|
<OfferDialog ref="customOfferDialog" />
|
||||||
|
<ContactNameDialog ref="contactNameDialog" />
|
||||||
|
|
||||||
<div v-if="showLargeIdenticon" class="fixed z-[100] top-0 inset-x-0 w-full">
|
<div v-if="showLargeIdenticon" class="fixed z-[100] top-0 inset-x-0 w-full">
|
||||||
<div
|
<div
|
||||||
@@ -296,6 +297,7 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { Buffer } from "buffer/";
|
import { Buffer } from "buffer/";
|
||||||
import { IndexableType } from "dexie";
|
import { IndexableType } from "dexie";
|
||||||
|
import { JWTPayload } 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 { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
@@ -304,7 +306,7 @@ import { useClipboard } from "@vueuse/core";
|
|||||||
import { AppString, NotificationIface } from "@/constants/app";
|
import { AppString, NotificationIface } from "@/constants/app";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount, updateAccountSettings,
|
||||||
updateDefaultSettings,
|
updateDefaultSettings,
|
||||||
} from "@/db/index";
|
} from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
@@ -319,16 +321,26 @@ import {
|
|||||||
register,
|
register,
|
||||||
setVisibilityUtil,
|
setVisibilityUtil,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
|
VerifiableCredential,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import * as libsUtil from "@/libs/util";
|
import * as libsUtil from "@/libs/util";
|
||||||
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 GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import OfferDialog from "@/components/OfferDialog.vue";
|
import OfferDialog from "@/components/OfferDialog.vue";
|
||||||
|
import ContactNameDialog from "@/components/ContactNameDialog.vue";
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
|
import {generateSaveAndActivateIdentity} from "@/libs/util";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { TopMessage, GiftedDialog, EntityIcon, OfferDialog, QuickNav },
|
components: {
|
||||||
|
GiftedDialog,
|
||||||
|
EntityIcon,
|
||||||
|
OfferDialog,
|
||||||
|
QuickNav,
|
||||||
|
ContactNameDialog,
|
||||||
|
TopMessage,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class ContactsView extends Vue {
|
export default class ContactsView extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
@@ -387,11 +399,11 @@ export default class ContactsView extends Vue {
|
|||||||
(a.name || "").localeCompare(b.name || ""),
|
(a.name || "").localeCompare(b.name || ""),
|
||||||
);
|
);
|
||||||
|
|
||||||
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded).query[
|
// handle a contact sent via URL
|
||||||
"contactJwt"
|
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
|
||||||
] as string;
|
.query["contactJwt"] as string;
|
||||||
if (importedContactJwt) {
|
if (importedContactJwt) {
|
||||||
// really should fully verify
|
// really should fully verify contents
|
||||||
const { payload } = decodeEndorserJwt(importedContactJwt);
|
const { payload } = decodeEndorserJwt(importedContactJwt);
|
||||||
const userInfo = payload["own"] as UserInfo;
|
const userInfo = payload["own"] as UserInfo;
|
||||||
const newContact = {
|
const newContact = {
|
||||||
@@ -404,6 +416,76 @@ export default class ContactsView extends Vue {
|
|||||||
} as Contact;
|
} as Contact;
|
||||||
this.addContact(newContact);
|
this.addContact(newContact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle an invite JWT sent via URL
|
||||||
|
const importedInviteJwt = (this.$route as RouteLocationNormalizedLoaded)
|
||||||
|
.query["inviteJwt"] as string;
|
||||||
|
if (importedInviteJwt) {
|
||||||
|
// make sure user is created
|
||||||
|
if (!this.activeDid) {
|
||||||
|
this.activeDid = await generateSaveAndActivateIdentity();
|
||||||
|
}
|
||||||
|
// send invite directly to server, with auth for this user
|
||||||
|
const headers = await getHeaders(this.activeDid);
|
||||||
|
try {
|
||||||
|
const response = await this.axios.post(
|
||||||
|
this.apiServer + "/api/v2/claim",
|
||||||
|
{ jwtEncoded: importedInviteJwt },
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
if (response.status != 201) {
|
||||||
|
throw { error: { response: response } };
|
||||||
|
}
|
||||||
|
await updateAccountSettings(this.activeDid, { isRegistered: true });
|
||||||
|
this.isRegistered = true;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Registered",
|
||||||
|
text: "You are now registered.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// now add the inviter as a contact
|
||||||
|
const payload: JWTPayload =
|
||||||
|
decodeEndorserJwt(importedInviteJwt).payload;
|
||||||
|
const registration = payload as VerifiableCredential;
|
||||||
|
(this.$refs.contactNameDialog as ContactNameDialog).open((name) =>
|
||||||
|
this.addContact({
|
||||||
|
did: registration.vc.credentialSubject.agent.identifier,
|
||||||
|
name: name, // may be undefined if they cancel
|
||||||
|
registered: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error redeeming invite:", error);
|
||||||
|
let message = "Got an error sending the invite.";
|
||||||
|
if (
|
||||||
|
error.response &&
|
||||||
|
error.response.data &&
|
||||||
|
error.response.data.error
|
||||||
|
) {
|
||||||
|
if (error.response.data.error.message) {
|
||||||
|
message = error.response.data.error.message;
|
||||||
|
} else {
|
||||||
|
message = error.response.data.error;
|
||||||
|
}
|
||||||
|
} else if (error.message) {
|
||||||
|
message = error.message;
|
||||||
|
}
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error with Invite",
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private danger(message: string, title: string = "Error", timeout = 5000) {
|
private danger(message: string, title: string = "Error", timeout = 5000) {
|
||||||
@@ -864,12 +946,14 @@ export default class ContactsView extends Vue {
|
|||||||
let userMessage = "There was an error. See logs for more info.";
|
let userMessage = "There was an error. See logs for more info.";
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError;
|
||||||
if (serverError.isAxiosError) {
|
if (serverError.isAxiosError) {
|
||||||
if (serverError.response?.data
|
if (
|
||||||
&& typeof serverError.response.data === 'object'
|
serverError.response?.data &&
|
||||||
&& 'error' in serverError.response.data
|
typeof serverError.response.data === "object" &&
|
||||||
&& typeof serverError.response.data.error === 'object'
|
"error" in serverError.response.data &&
|
||||||
&& serverError.response.data.error !== null
|
typeof serverError.response.data.error === "object" &&
|
||||||
&& 'message' in serverError.response.data.error){
|
serverError.response.data.error !== null &&
|
||||||
|
"message" in serverError.response.data.error
|
||||||
|
) {
|
||||||
userMessage = serverError.response.data.error.message as string;
|
userMessage = serverError.response.data.error.message as string;
|
||||||
} else if (serverError.message) {
|
} else if (serverError.message) {
|
||||||
userMessage = serverError.message; // Info for the user
|
userMessage = serverError.message; // Info for the user
|
||||||
|
|||||||
@@ -44,7 +44,10 @@
|
|||||||
:key="invite.inviteIdentifier"
|
:key="invite.inviteIdentifier"
|
||||||
class="border-t"
|
class="border-t"
|
||||||
>
|
>
|
||||||
<td class="py-2 text-center">
|
<td
|
||||||
|
class="py-2 text-center text-blue-500"
|
||||||
|
@click="copyInviteAndNotify(invite.jwt)"
|
||||||
|
>
|
||||||
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
|
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 text-left">{{ invite.notes }}</td>
|
<td class="py-2 text-left">{{ invite.notes }}</td>
|
||||||
@@ -62,19 +65,21 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db";
|
import { db, retrieveSettingsForActiveAccount } from "../db";
|
||||||
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 InviteDialog from "@/components/InviteDialog.vue";
|
import InviteDialog from "@/components/InviteDialog.vue";
|
||||||
import { NotificationIface } from "@/constants/app";
|
import { APP_SERVER, NotificationIface } from "@/constants/app";
|
||||||
import { createInviteJwt, getHeaders } from "@/libs/endorserServer";
|
import { createInviteJwt, getHeaders } from "@/libs/endorserServer";
|
||||||
|
|
||||||
interface Invite {
|
interface Invite {
|
||||||
inviteIdentifier: string;
|
inviteIdentifier: string;
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
|
jwt: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
redeemedBy: string | null;
|
redeemedBy: string | null;
|
||||||
}
|
}
|
||||||
@@ -124,11 +129,25 @@ export default class InviteOneView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTruncatedRedeemedBy(redeemedBy: string | null): string {
|
getTruncatedRedeemedBy(redeemedBy: string | null): string {
|
||||||
if (!redeemedBy) return "Not yet redeemed";
|
if (!redeemedBy) return "";
|
||||||
if (redeemedBy.length <= 19) return redeemedBy;
|
if (redeemedBy.length <= 19) return redeemedBy;
|
||||||
return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`;
|
return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copyInviteAndNotify(jwt: string) {
|
||||||
|
const link = APP_SERVER + "/contacts?inviteJwt=" + jwt;
|
||||||
|
useClipboard().copy(link);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Copied",
|
||||||
|
text: "Invitation link is copied to clipboard.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async createInvite() {
|
async createInvite() {
|
||||||
(this.$refs.inviteDialog as InviteDialog).open(
|
(this.$refs.inviteDialog as InviteDialog).open(
|
||||||
"Invitation Note",
|
"Invitation Note",
|
||||||
@@ -148,8 +167,7 @@ export default class InviteOneView extends Vue {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const expiresIn =
|
const expiresIn = (new Date(expiresAt).getTime() - Date.now()) / 1000;
|
||||||
(new Date(expiresAt).getTime() - new Date().getTime()) / 1000;
|
|
||||||
const inviteJwt = await createInviteJwt(
|
const inviteJwt = await createInviteJwt(
|
||||||
this.activeDid,
|
this.activeDid,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -164,6 +182,7 @@ export default class InviteOneView extends Vue {
|
|||||||
this.invites.push({
|
this.invites.push({
|
||||||
inviteIdentifier: inviteIdentifier,
|
inviteIdentifier: inviteIdentifier,
|
||||||
expiresAt: expiresAt,
|
expiresAt: expiresAt,
|
||||||
|
jwt: inviteJwt,
|
||||||
notes: notes,
|
notes: notes,
|
||||||
redeemedBy: null,
|
redeemedBy: null,
|
||||||
});
|
});
|
||||||
@@ -176,7 +195,11 @@ export default class InviteOneView extends Vue {
|
|||||||
error.response.data &&
|
error.response.data &&
|
||||||
error.response.data.error
|
error.response.data.error
|
||||||
) {
|
) {
|
||||||
message = error.response.data.error;
|
if (error.response.data.error.message) {
|
||||||
|
message = error.response.data.error.message;
|
||||||
|
} else {
|
||||||
|
message = error.response.data.error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -262,7 +262,8 @@ export default class NewEditProjectView extends Vue {
|
|||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
|
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
|
||||||
|
|
||||||
this.projectId = (this.$route as RouteLocationNormalizedLoaded).query["projectId"] || "";
|
this.projectId =
|
||||||
|
(this.$route as RouteLocationNormalizedLoaded).query["projectId"] || "";
|
||||||
|
|
||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
if (this.numAccounts === 0) {
|
if (this.numAccounts === 0) {
|
||||||
@@ -623,6 +624,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`Error sending to ${serviceName}`, error);
|
console.error(`Error sending to ${serviceName}`, error);
|
||||||
let errorMessage = `There was an error sending to ${serviceName}.`;
|
let errorMessage = `There was an error sending to ${serviceName}.`;
|
||||||
|
|||||||
Reference in New Issue
Block a user