Merge branch 'master' into another-download

This commit is contained in:
2024-01-01 19:27:52 -07:00
56 changed files with 1700 additions and 542 deletions

View File

@@ -169,20 +169,12 @@
>
Turn on Notifications
</button>
<div class="grid grid-cols-2 gap-2">
<button
@click="maybeLater(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Maybe Later
</button>
<button
@click="never(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
>
Never
</button>
</div>
<button
@click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Maybe Later
</button>
</div>
</div>
</div>
@@ -238,6 +230,10 @@
</p>
<button
@click="
close(notification.id);
turnOffNotifications();
"
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
>
Turn Off Notifications
@@ -285,7 +281,7 @@ interface VapidResponse {
};
}
import { AppString } from "@/constants/app";
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@@ -306,7 +302,7 @@ export default class App extends Vue {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
let pushUrl: string = AppString.DEFAULT_PUSH_SERVER;
let pushUrl = DEFAULT_PUSH_SERVER;
if (settings?.webPushServer) {
pushUrl = settings.webPushServer;
}
@@ -332,16 +328,20 @@ export default class App extends Vue {
);
}
} catch (error) {
console.error("Got an error initializing notifications:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notifications",
text: "Got an error setting notifications.",
},
-1,
);
if (window.location.host.startsWith("localhost")) {
console.log("Ignoring the error getting VAPID for local development.");
} else {
console.error("Got an error initializing notifications:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notifications",
text: "Got an error setting notifications.",
},
-1,
);
}
}
}
@@ -370,6 +370,7 @@ export default class App extends Vue {
}
private askPermission(): Promise<NotificationPermission> {
console.log("Requesting permission for notifications:", navigator);
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
return Promise.reject("Service worker not available.");
}
@@ -410,14 +411,17 @@ export default class App extends Vue {
private requestNotificationPermission(): Promise<NotificationPermission> {
return Notification.requestPermission().then((permission) => {
if (permission !== "granted") {
alert("We need notification permission to provide certain features.");
alert(
"Allow this app permission to make notifications for personal reminders." +
" You can adjust them at any time in your settings.",
);
throw new Error("We weren't granted permission.");
}
return permission;
});
}
async turnOnNotifications() {
public async turnOnNotifications() {
return this.askPermission()
.then((permission) => {
console.log("Permission granted:", permission);
@@ -439,7 +443,18 @@ export default class App extends Vue {
}
})
.then(() => {
console.log("Subscription data sent to server.");
console.log(
"Subscription data sent to server and all finished successfully.",
);
this.$notify(
{
group: "alert",
type: "success",
title: "Notifications Turned On",
text: "Notifications are on. You should see one on your device; if not, see the 'Troubleshoot' page.",
},
-1,
);
})
.catch((error) => {
console.error(
@@ -452,8 +467,11 @@ export default class App extends Vue {
});
})
.catch((error) => {
console.error("An error occurred:", error);
alert("Some error occurred." + error);
console.error(
"An error occurred setting notification permissions:",
error,
);
alert("Some error occurred setting notification permissions.");
});
}
@@ -500,11 +518,7 @@ export default class App extends Vue {
resolve();
})
.catch((error) => {
console.error(
"Subscription or server communication failed:",
error,
options,
);
console.error("Push subscription failed:", error, options);
// Inform the user about the issue
alert(
@@ -520,7 +534,7 @@ export default class App extends Vue {
private sendSubscriptionToServer(
subscription: PushSubscription,
): Promise<void> {
console.log("About to send subscription", subscription);
console.log("About to send subscription...", subscription);
return fetch("/web-push/subscribe", {
method: "POST",
headers: {
@@ -535,12 +549,49 @@ export default class App extends Vue {
});
}
never(ID: string) {
alert(ID);
}
async turnOffNotifications() {
let subscription;
const pushProviderSuccess = await navigator.serviceWorker.ready
.then((registration) => {
return registration.pushManager.getSubscription();
})
.then((subscript) => {
subscription = subscript;
if (subscription) {
return subscription.unsubscribe();
} else {
console.log("Subscription object is not available.");
return false;
}
})
.catch((error) => {
console.log("Push provider server communication failed:", error);
return false;
});
maybeLater(ID: string) {
alert(ID);
const pushServerSuccess = await fetch("/web-push/unsubscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(subscription),
})
.then((response) => {
return response.ok;
})
.catch((error) => {
console.log("Push server communication failed:", error);
return false;
});
alert(
"Notifications are off. Push provider unsubscribe " +
(pushProviderSuccess ? "succeeded" : "failed") +
(pushProviderSuccess === pushServerSuccess ? " and" : " but") +
" push server unsubscribe " +
(pushServerSuccess ? "succeeded" : "failed") +
".",
);
}
}
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -123,9 +123,7 @@ export default class GiftedDialog extends Vue {
group: "alert",
type: "danger",
title: "Error",
text:
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
text: err.message || "There was an error retrieving your settings.",
},
-1,
);

View File

@@ -105,9 +105,7 @@ export default class OfferDialog extends Vue {
group: "alert",
type: "danger",
title: "Error",
text:
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
text: err.message || "There was an error retrieving your settings.",
},
-1,
);

View File

@@ -0,0 +1,58 @@
<template>
<div class="text-center text-red-500">{{ message }}</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { AppString } from "@/constants/app";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component
export default class TopMessage extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@Prop selected = "";
message = "";
async mounted() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
if (
settings?.warnIfTestServer &&
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message = "You're linked to a non-prod server, user " + didPrefix;
} else if (
settings?.warnIfProdServer &&
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message =
"You're linked to the production server, user " + didPrefix;
}
} catch (err: unknown) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Detecting Server",
text: JSON.stringify(err),
},
-1,
);
}
}
}
</script>

View File

@@ -4,21 +4,20 @@
* See also ../libs/veramo/setup.ts
*/
export enum AppString {
APP_NAME = "Time Safari",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app",
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
DEFAULT_PUSH_SERVER = TEST1_PUSH_SERVER,
}
export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER;
export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host;
/**
* The possible values for "group" and "type" are in App.vue.
* From the notiwind package

View File

@@ -1,18 +1,20 @@
import BaseDexie, { Table } from "dexie";
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
import { Account, AccountsSchema } from "./tables/accounts";
import { Contact, ContactsSchema } from "./tables/contacts";
import { Contact, ContactSchema } from "./tables/contacts";
import { Log, LogSchema } from "./tables/logs";
import {
MASTER_SETTINGS_KEY,
Settings,
SettingsSchema,
} from "./tables/settings";
import { AppString } from "@/constants/app";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
// Define types for tables that hold sensitive and non-sensitive data
type SensitiveTables = { accounts: Table<Account> };
type NonsensitiveTables = {
contacts: Table<Contact>;
logs: Table<Log>;
settings: Table<Settings>;
};
@@ -26,7 +28,11 @@ export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
const SensitiveSchemas = { ...AccountsSchema };
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
const NonsensitiveSchemas = { ...ContactsSchema, ...SettingsSchema };
const NonsensitiveSchemas = {
...ContactSchema,
...LogSchema,
...SettingsSchema,
};
// Manage the encryption key. If not present in localStorage, create and store it.
const secret =
@@ -38,15 +44,14 @@ encrypted(accountsDB, { secretKey: secret });
// Define the schema for our databases
accountsDB.version(1).stores(SensitiveSchemas);
db.version(1).stores(NonsensitiveSchemas);
// v1 was contacts & settings
// v2 added logs
db.version(2).stores(NonsensitiveSchemas);
// Event handler to initialize the non-sensitive database with default settings
db.on("populate", () => {
db.settings.add({
id: MASTER_SETTINGS_KEY,
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
// remember that things you add from now on aren't automatically in the DB for old users
webPushServer: AppString.DEFAULT_PUSH_SERVER,
apiServer: DEFAULT_ENDORSER_API_SERVER,
});
});

View File

@@ -6,6 +6,6 @@ export interface Contact {
registered?: boolean;
}
export const ContactsSchema = {
contacts: "&did, name, publicKeyBase64, registered, seesMe",
export const ContactSchema = {
contacts: "&did, name", // no need to key by publicKeyBase64, registered, seesMe
};

11
src/db/tables/logs.ts Normal file
View File

@@ -0,0 +1,11 @@
export interface Log {
date: string;
message: string;
}
export const LogSchema = {
// Currently keyed by "date" because A) today's log data is what we need so we append, and
// B) we don't want it to grow so we remove everything if this is the first entry today.
// See safari-notifications.js logMessage for the associated logic.
logs: "date", // definitely don't key by the potentially large message field
};

View File

@@ -12,15 +12,17 @@ export type BoundingBox = {
* Settings type encompasses user-specific configuration details.
*/
export type Settings = {
id: number; // Only one entry using MASTER_SETTINGS_KEY
id: number; // Only one entry, keyed with MASTER_SETTINGS_KEY
activeDid?: string; // Active Decentralized ID
apiServer?: string; // API server URL
firstName?: string; // User's first name
lastName?: string; // User's last name
lastViewedClaimId?: string; // Last viewed claim ID
lastNotifiedClaimId?: string; // Last notified claim ID
isRegistered?: boolean;
webPushServer?: string; // Web Push server URL
lastName?: string; // deprecated - put all names in firstName
lastNotifiedClaimId?: string; // Last notified claim ID
lastViewedClaimId?: string; // Last viewed claim ID
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
reminderOn?: boolean; // Toggle to enable or disable reminders
// Array of named search boxes defined by bounding boxes
searchBoxes?: Array<{
@@ -30,8 +32,9 @@ export type Settings = {
showContactGivesInline?: boolean; // Display contact inline or not
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
reminderOn?: boolean; // Toggle to enable or disable reminders
warnIfProdServer?: boolean; // Warn if using a production server
warnIfTestServer?: boolean; // Warn if using a testing server
webPushServer?: string; // Web Push server URL
};
/**

View File

@@ -486,6 +486,10 @@ export interface ProjectData {
* URL referencing information about the project
**/
handleId: string;
/**
* The DID of the issuer
*/
issuerDid: string;
/**
* The Identier of the project
**/

View File

@@ -13,6 +13,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import {
faArrowLeft,
faArrowRight,
faArrowUpRightFromSquare,
faBan,
faBitcoinSign,
faBurst,
@@ -65,6 +66,7 @@ import {
library.add(
faArrowLeft,
faArrowRight,
faArrowUpRightFromSquare,
faBan,
faBitcoinSign,
faBurst,

View File

@@ -1,5 +1,7 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
@@ -32,8 +34,24 @@
</span>
</div>
<!-- ID notice -->
<div
v-if="!activeDid"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
>
<p class="mb-4">
<b>Note:</b> Before you can take any action, you need an ID.
</p>
<router-link
:to="{ name: 'start' }"
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
>
Generate Identity
</router-link>
</div>
<!-- Registration notice -->
<!-- We won't show any loading indicator; we'll just pop the message in once we know they need it. -->
<!-- We won't show any loading indicator because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. -->
<div
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
@@ -58,9 +76,9 @@
<span v-else>
<router-link
:to="{ name: 'new-edit-account' }"
class="text-xs bg-blue-500 text-white px-1.5 py-1 rounded-md"
class="block w-full text-center text-md text-slate-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
>
(set name)
(Set Your Name)
</router-link>
</span>
@@ -88,18 +106,10 @@
</router-link>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<label
for="toggleNotifications"
class="flex items-center cursor-pointer"
@click="
this.$notify(
{
group: 'modal',
type: 'notification-permission',
},
-1,
)
"
<div
v-if="!notificationMaybeChanged"
class="flex items-center justify-between cursor-pointer"
@click="showNotificationChoice()"
>
<!-- label -->
<div>App Notifications</div>
@@ -108,8 +118,8 @@
<!-- input -->
<input
type="checkbox"
v-model="toggleNotifications"
name="toggleNotifications"
v-model="isSubscribed"
name="toggleNotificationsInput"
class="sr-only"
/>
<!-- line -->
@@ -119,52 +129,27 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
<label
for="toggleMuteNotifications"
class="flex items-center cursor-pointer mt-4"
@click="
this.$notify(
{
group: 'modal',
type: 'notification-mute',
},
-1,
)
"
>
<!-- label -->
<div>Mute Notifications</div>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
name="toggleMuteNotifications"
class="sr-only"
disabled
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
</div>
<div v-else>
Notification status may have changed. Revisit this page to see the
latest setting.
</div>
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications">
Test your notification setup.
</router-link>
</div>
<h3 class="text-sm uppercase font-semibold mb-3">Data Export</h3>
<router-link
:to="{ name: 'seed-backup' }"
href=""
v-if="activeDid"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Backup Identifier Seed
</router-link>
<a
<button
v-bind:class="computedStartDownloadLinkClassNames()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="exportDatabase()"
@@ -172,39 +157,51 @@
Download Settings & Contacts
<br />
(excluding Identifier Data)
</a>
</button>
<a
ref="downloadLink"
v-bind:class="computedDownloadLinkClassNames()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
class="block w-full text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
>
If no download happened yet, click again here to download now.
</a>
<div v-if="activeDid" class="flex mt-8 py-2">
<button class="text-center text-md text-blue-500" @click="checkLimits()">
<h3 class="text-sm uppercase font-semibold">Rate Limits</h3>
<button
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md ml-2 mr-2 mb-2"
@click="checkLimits()"
>
Check Limits
</button>
<!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="ml-2">
Checking... <fa icon="spinner" class="fa-spin"></fa>
<div v-if="loadingLimits" class="text-center">
Checking&hellip; <fa icon="spinner" class="fa-spin"></fa>
</div>
<div class="ml-2">
<div>
{{ limitsMessage }}
</div>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
<div v-if="!!limits?.nextWeekBeginDateTime">
<p class="mb-3 text-sm">
You have done <b>{{ limits.doneClaimsThisWeek }}</b> claims out of
<b>{{ limits.maxClaimsPerWeek }}</b> for this week. Your claims
counter resets at
<b class="whitespace-nowrap">{{
readableTime(limits.nextWeekBeginDateTime)
}}</b>
</p>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. (You can
register nobody on your first day, and after that only one a day in
your first month.) Your registration counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
<p class="text-sm">
You have done
<b>{{ limits.doneRegistrationsThisMonth }}</b> registrations out of
<b>{{ limits.maxRegistrationsPerMonth }}</b> for this month.
<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
<b class="whitespace-nowrap">
{{ readableTime(limits.nextMonthBeginDateTime) }}
</b>
</p>
</div>
</div>
@@ -218,13 +215,13 @@
Advanced
</h3>
<div v-if="showAdvanced">
<p>
<p class="text-rose-600 mb-8">
Beware: the features here can be confusing and even change data in ways
you do not expect. But we support your freedoms!
you do not expect. But we support your freedom!
</p>
<!-- Deep Identity Details -->
<h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
<h2 class="text-sm uppercase font-semibold mb-3">
Deep Identity Details
</h2>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
@@ -282,13 +279,11 @@
<label
for="toggleShowAmounts"
class="flex items-center cursor-pointer py-2"
class="flex items-center justify-between cursor-pointer my-4"
@click="handleChange"
>
<!-- label -->
<h2 class="text-slate-500 text-sm font-bold mb-2">
Show amounts given with contacts
</h2>
<h2>Show amounts given with contacts</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
@@ -307,12 +302,12 @@
</div>
</label>
<div class="grid-cols-2">
<div class="grid-cols-2 mb-4">
<span class="text-slate-500 text-sm font-bold mb-2">Data Import</span>
<input type="file" @change="uploadFile" class="ml-2" />
<div v-if="showContactImport()">
<button
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
class="block text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
@click="submitFile()"
>
Import Settings & Contacts
@@ -323,25 +318,24 @@
</div>
<div class="flex py-2">
<button class="text-blue-500">
<router-link :to="{ name: 'statistics' }" class="block text-center">
<button>
<router-link
:to="{ name: 'statistics' }"
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
See Global Animated History of Giving
</router-link>
</button>
</div>
<div class="flex py-2">
<button class="text-blue-500">
<!-- id used by puppeteer test script -->
<router-link
id="switch-identity-link"
:to="{ name: 'identity-switcher' }"
class="block text-center"
>
Switch Identity / No Identity
</router-link>
</button>
</div>
<!-- id used by puppeteer test script -->
<router-link
id="switch-identity-link"
:to="{ name: 'identity-switcher' }"
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Switch Identity
</router-link>
<div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
@@ -377,6 +371,46 @@
</button>
</div>
<label
for="toggleProdWarningMessage"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleProdWarning"
>
<!-- label -->
<h2>Show warning if on prod server</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
<label
for="toggleTestWarningMessage"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleTestWarning"
>
<!-- label -->
<h2>Show warning if on non-prod server</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
<div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">
Notification Push Server
@@ -430,6 +464,7 @@ import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@@ -457,7 +492,7 @@ interface IAccount {
const inputFileNameRef = ref<Blob>();
@Component({ components: { QuickNav } })
@Component({ components: { QuickNav, TopMessage } })
export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@@ -470,6 +505,8 @@ export default class AccountViewView extends Vue {
downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
givenName = "";
isRegistered = false;
isSubscribed = false;
notificationMaybeChanged = false;
numAccounts = 0;
publicHex = "";
publicBase64 = "";
@@ -487,14 +524,74 @@ export default class AccountViewView extends Vue {
showAdvanced = false;
private isSubscribed = false;
subscription: PushSubscription | null = null;
warnIfProdServer = false;
warnIfTestServer = false;
get toggleNotifications() {
return this.isSubscribed;
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
set toggleNotifications(value) {
this.isSubscribed = value;
/**
* Async function executed when the component is created.
* Initializes the component's state with values from the database,
* handles identity-related tasks, and checks limitations.
*
* @throws Will display specific messages to the user based on different errors.
*/
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults
this.initializeState(settings);
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
} catch (err: unknown) {
this.handleError(err);
}
}
async mounted() {
try {
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
this.isSubscribed = !!this.subscription;
} catch (error) {
console.error("Mount error:", error);
}
}
beforeUnmount() {
if (this.downloadUrl) {
URL.revokeObjectURL(this.downloadUrl);
}
}
/**
* Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/
initializeState(settings: Settings | undefined) {
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.showContactGives = !!settings?.showContactGivesInline;
this.warnIfProdServer = !!settings?.warnIfProdServer;
this.warnIfTestServer = !!settings?.warnIfTestServer;
this.webPushServer = (settings?.webPushServer as string) || "";
this.webPushServerInput = (settings?.webPushServer as string) || "";
}
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
@@ -553,77 +650,20 @@ export default class AccountViewView extends Vue {
this.updateShowContactAmounts();
}
toggleProdWarning() {
this.warnIfProdServer = !this.warnIfProdServer;
this.updateWarnIfProdServer(this.warnIfProdServer);
}
toggleTestWarning() {
this.warnIfTestServer = !this.warnIfTestServer;
this.updateWarnIfTestServer(this.warnIfTestServer);
}
readableTime(timeStr: string) {
return timeStr.substring(0, timeStr.indexOf("T"));
}
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
/**
* Async function executed when the component is created.
* Initializes the component's state with values from the database,
* handles identity-related tasks, and checks limitations.
*
* @throws Will display specific messages to the user based on different errors.
*/
async created() {
console.error("created");
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults
this.initializeState(settings);
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
} catch (err: unknown) {
this.handleError(err);
}
}
async mounted() {
console.error("mounted()");
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
this.toggleNotifications = !!subscription;
} catch (error) {
console.error(error);
this.toggleNotifications = false;
}
}
beforeUnmount() {
if (this.downloadUrl) {
URL.revokeObjectURL(this.downloadUrl);
}
}
/**
* Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/
initializeState(settings: Settings | undefined) {
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.webPushServer = (settings?.webPushServer as string) || "";
this.webPushServerInput = (settings?.webPushServer as string) || "";
this.showContactGives = !!settings?.showContactGivesInline;
}
/**
* Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information.
@@ -648,6 +688,31 @@ export default class AccountViewView extends Vue {
}
}
async showNotificationChoice() {
if (!this.subscription) {
this.$notify(
{
group: "modal",
type: "notification-permission",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
} else {
this.$notify(
{
group: "modal",
type: "notification-off",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
}
this.notificationMaybeChanged = true;
}
/**
* Handles errors and updates the component's state accordingly.
* @param {Error} err - The error object.
@@ -686,12 +751,58 @@ export default class AccountViewView extends Vue {
group: "alert",
type: "danger",
title: "Error Updating Contact Setting",
text: "Clear your cache and start over (after data backup).",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to clear cache after contact setting update because:",
"Telling user to try again after contact setting update because:",
err,
);
}
}
public async updateWarnIfProdServer(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Prod Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after setting update because:",
err,
);
}
}
public async updateWarnIfTestServer(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Test Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after setting update because:",
err,
);
}
@@ -775,7 +886,7 @@ export default class AccountViewView extends Vue {
group: "alert",
type: "success",
title: "Download Started",
text: "See your downloads directory for the backup.",
text: "See your downloads directory for the backup. It is in the Dexie format.",
},
-1,
);

View File

@@ -19,9 +19,9 @@
<!-- Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<div>
<div class="block pb-4 flex gap-4 overflow-hidden">
<div class="block flex gap-4 overflow-hidden">
<div class="overflow-hidden">
<h2 class="text-xl">{{ veriClaim.id }}</h2>
<h2 class="text-md font-bold">{{ veriClaim.id }}</h2>
<div class="text-sm">
<div>
{{ veriClaim.claimType }}
@@ -45,7 +45,7 @@
</div>
<div>
<h2 class="font-bold text-2xl">Confirmations</h2>
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Confirmations</h2>
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
<span v-else-if="totalConfirmers() === 1">
@@ -136,22 +136,24 @@
</div>
<div>
<h2 class="font-bold text-2xl mt-8">Claim</h2>
<pre>{{ util.inspect(veriClaim, false, null) }}</pre>
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Claim</h2>
<pre class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md">
{{ util.inspect(veriClaim, false, null) }}
</pre>
</div>
<h2 class="font-bold text-2xl mt-8">Full Claim</h2>
<p>
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Full Claim</h2>
<p class="mb-4">
The full claim includes the claim as it was originally issued, including
the signature (ie. the proof of issuance by that person).
</p>
<div v-if="!fullClaim">
<div v-if="fullClaimMessage">
<p v-if="fullClaimMessage" class="mb-4">
{{ fullClaimMessage }}
</div>
</p>
<button
v-else
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
@click="showFullClaim(veriClaim.id)"
>
Load Full Claim Details
@@ -161,10 +163,12 @@
<pre>{{ util.inspect(fullClaim, false, null) }}</pre>
</div>
<a :href="apiServer + '/api/claim/' + veriClaim.id" target="_blank">
<button class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4">
View on the Public Server
</button>
<a
:href="apiServer + '/api/claim/' + veriClaim.id"
target="_blank"
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
>
View on the Public Server
</a>
</section>
</template>

View File

@@ -25,6 +25,13 @@
<span class="justify-around">(Only 50 most recent)</span>
<span />
</div>
<div class="flex justify-around">
<span />
<span class="justify-around">
(This does not include claims by them if they're not visible to you.)
</span>
<span />
</div>
<!-- Results List -->
<table
@@ -124,7 +131,7 @@ interface Notification {
}
@Component({ components: { QuickNav } })
export default class ContactsView extends Vue {
export default class ContactAmountssView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = "";
@@ -178,6 +185,7 @@ export default class ContactsView extends Vue {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.log("Error retrieving settings or gives.", err);
this.$notify(
{
group: "alert",
@@ -185,7 +193,7 @@ export default class ContactsView extends Vue {
title: "Error",
text:
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.",
"There was an error retrieving your settings and/or contacts and/or gives.",
},
-1,
);

View File

@@ -145,6 +145,7 @@ export default class ContactGiftingView extends Vue {
this.allContacts = await db.contacts.toArray();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.log("Error retrieving settings & contacts:", err);
this.$notify(
{
group: "alert",
@@ -152,7 +153,7 @@ export default class ContactGiftingView extends Vue {
title: "Error",
text:
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
"There was an error retrieving your settings and/or contacts.",
},
-1,
);

View File

@@ -18,6 +18,16 @@
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
Your Contact Info
</h1>
<p v-if="!givenName" class="text-center mt-2">
<span class="text-red">Beware!</span>
You aren't sharing your name, so hurry and
<router-link
:to="{ name: 'new-edit-account' }"
class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
>
go here to set it for them.
</router-link>
</p>
</div>
<div @click="onCopyToClipboard()" v-if="activeDid">
@@ -87,6 +97,7 @@ export default class ContactQRScanShow extends Vue {
activeDid = "";
apiServer = "";
givenName = "";
qrValue = "";
public async getIdentity(activeDid: string) {
@@ -111,6 +122,7 @@ export default class ContactQRScanShow extends Vue {
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.givenName = settings?.firstName || "";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();

View File

@@ -19,12 +19,13 @@
</div>
<!-- New Contact -->
<div class="mb-4 flex">
<span class="self-center bg-slate-500 text-white px-1.5 py-1 rounded-md">
<router-link :to="{ name: 'contact-qr' }">
<fa icon="qrcode" class="fa-fw" />
</router-link>
</span>
<div class="mb-4 flex items-stretch">
<router-link
:to="{ name: 'contact-qr' }"
class="flex items-center bg-slate-500 text-white px-1.5 py-1 mr-1 rounded-md"
>
<fa icon="qrcode" class="fa-fw text-2xl" />
</router-link>
<input
type="text"
placeholder="DID, Name, Public Key (base 16 or 64)"
@@ -170,7 +171,7 @@
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md"
@click="onClickAddGive(activeDid, contact.did)"
title="givenByMeDescriptions[contact.did]"
:title="givenByMeDescriptions[contact.did] || ''"
>
To:
{{
@@ -189,7 +190,7 @@
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-r-md -ml-1.5 border-l border-blue-400"
@click="onClickAddGive(contact.did, activeDid)"
title="givenToMeDescriptions[contact.did]"
:title="givenToMeDescriptions[contact.did] || ''"
>
From:
{{
@@ -366,6 +367,10 @@ export default class ContactsView extends Vue {
}
async loadGives() {
if (!this.activeDid) {
return;
}
const handleResponse = (
resp: { status: number; data: { data: GiveServerRecord[] } },
descriptions: Record<string, string>,
@@ -400,11 +405,11 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Retrieval Error",
text:
"Got an error retrieving your " +
(useRecipient ? "given" : "received") +
" time from the server.",
" data from the server.",
},
-1,
);
@@ -455,12 +460,13 @@ export default class ContactsView extends Vue {
this.givenToMeConfirmed = givenToMeConfirmed;
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
} catch (error) {
console.log("Error loading gives", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Error",
text: error as string,
title: "Load Error",
text: "Got an error loading your gives.",
},
-1,
);
@@ -711,6 +717,7 @@ export default class ContactsView extends Vue {
);
}
} catch (error) {
console.error("Error when registering:", error);
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
@@ -727,7 +734,7 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Registration Error",
text: userMessage,
},
-1,
@@ -773,27 +780,28 @@ export default class ContactsView extends Vue {
} else {
console.error(
"Got some bad server response when setting visibility: ",
resp.status,
resp,
);
const message =
resp.data.error?.message || "Bad server response of " + resp.status;
resp.data.error?.message || "Got some error setting visibility.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Error Setting Visibility",
text: message,
},
-1,
);
}
} catch (err) {
console.error("Got some server error when setting visibility:", err);
console.error("Got some error when setting visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Error Setting Visibility",
text: "Check connectivity and try again.",
},
-1,
@@ -836,19 +844,19 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Error Checking Visibility",
text: message,
},
-1,
);
}
} catch (err) {
console.log("Caught error from server request to check visibility:", err);
console.log("Caught error from request to check visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Error Checking Visibility",
text: "Check connectivity and try again.",
},
-1,
@@ -907,13 +915,13 @@ export default class ContactsView extends Vue {
},
-1,
);
} else if (!parseFloat(this.hourInput)) {
} else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Input Error",
text: "Giving 0 hours does nothing.",
text: "Giving no hours or descrption does nothing.",
},
-1,
);
@@ -1034,6 +1042,7 @@ export default class ContactsView extends Vue {
}
}
} catch (error) {
console.log("Error in createAndSubmitGive: ", error);
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
@@ -1050,7 +1059,7 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Error Sending Give",
text: userMessage,
},
-1,

View File

@@ -1,5 +1,6 @@
<template>
<QuickNav selected="Discover"></QuickNav>
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
@@ -136,6 +137,7 @@ import { didInfo, ProjectData } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import TopMessage from "@/components/TopMessage.vue";
interface Notification {
group: string;
@@ -149,6 +151,7 @@ interface Notification {
QuickNav,
InfiniteScroll,
EntityIcon,
TopMessage,
},
})
export default class DiscoverView extends Vue {
@@ -274,8 +277,8 @@ export default class DiscoverView extends Vue {
const plans: ProjectData[] = results.data;
if (plans) {
for (const plan of plans) {
const { name, description, handleId, rowid } = plan;
this.projects.push({ name, description, handleId, rowid });
const { name, description, handleId, issuerDid, rowid } = plan;
this.projects.push({ name, description, handleId, issuerDid, rowid });
}
this.remoteCount = this.projects.length;
} else {
@@ -357,8 +360,14 @@ export default class DiscoverView extends Vue {
if (beforeId) {
const plans: ProjectData[] = results.data;
for (const plan of plans) {
const { name, description, handleId = plan.handleId, rowid } = plan;
this.projects.push({ name, description, handleId, rowid });
const { name, description, handleId, issuerDid, rowid } = plan;
this.projects.push({
name,
description,
handleId,
issuerDid,
rowid,
});
}
} else {
this.projects = results.data;

View File

@@ -22,34 +22,219 @@
</div>
<div>
<p>Here are things to try to get notifications working.</p>
<p>Here are ways to test notifications and get them working.</p>
<h2 class="text-xl font-semibold">Test</h2>
<p>Somehow call the service-worker self.showNotification</p>
<h2 class="text-xl font-semibold">Full Test</h2>
<div>
<p>
If this works then you're all set.
<button
@click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Yourself a Test Web Push Message (Through Push Server but
Skipping Client Filter)
</button>
</p>
</div>
<h2 class="text-xl font-semibold">Check OS-level permissions</h2>
<p>
Walk-throughs & screenshots, maybe for all combinations of OS &
browsers.
</p>
<h2 class="text-xl font-semibold">If this app is not installed...</h2>
<div>
<p>
For best results on mobile, install this app on your device (as
opposed to using it inside the broser app). In Chrome, it may prompt
you, and you can also look for the "Install" command in the browser
settings; on the the deskop, look for this icon in the address bar:
<img
src="../assets/help/chrome-install-pwa.png"
alt="Chrome 'install' icon"
class="ml-4"
/>
</p>
</div>
<h2 class="text-xl font-semibold">Check browser-level permissions</h2>
<p>Walk-throughs & screenshots for browser settings</p>
<h2 class="text-xl font-semibold">
If "you must enable notifications"...
</h2>
<div>
<p>
Wait for about 10 seconds (for the service worker to activate), then
<button class="text-blue-500" @click="showNotificationChoice()">
click here.
</button>
</p>
</div>
<h2 class="text-xl font-semibold">Explain full reset to start again</h2>
<p>
Walk-throughs for clearing everything & subscribing anew to get a
message
</p>
<h2 class="text-xl font-semibold">Check App Permissions</h2>
<div>
<p>
In Apple iOS, check "Settings" -> "Notifications", look for the Time
Safari app (or the browser you're using), and make sure notifications
are enabled.
</p>
<p>
In Android, hold on to the app icon, then select "App Info", then
"Notifications" and make sure they're enabled. If it's still a problem
then go further:
</p>
<p>
If you installed the app with Chrome, make sure there are no other
tabs with it open. Here are some ways to clear caches that can mess
things up (and note that this clears out data from the installed app
-- which is good to do while the app is installed):
</p>
<ul>
<li class="list-disc ml-4">
Go to Chrome "App Info", then "Storage & Cache" and "Clear Storage".
</li>
<li class="list-disc ml-4">
Go to Chrome "Settings", then "Privacy and Security" and "Clear
"Clear browsing data", then "Cookies and site data". Also make sure
the "Time Range" at the top shows "All time".
</li>
</ul>
<p>
On a Mac, go to "Settings" and check "Notifications".
<img
src="../assets/help/mac-installed-app-settings.png"
alt="Mac app settings"
class="ml-4"
/>
</p>
</div>
<h2 class="text-xl font-semibold">Auto-detection</h2>
<p>Show results of auto-detection whether they're turned on</p>
<h2 class="text-xl font-semibold">Check Browser Permissions</h2>
<div>
<p>In Apple iOS, check Settings -> Notifications.</p>
<p>In Android, check Settings -> Notifications.</p>
You can find more details about compatibility
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
class="text-blue-500"
target="_blank"
>
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
<h2 class="text-xl font-semibold">Check OS Permissions</h2>
<div class="px-2">
<div>
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3>
<div>
Notifications require iOS 16.4 or higher. To check your iOS version,
go to Settings > General > About > Software Version.
</div>
<h3 class="text-lg font-semibold">Mobile Phone - Google Android</h3>
<div>
We recommend Chrome. It must be version 42 or higher. Check your
version under Settings -> About Chrome.
</div>
<h3 class="text-lg font-semibold">Desktop - Mac</h3>
<div>
<span>
Requires Mac OS 13; see your macOS version under Apple -> About
This Mac.
</span>
</div>
<h3 class="text-lg font-semibold">Windows desktop</h3>
In Windows, check Settings -> Notifications.
<img
src="../assets/help/windows-system-enable-notifications.png"
alt="Windows system settings"
class="ml-4"
/>
</div>
<div>
You can find more details about compatibility
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
class="text-blue-500"
target="_blank"
>
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
</div>
<h2 class="text-xl font-semibold">Reinstall</h2>
<div>
<p>
If all else fails, uninstall the app, ensure all the browser tabs with
it are closed, and clear out caches and storage.
</p>
<ul class="ml-4 list-disc">
<li>
Clear cache for site. (In Chrome, go to `chrome://settings/cookies`
and "all site data and permissions"; in Firefox, go to
`about:preferences` and search for "cache" then "Manage Data", and
also manually remove the IndexedDB data if the DBs still show.)
</li>
<li>
Clear notification permission. (in Chrome, go to
`chrome://settings/content/notifications`; in Firefox, go to
`about:preferences` and search for "notifications".)
</li>
<li>
Unregister service worker. (in Chrome, go to
`chrome://serviceworker-internals/`; in Firefox, go to
`about:serviceworkers`.)
</li>
<li>
Clear Cache Storage. (in Chrome, in dev tools under Application; in
Firefox, in dev tools under Storage.)
</li>
</ul>
<p>Then reinstall the app.</p>
</div>
<button
@click="showTestNotification()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Test Notification Directly to Device (Not Through Push Server)
</button>
<button
@click="alertWebPushSubscription()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Show Web Push Subscription Info
</button>
<button
@click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Yourself a Test Web Push Message (Through Push Server but Skipping
Client Filter)
</button>
<button
@click="sendTestWebPushMessage()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Yourself a Test Web Push Message (Through Push Server and Client
Filter)
</button>
</div>
</section>
</template>
<script lang="ts">
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface Notification {
group: string;
@@ -61,5 +246,156 @@ interface Notification {
@Component({ components: { QuickNav } })
export default class HelpNotificationsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
subscription: PushSubscription | null = null;
async mounted() {
try {
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
} catch (error) {
console.error("Mount error:", error);
}
}
alertWebPushSubscription() {
console.log(
"Web push subscription:",
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
);
alert(JSON.stringify(this.subscription));
}
async sendTestWebPushMessage(skipFilter: boolean = false) {
if (!this.subscription) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not Subscribed",
text: "You must enable notifications before testing the web push.",
},
-1,
);
return;
}
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
if (settings?.webPushServer) {
pushUrl = settings.webPushServer;
}
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
const auth = Buffer.from(this.subscription.getKey("auth"));
const authB64 = auth
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const p256dh = Buffer.from(this.subscription.getKey("p256dh"));
const p256dhB64 = p256dh
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const newPayload = {
endpoint: this.subscription.endpoint,
keys: {
auth: authB64,
p256dh: p256dhB64,
},
message: `Test, where you will see this message ${
skipFilter ? "un" : ""
}filtered.`,
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
};
console.log("Sending a test web push message:", newPayload);
const payloadStr = JSON.stringify(newPayload);
const response = await axios.post(
pushUrl + "/web-push/send-test",
payloadStr,
{
headers: {
"Content-Type": "application/json",
},
},
);
console.log("Got response from web push server:", response);
this.$notify(
{
group: "alert",
type: "success",
title: "Test Web Push Sent",
text: "Check your device for the test web push message, depending on the filtering you chose.",
},
-1,
);
} catch (error) {
console.error("Got an error sending test notification:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Sending Test",
text: "Got an error sending the test web push notification.",
},
-1,
);
}
}
showTestNotification() {
const TEST_NOTIFICATION_TITLE = "It Worked";
navigator.serviceWorker.ready
.then((registration) => {
return registration.showNotification(TEST_NOTIFICATION_TITLE, {
body: "This is your test notification.",
});
})
.then(() => {
this.$notify(
{
group: "alert",
type: "success",
title: "Sent",
text: `A notification was triggered, so one should show on your device entitled '${TEST_NOTIFICATION_TITLE}'.`,
},
5000,
);
})
.catch((error) => {
console.error("Got a notification error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Failed",
text: "Got an error sending a notification.",
},
-1,
);
});
}
showNotificationChoice() {
this.$notify(
{
group: "modal",
type: "notification-permission",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
}
}
</script>

View File

@@ -27,19 +27,20 @@
gifts and collaboration.
</p>
<h2 class="text-xl font-semibold">What is the philosophy here?</h2>
<h2 class="text-xl font-semibold">What is the idea here?</h2>
<p>
We are building networks of people who want to grow a giving society.
First of all, you can record ways you've seen people give, and that
leaves a permanent record -- one that came from you, and the recipient
can prove it was for them. This is personally gratifying, but it extends
to broader work: volunteers can get confirmation of activity and
selectively show off their contributions and network.
First of all, you can see what people have given, and also recognize
gifts you've seen, in a way that leaves a permanent record -- one that
came from you, and the recipient can prove it was for them. This is
personally gratifying, but it extends to broader work: volunteers get
confirmation of activity, and selectively show off their contributions
and network.
</p>
<p>
You can also record projects and plans and invite others to collaborate.
Soon you'll be able to see when others are interested and see how much
they're willing to contribute, even if there are conditions.
You can show giving and also offer help to ideas, based on others'
willingness to help out, too. You can record your own ideas and invite
others to collaborate.
</p>
<p>
This app uses the power of cryptography to build a reputation, recording
@@ -145,10 +146,8 @@
</h2>
<ul class="list-disc list-inside">
<li>
Make sure you have your backup file (above), then contact us with
your interest. This is functionality that has to be written, and
your interest will help us prioritize it, but there are also manual
ways to restore your data.
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
click Advanced, and follow the instructions for Data Import.
</li>
</ul>
</div>
@@ -163,6 +162,47 @@
</router-link>
</p>
<h2 class="text-xl font-semibold">How do I erase my data?</h2>
<p>
Before doing this, note the two kinds of data to backup: identity data,
and other data for contacts and settings (see instructions above).
</p>
<ul>
<li class="list-disc list-inside">
Mobile
<ul>
<li class="list-disc list-inside ml-4">
Chrome: Settings -> Privacy and Security -> Clear Browsing Data
</li>
<li class="list-disc list-inside ml-4">
DuckDuckGo: long hold -> Clear Data (takes effect immediately)
</li>
</ul>
</li>
<li class="list-disc list-inside">
Desktop
<ul>
<li class="list-disc list-inside ml-4">
Chrome:
<a href="chrome://settings/content/all" class="text-blue-500"
>clear here</a
>
also clear under dev tools Application
</li>
<li class="list-disc list-inside ml-4">
Firefox: <a href="about:preferences">go here</a>, Manage Data,
find timesafari.app and select, hit Remove Selected, then Save
Changes
</li>
<li class="list-disc list-inside ml-4">
Safari: Settings -> Privacy -> Manage Website Data, search for
timesafari.app and select, hit Remove Selected, then Done.
</li>
</ul>
</li>
</ul>
<p>To erase your data from our servers, contact us (below).</p>
<h2 class="text-xl font-semibold">
I know there is a record from someone, so why can't I see that info?
</h2>
@@ -181,6 +221,15 @@
different page.
</p>
<h2 class="text-xl font-semibold">
Where do I get help with notifications?
</h2>
<p>
<router-link class="text-blue-500" to="/help-notifications"
>Here.</router-link
>
</p>
<h2 class="text-xl font-semibold">
How do I access even more functionality?
</h2>
@@ -214,16 +263,16 @@
</p>
<h2 class="text-xl font-semibold">What app version is this?</h2>
<p>
{{ package.version }}
</p>
<p>{{ package.version }} ({{ commitHash }})</p>
<h2 class="text-xl font-semibold">
For any other questions, including removing your data:
</h2>
<p>
Contact us at
<a mailto="info@TimeSafari.app">info@TimeSafari.app</a>
<a href="mailto:info@TimeSafari.app" class="text-blue-500"
>info@TimeSafari.app</a
>
</p>
</div>
</section>
@@ -246,6 +295,7 @@ export default class Help extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
package = Package;
commitHash = process.env.VUE_APP_GIT_HASH;
showOnboardInfo() {
this.$notify(

View File

@@ -1,5 +1,7 @@
<template>
<QuickNav selected="Home"></QuickNav>
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
@@ -8,28 +10,59 @@
<!-- show the actions for recognizing a give -->
<div class="mb-8">
<div v-if="!activeDid">
To record others' giving,
<router-link :to="{ name: 'start' }" class="text-blue-500">
create your identifier.</router-link
<div
v-if="!isInstalled()"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<p>
You should install this as an app.
<router-link
:to="{ name: 'help-notifications' }"
class="text-blue-500"
>
Go here for instructions.
</router-link>
</p>
</div>
<div
v-if="!activeDid"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<p class="text-lg mb-3">
You need an <b>identifier</b> before you can record anyone's gives.
</p>
<router-link
:to="{ name: 'start' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
>
Create Your Identifier</router-link
>
</div>
<div v-else-if="!isRegistered">
To record others' giving, someone must register your account, so show
them
<router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
your identity info</router-link
<div
v-else-if="!isRegistered"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
Someone must register your account before you can record anyone's gives.
To do this:
<router-link
:to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
>
and then
<router-link :to="{ name: 'account' }" class="text-blue-500">
check your limits.</router-link
1. Show Them Your Identity Info</router-link
>
<router-link
:to="{ name: 'account' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
>
2. Check Your Limits</router-link
>
</div>
<div v-else>
<!-- activeDid && isRegistered -->
<h2 class="text-xl font-bold">Record Something Given</h2>
<h2 class="text-xl font-bold mb-4">Record Something Given</h2>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openDialog()">
@@ -87,42 +120,53 @@
showGivenToUser="true"
/>
<!-- Results List -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
<InfiniteScroll @reached-bottom="loadMoreGives">
<ul class="border-t border-slate-300">
<li
class="border-b border-slate-300 py-2"
v-for="record in feedData"
:key="record.jwtId"
>
<div
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedClaimId"
>
You've seen all the following
</div>
<div class="flex">
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
<span class="">{{ this.giveDescription(record) }}</span>
<a @click="onClickLoadClaim(record.jwtId)">
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
</a>
</div>
</li>
</ul>
</InfiniteScroll>
<div :class="{ hidden: isHiddenSpinner }">
<p class="text-slate-500 text-center italic mt-4 mb-4">
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip;
</p>
</div>
<ul class="border-t border-slate-300">
<li
class="border-b border-slate-300 py-2"
v-for="record in feedData"
:key="record.jwtId"
>
<div
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedId"
>
You've seen all the following
</div>
<div class="flex">
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
<span class="">{{ this.giveDescription(record) }}</span>
<a @click="onClickLoadClaim(record.jwtId)">
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
</a>
</div>
</li>
</ul>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
@@ -130,11 +174,7 @@ import {
GiverInputInfo,
GiveServerRecord,
} from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { IIdentifier } from "@veramo/core";
import { Account } from "@/db/tables/accounts";
interface Notification {
group: string;
@@ -144,7 +184,13 @@ interface Notification {
}
@Component({
components: { GiftedDialog, QuickNav, EntityIcon },
components: {
GiftedDialog,
QuickNav,
EntityIcon,
InfiniteScroll,
TopMessage,
},
})
export default class HomeView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@@ -153,10 +199,9 @@ export default class HomeView extends Vue {
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
feedAllLoaded = false;
feedData = [];
feedPreviousOldestId?: string;
feedLastViewedId?: string;
feedLastViewedClaimId?: string;
isHiddenSpinner = true;
isRegistered = false;
numAccounts = 0;
@@ -196,11 +241,15 @@ export default class HomeView extends Vue {
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId;
this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.isRegistered = !!settings?.isRegistered;
// this returns a Promise but we don't need to wait for it
this.updateAllFeed();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.log("Error retrieving settings and/or feed.", err);
this.$notify(
{
group: "alert",
@@ -208,13 +257,25 @@ export default class HomeView extends Vue {
title: "Error",
text:
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.",
"There was an error retrieving your settings and/or the latest activity.",
},
-1,
);
}
}
// from https://benborgers.com/posts/pwa-detect-installed
isInstalled() {
// For iOS
if ("standalone" in window.navigator) return true;
// For Android
if (window.matchMedia("(display-mode: standalone)").matches) return true;
// If neither is true, it's not installed
return false;
}
public async buildHeaders() {
const headers: HeadersInit = {
"Content-Type": "application/json",
@@ -241,24 +302,33 @@ export default class HomeView extends Vue {
return headers;
}
/**
* Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load
**/
public async loadMoreGives(payload: boolean) {
if (payload) {
this.updateAllFeed();
}
}
public async updateAllFeed() {
this.isHiddenSpinner = false;
await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId)
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
.then(async (results) => {
if (results.data.length > 0) {
this.feedData = this.feedData.concat(results.data);
this.feedAllLoaded = results.hitLimit;
this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId;
// The following update is only done on the first load.
if (
this.feedLastViewedId == null ||
this.feedLastViewedId < results.data[0].jwtId
this.feedLastViewedClaimId == null ||
this.feedLastViewedClaimId < results.data[0].jwtId
) {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: results.data[0].jwtId,
});
// but not for this page because we need to remember what it was before
}
}
})
@@ -268,17 +338,22 @@ export default class HomeView extends Vue {
{
group: "alert",
type: "danger",
title: "Export Error",
title: "Feed Error",
text: e.userMessage || "There was an error retrieving feed data.",
},
-1,
);
});
this.isHiddenSpinner = true;
}
public async retrieveClaims(endorserApiServer: string, beforeId?: string) {
/**
* Retrieve claims in reverse chronological order
*
* @param beforeId the earliest ID (of previous searches) to search earlier
* @return claims in reverse chronological order
*/
public async retrieveGives(endorserApiServer: string, beforeId?: string) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const response = await fetch(
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,

View File

@@ -68,7 +68,7 @@ export default class NewEditAccountView extends Vue {
});
localStorage.setItem("firstName", this.givenName as string);
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
this.$router.push({ name: "account" });
this.$router.back();
}
onClickCancel() {

View File

@@ -37,7 +37,7 @@
maxlength="5000"
></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
{{ fullClaim.description.length }}/5000 max. characters
{{ fullClaim.description?.length }}/5000 max. characters
</div>
<input

View File

@@ -88,7 +88,7 @@ export default class NewIdentifierView extends Vue {
this.loading = false;
setTimeout(() => {
this.$router.push({ name: "account" });
this.$router.push({ name: "home" });
}, 1000);
}
}

View File

@@ -1,5 +1,7 @@
<template>
<QuickNav />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
@@ -46,6 +48,7 @@
target="_blank"
class="underline"
>Map View
<fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
<div v-if="url">
@@ -88,8 +91,8 @@
</button>
</div>
<div class="mb-4">
<div v-if="activeDid" class="text-center">
<div v-if="activeDid" class="mb-4">
<div class="text-center">
<button
@click="openOfferDialog({ name: 'you', did: activeDid })"
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
@@ -99,17 +102,16 @@
</div>
</div>
<div>
<div v-if="activeDid" class="text-center">
<div v-if="activeDid">
<div class="text-center">
<button
@click="openGiftDialog({ name: 'you', did: activeDid })"
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
>
I gave&hellip;
</button>
<p class="mt-2 mb-4 text-center">Or, record a gift from:</p>
<p class="mt-2 mb-4 text-center">Or, record a contribution from:</p>
</div>
<p v-if="!activeDid" class="mt-2 mb-4">Record a gift from:</p>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openGiftDialog()">
@@ -234,28 +236,32 @@
<h3 class="text-sm uppercase font-semibold mb-3">
Contributions To This Idea
</h3>
<ul>
<li v-for="plan in fulfillersToThis" :key="plan.handleId">
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center">
<div v-for="plan in fulfillersToThis" :key="plan.handleId">
<button
@click="onClickLoadProject(plan.handleId)"
class="text-blue-500"
>
{{ plan.name }}
</button>
</li>
</ul>
</div>
</div>
</div>
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3">
Contributions By This Idea
</h3>
<button
@click="onClickLoadProject(fulfilledByThis.handleId)"
class="text-blue-500"
>
{{ fulfilledByThis.name }}
</button>
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center">
<button
@click="onClickLoadProject(fulfilledByThis.handleId)"
class="text-blue-500"
>
{{ fulfilledByThis.name }}
</button>
</div>
</div>
</div>
</div>
@@ -279,6 +285,7 @@ import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue";
import TopMessage from "@/components/TopMessage.vue";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@@ -303,7 +310,7 @@ interface Notification {
}
@Component({
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav, TopMessage },
})
export default class ProjectViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@@ -356,12 +363,6 @@ export default class ProjectViewView extends Vue {
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identity available.",
);
}
return identity;
}

View File

@@ -1,9 +1,11 @@
<template>
<QuickNav selected="Projects"></QuickNav>
<TopMessage />
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Plans
Your Ideas
</h1>
<!-- Quick Search -->
@@ -79,6 +81,7 @@ import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import TopMessage from "@/components/TopMessage.vue";
import { ProjectData } from "@/libs/endorserServer";
interface Notification {
@@ -89,7 +92,7 @@ interface Notification {
}
@Component({
components: { InfiniteScroll, QuickNav, EntityIcon },
components: { InfiniteScroll, QuickNav, EntityIcon, TopMessage },
})
export default class ProjectsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@@ -122,8 +125,8 @@ export default class ProjectsView extends Vue {
if (resp.status === 200 || !resp.data.data) {
const plans: ProjectData[] = resp.data.data;
for (const plan of plans) {
const { name, description, handleId, rowid } = plan;
this.projects.push({ name, description, handleId, rowid });
const { name, description, handleId, issuerDid, rowid } = plan;
this.projects.push({ name, description, handleId, issuerDid, rowid });
}
} else {
console.log("Bad server response & data:", resp.status, resp.data);