add page for one-on-one invites (incomplete)
This commit is contained in:
118
src/components/InviteDialog.vue
Normal file
118
src/components/InviteDialog.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="dialog-overlay">
|
||||||
|
<div class="dialog">
|
||||||
|
<h1 class="text-xl font-bold text-center mb-4">{{ title }}</h1>
|
||||||
|
|
||||||
|
{{ message }}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
|
v-model="text"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Add date selection element -->
|
||||||
|
Expiration
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
class="block rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
|
v-model="expiresAt"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class InviteDialog extends Vue {
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
callback: (text: string, expiresAt: string) => void = () => {};
|
||||||
|
message = "";
|
||||||
|
text = "";
|
||||||
|
title = "";
|
||||||
|
visible = false;
|
||||||
|
expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30 * 3)
|
||||||
|
.toISOString()
|
||||||
|
.substring(0, 10);
|
||||||
|
|
||||||
|
async open(
|
||||||
|
title: string,
|
||||||
|
message: string,
|
||||||
|
aCallback: (text: string, expiresAt: string) => void,
|
||||||
|
) {
|
||||||
|
this.callback = aCallback;
|
||||||
|
this.title = title;
|
||||||
|
this.message = message;
|
||||||
|
this.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClickSaveChanges() {
|
||||||
|
if (!this.expiresAt) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Needs Expiration",
|
||||||
|
text: "You must select an expiration date.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.callback(this.text, this.expiresAt);
|
||||||
|
this.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -46,7 +46,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|||||||
export default class UserNameDialog extends Vue {
|
export default class UserNameDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
callback: (string?) => void = () => {};
|
callback: (name?: string) => void = () => {};
|
||||||
givenName = "";
|
givenName = "";
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
|
|||||||
@@ -54,16 +54,22 @@ export function isFromPasskey(keyMeta?: KeyMeta): boolean {
|
|||||||
export async function createEndorserJwtForKey(
|
export async function createEndorserJwtForKey(
|
||||||
account: KeyMeta,
|
account: KeyMeta,
|
||||||
payload: object,
|
payload: object,
|
||||||
|
expiresIn?: number,
|
||||||
) {
|
) {
|
||||||
if (account?.identity) {
|
if (account?.identity) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const identity: IIdentifier = JSON.parse(account.identity!);
|
const identity: IIdentifier = JSON.parse(account.identity!);
|
||||||
const privateKeyHex = identity.keys[0].privateKeyHex;
|
const privateKeyHex = identity.keys[0].privateKeyHex;
|
||||||
const signer = await SimpleSigner(privateKeyHex as string);
|
const signer = await SimpleSigner(privateKeyHex as string);
|
||||||
return didJwt.createJWT(payload, {
|
const options = {
|
||||||
issuer: account.did,
|
issuer: account.did,
|
||||||
signer: signer,
|
signer: signer,
|
||||||
});
|
expiresIn: undefined as number | undefined,
|
||||||
|
}
|
||||||
|
if (expiresIn) {
|
||||||
|
options.expiresIn = expiresIn;
|
||||||
|
}
|
||||||
|
return didJwt.createJWT(payload, options);
|
||||||
} else if (account?.passkeyCredIdHex) {
|
} else if (account?.passkeyCredIdHex) {
|
||||||
return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload);
|
return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -239,8 +239,9 @@ export interface RegisterVerifiableCredential {
|
|||||||
"@context": string;
|
"@context": string;
|
||||||
"@type": string;
|
"@type": string;
|
||||||
agent: { identifier: string };
|
agent: { identifier: string };
|
||||||
|
identifier?: string;
|
||||||
object: string;
|
object: string;
|
||||||
participant: { identifier: string };
|
participant?: { identifier: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
// now for some of the error & other wrapper types
|
// now for some of the error & other wrapper types
|
||||||
@@ -993,9 +994,10 @@ export async function generateEndorserJwtForAccount(
|
|||||||
export async function createEndorserJwtForDid(
|
export async function createEndorserJwtForDid(
|
||||||
issuerDid: string,
|
issuerDid: string,
|
||||||
payload: object,
|
payload: object,
|
||||||
|
expiresIn?: number,
|
||||||
) {
|
) {
|
||||||
const account = await getAccount(issuerDid);
|
const account = await getAccount(issuerDid);
|
||||||
return createEndorserJwtForKey(account as KeyMeta, payload);
|
return createEndorserJwtForKey(account as KeyMeta, payload, expiresIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1225,19 +1227,24 @@ export async function createEndorserJwtVcFromClaim(
|
|||||||
return createEndorserJwtForDid(issuerDid, vcPayload);
|
return createEndorserJwtForDid(issuerDid, vcPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function register(
|
export async function createInviteJwt(
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
apiServer: string,
|
contact?: Contact,
|
||||||
axios: Axios,
|
inviteId?: string,
|
||||||
contact: Contact,
|
expiresIn?: number,
|
||||||
) {
|
): Promise<string> {
|
||||||
const vcClaim: RegisterVerifiableCredential = {
|
const vcClaim: RegisterVerifiableCredential = {
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
"@context": SCHEMA_ORG_CONTEXT,
|
||||||
"@type": "RegisterAction",
|
"@type": "RegisterAction",
|
||||||
agent: { identifier: activeDid },
|
agent: { identifier: activeDid },
|
||||||
object: SERVICE_ID,
|
object: SERVICE_ID,
|
||||||
participant: { identifier: contact.did },
|
|
||||||
};
|
};
|
||||||
|
if (contact) {
|
||||||
|
vcClaim.participant = { identifier: contact.did };
|
||||||
|
}
|
||||||
|
if (inviteId) {
|
||||||
|
vcClaim.identifier = inviteId;
|
||||||
|
}
|
||||||
// Make a payload for the claim
|
// Make a payload for the claim
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
vc: {
|
vc: {
|
||||||
@@ -1247,7 +1254,17 @@ export async function register(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Create a signature using private key of identity
|
// Create a signature using private key of identity
|
||||||
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload);
|
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn);
|
||||||
|
return vcJwt;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function register(
|
||||||
|
activeDid: string,
|
||||||
|
apiServer: string,
|
||||||
|
axios: Axios,
|
||||||
|
contact: Contact,
|
||||||
|
): Promise<{ success?: boolean; error?: string }> {
|
||||||
|
const vcJwt = await createInviteJwt(activeDid, contact);
|
||||||
|
|
||||||
const url = apiServer + "/api/v2/claim";
|
const url = apiServer + "/api/v2/claim";
|
||||||
const resp = await axios.post(url, { jwtEncoded: vcJwt });
|
const resp = await axios.post(url, { jwtEncoded: vcJwt });
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
faDollar,
|
faDollar,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
|
faEnvelopeOpenText,
|
||||||
faEye,
|
faEye,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faFileLines,
|
faFileLines,
|
||||||
@@ -109,6 +110,7 @@ library.add(
|
|||||||
faDollar,
|
faDollar,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
|
faEnvelopeOpenText,
|
||||||
faEye,
|
faEye,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faFileLines,
|
faFileLines,
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "import-derive",
|
name: "import-derive",
|
||||||
component: () => import("../views/ImportDerivedAccountView.vue"),
|
component: () => import("../views/ImportDerivedAccountView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/invite-one",
|
||||||
|
name: "invite-one",
|
||||||
|
component: () => import("../views/InviteOneView.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/new-edit-account",
|
path: "/new-edit-account",
|
||||||
name: "new-edit-account",
|
name: "new-edit-account",
|
||||||
|
|||||||
@@ -23,12 +23,20 @@
|
|||||||
|
|
||||||
<!-- New Contact -->
|
<!-- New Contact -->
|
||||||
<div id="formAddNewContact" class="mt-4 mb-4 flex items-stretch">
|
<div id="formAddNewContact" class="mt-4 mb-4 flex items-stretch">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'invite-one' }"
|
||||||
|
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
||||||
|
>
|
||||||
|
<fa icon="envelope-open-text" class="fa-fw text-2xl" />
|
||||||
|
</router-link>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'contact-qr' }"
|
:to="{ name: 'contact-qr' }"
|
||||||
class="flex items-center 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-1.5 py-1 mr-1 rounded-md"
|
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
||||||
>
|
>
|
||||||
<fa icon="qrcode" class="fa-fw text-2xl" />
|
<fa icon="qrcode" class="fa-fw text-2xl" />
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="URL or DID, Name, Public Key, Next Public Key Hash"
|
placeholder="URL or DID, Name, Public Key, Next Public Key Hash"
|
||||||
@@ -79,7 +87,9 @@
|
|||||||
class="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-1 py-1 rounded-md"
|
class="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-1 py-1 rounded-md"
|
||||||
@click="toggleShowContactAmounts()"
|
@click="toggleShowContactAmounts()"
|
||||||
>
|
>
|
||||||
{{ showGiveNumbers ? "Hide Given Hours" : "Show Given Hours" }}
|
{{
|
||||||
|
showGiveNumbers ? "Hide Given Hours etc" : "Show Given Hours etc"
|
||||||
|
}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -288,7 +298,7 @@ import { Buffer } from "buffer/";
|
|||||||
import { IndexableType } from "dexie";
|
import { IndexableType } from "dexie";
|
||||||
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 { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import { AppString, NotificationIface } from "@/constants/app";
|
import { AppString, NotificationIface } from "@/constants/app";
|
||||||
@@ -377,7 +387,7 @@ export default class ContactsView extends Vue {
|
|||||||
(a.name || "").localeCompare(b.name || ""),
|
(a.name || "").localeCompare(b.name || ""),
|
||||||
);
|
);
|
||||||
|
|
||||||
const importedContactJwt = (this.$route as Router).query[
|
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded).query[
|
||||||
"contactJwt"
|
"contactJwt"
|
||||||
] as string;
|
] as string;
|
||||||
if (importedContactJwt) {
|
if (importedContactJwt) {
|
||||||
@@ -736,7 +746,7 @@ export default class ContactsView extends Vue {
|
|||||||
type: "confirm",
|
type: "confirm",
|
||||||
title: "Register",
|
title: "Register",
|
||||||
text: "Do you want to register them?",
|
text: "Do you want to register them?",
|
||||||
onCancel: async (stopAsking: boolean) => {
|
onCancel: async (stopAsking?: boolean) => {
|
||||||
if (stopAsking) {
|
if (stopAsking) {
|
||||||
await updateDefaultSettings({
|
await updateDefaultSettings({
|
||||||
hideRegisterPromptOnNewContact: stopAsking,
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
@@ -744,7 +754,7 @@ export default class ContactsView extends Vue {
|
|||||||
this.hideRegisterPromptOnNewContact = stopAsking;
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNo: async (stopAsking: boolean) => {
|
onNo: async (stopAsking?: boolean) => {
|
||||||
if (stopAsking) {
|
if (stopAsking) {
|
||||||
await updateDefaultSettings({
|
await updateDefaultSettings({
|
||||||
hideRegisterPromptOnNewContact: stopAsking,
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
@@ -853,9 +863,14 @@ export default class ContactsView extends Vue {
|
|||||||
console.error("Error when registering:", error);
|
console.error("Error when registering:", error);
|
||||||
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) {
|
if (serverError.isAxiosError) {
|
||||||
if (serverError.response?.data?.error?.message) {
|
if (serverError.response?.data
|
||||||
userMessage = serverError.response.data.error.message;
|
&& typeof serverError.response.data === 'object'
|
||||||
|
&& 'error' in serverError.response.data
|
||||||
|
&& typeof serverError.response.data.error === 'object'
|
||||||
|
&& serverError.response.data.error !== null
|
||||||
|
&& 'message' in serverError.response.data.error){
|
||||||
|
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
|
||||||
} else {
|
} else {
|
||||||
@@ -971,8 +986,8 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showGiftedDialog(giverDid: string, recipientDid: string) {
|
private showGiftedDialog(giverDid: string, recipientDid: string) {
|
||||||
let giver: libsUtil.GiverReceiverInputInfo;
|
let giver: libsUtil.GiverReceiverInputInfo | undefined;
|
||||||
let receiver: libsUtil.GiverReceiverInputInfo;
|
let receiver: libsUtil.GiverReceiverInputInfo | undefined;
|
||||||
if (giverDid) {
|
if (giverDid) {
|
||||||
giver = {
|
giver = {
|
||||||
did: giverDid,
|
did: giverDid,
|
||||||
@@ -995,7 +1010,7 @@ export default class ContactsView extends Vue {
|
|||||||
newList[recipientDid] = (newList[recipientDid] || 0) + amount;
|
newList[recipientDid] = (newList[recipientDid] || 0) + amount;
|
||||||
this.givenByMeUnconfirmed = newList;
|
this.givenByMeUnconfirmed = newList;
|
||||||
};
|
};
|
||||||
customTitle = "Given to " + receiver.name;
|
customTitle = "Given to " + (receiver?.name || "Someone Unnamed");
|
||||||
} else {
|
} else {
|
||||||
// must be (recipientDid == this.activeDid)
|
// must be (recipientDid == this.activeDid)
|
||||||
callback = (amount: number) => {
|
callback = (amount: number) => {
|
||||||
@@ -1003,14 +1018,14 @@ export default class ContactsView extends Vue {
|
|||||||
newList[giverDid] = (newList[giverDid] || 0) + amount;
|
newList[giverDid] = (newList[giverDid] || 0) + amount;
|
||||||
this.givenToMeUnconfirmed = newList;
|
this.givenToMeUnconfirmed = newList;
|
||||||
};
|
};
|
||||||
customTitle = "Received from " + giver.name;
|
customTitle = "Received from " + (giver?.name || "Someone Unnamed");
|
||||||
}
|
}
|
||||||
(this.$refs.customGivenDialog as GiftedDialog).open(
|
(this.$refs.customGivenDialog as GiftedDialog).open(
|
||||||
giver,
|
giver,
|
||||||
receiver,
|
receiver,
|
||||||
undefined as string,
|
undefined as unknown as string,
|
||||||
customTitle,
|
customTitle,
|
||||||
undefined as string,
|
undefined as unknown as string,
|
||||||
callback,
|
callback,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
195
src/views/InviteOneView.vue
Normal file
195
src/views/InviteOneView.vue
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav selected="Invite" />
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
|
<!-- 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"></fa>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Heading -->
|
||||||
|
<h1 class="text-4xl text-center font-light">Invitations</h1>
|
||||||
|
|
||||||
|
<!-- New Project -->
|
||||||
|
<button
|
||||||
|
v-if="isRegistered"
|
||||||
|
class="fixed right-6 top-12 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full"
|
||||||
|
@click="createInvite()"
|
||||||
|
>
|
||||||
|
<fa icon="plus" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<InviteDialog ref="inviteDialog" />
|
||||||
|
|
||||||
|
<!-- Invites Table -->
|
||||||
|
<div v-if="invites.length" class="mt-6">
|
||||||
|
<table class="min-w-full bg-white">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="py-2">ID</th>
|
||||||
|
<th class="py-2">Notes</th>
|
||||||
|
<th class="py-2">Expires At</th>
|
||||||
|
<th class="py-2">Redeemed By</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="invite in invites"
|
||||||
|
:key="invite.inviteIdentifier"
|
||||||
|
class="border-t"
|
||||||
|
>
|
||||||
|
<td class="py-2 text-center">
|
||||||
|
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 text-left">{{ invite.notes }}</td>
|
||||||
|
<td class="py-2 text-center">
|
||||||
|
{{ invite.expiresAt.substring(0, 10) }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 text-center">
|
||||||
|
{{ getTruncatedRedeemedBy(invite.redeemedBy) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p v-else class="mt-6 text-center">No invites found.</p>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { db, retrieveSettingsForActiveAccount } from "../db";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
|
import InviteDialog from "@/components/InviteDialog.vue";
|
||||||
|
import { NotificationIface } from "@/constants/app";
|
||||||
|
import { createInviteJwt, getHeaders } from "@/libs/endorserServer";
|
||||||
|
|
||||||
|
interface Invite {
|
||||||
|
inviteIdentifier: string;
|
||||||
|
expiresAt: string;
|
||||||
|
notes: string;
|
||||||
|
redeemedBy: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { QuickNav, TopMessage, InviteDialog },
|
||||||
|
})
|
||||||
|
export default class InviteOneView extends Vue {
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
invites: Invite[] = [];
|
||||||
|
activeDid: string = "";
|
||||||
|
apiServer: string = "";
|
||||||
|
isRegistered: boolean = false;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
|
this.activeDid = settings.activeDid || "";
|
||||||
|
this.apiServer = settings.apiServer || "";
|
||||||
|
this.isRegistered = !!settings.isRegistered;
|
||||||
|
|
||||||
|
const headers = await getHeaders(this.activeDid);
|
||||||
|
const response = await axios.get(
|
||||||
|
this.apiServer + "/api/userUtil/invite",
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
this.invites = response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching invites:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Load Error",
|
||||||
|
text: "Got an error loading your invites.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTruncatedInviteId(inviteId: string): string {
|
||||||
|
if (inviteId.length <= 9) return inviteId;
|
||||||
|
return `${inviteId.slice(0, 3)}...${inviteId.slice(-3)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTruncatedRedeemedBy(redeemedBy: string | null): string {
|
||||||
|
if (!redeemedBy) return "Not yet redeemed";
|
||||||
|
if (redeemedBy.length <= 19) return redeemedBy;
|
||||||
|
return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createInvite() {
|
||||||
|
(this.$refs.inviteDialog as InviteDialog).open(
|
||||||
|
"Invitation Note",
|
||||||
|
`These notes are only for your use, to make comments for a link to recall later if redeemed by someone.
|
||||||
|
Note that this is sent to the server.`,
|
||||||
|
async (notes, expiresAt) => {
|
||||||
|
try {
|
||||||
|
const inviteIdentifier =
|
||||||
|
Math.random().toString(36).substring(2) +
|
||||||
|
Math.random().toString(36).substring(2) +
|
||||||
|
Math.random().toString(36).substring(2);
|
||||||
|
const headers = await getHeaders(this.activeDid);
|
||||||
|
if (!expiresAt) {
|
||||||
|
throw {
|
||||||
|
response: {
|
||||||
|
data: { error: "You must select an expiration date." },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const expiresIn =
|
||||||
|
(new Date(expiresAt).getTime() - new Date().getTime()) / 1000;
|
||||||
|
const inviteJwt = await createInviteJwt(
|
||||||
|
this.activeDid,
|
||||||
|
undefined,
|
||||||
|
inviteIdentifier,
|
||||||
|
expiresIn,
|
||||||
|
);
|
||||||
|
await axios.post(
|
||||||
|
this.apiServer + "/api/userUtil/invite",
|
||||||
|
{ inviteJwt: inviteJwt, notes: notes },
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
this.invites.push({
|
||||||
|
inviteIdentifier: inviteIdentifier,
|
||||||
|
expiresAt: expiresAt,
|
||||||
|
notes: notes,
|
||||||
|
redeemedBy: null,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error creating invite:", error);
|
||||||
|
let message = "Got an error creating your invite.";
|
||||||
|
if (
|
||||||
|
error.response &&
|
||||||
|
error.response.data &&
|
||||||
|
error.response.data.error
|
||||||
|
) {
|
||||||
|
message = error.response.data.error;
|
||||||
|
}
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Creating Invite",
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -196,7 +196,7 @@ import { accountFromSeedWords } from "nostr-tools/nip06";
|
|||||||
import { finalizeEvent, serializeEvent } from "nostr-tools/pure";
|
import { finalizeEvent, serializeEvent } from "nostr-tools/pure";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
import { Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
|
|
||||||
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
|
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
@@ -218,7 +218,7 @@ import { getAccount } from "@/libs/util";
|
|||||||
})
|
})
|
||||||
export default class NewEditProjectView extends Vue {
|
export default class NewEditProjectView extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
errNote(message) {
|
errNote(message: string) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{ group: "alert", type: "danger", title: "Error", text: message },
|
{ group: "alert", type: "danger", title: "Error", text: message },
|
||||||
5000,
|
5000,
|
||||||
@@ -262,7 +262,7 @@ 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 Router).query["projectId"] || "";
|
this.projectId = (this.$route as RouteLocationNormalizedLoaded).query["projectId"] || "";
|
||||||
|
|
||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
if (this.numAccounts === 0) {
|
if (this.numAccounts === 0) {
|
||||||
@@ -447,7 +447,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
|
|
||||||
const projectPath = encodeURIComponent(resp.data.success.handleId);
|
const projectPath = encodeURIComponent(resp.data.success.handleId);
|
||||||
|
|
||||||
let signedPayload: VerifiedEvent; // sign something to prove ownership of pubkey
|
let signedPayload: VerifiedEvent | undefined; // sign something to prove ownership of pubkey
|
||||||
if (this.sendToTrustroots) {
|
if (this.sendToTrustroots) {
|
||||||
signedPayload = await this.signPayload();
|
signedPayload = await this.signPayload();
|
||||||
this.sendToNostrPartner(
|
this.sendToNostrPartner(
|
||||||
@@ -623,7 +623,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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}.`;
|
||||||
if (error.response?.data?.error?.message) {
|
if (error.response?.data?.error?.message) {
|
||||||
|
|||||||
Reference in New Issue
Block a user