You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1989 lines
64 KiB
1989 lines
64 KiB
<template>
|
|
<QuickNav selected="Profile" />
|
|
<TopMessage />
|
|
|
|
<!-- CONTENT -->
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
<!-- Heading -->
|
|
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
|
Your Identity
|
|
</h1>
|
|
|
|
<!-- ID notice -->
|
|
<div
|
|
v-if="!activeDid"
|
|
id="noticeBeforeShare"
|
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
|
|
>
|
|
<p class="mb-4">
|
|
<b>Note:</b> Before you can share with others or take any action, you
|
|
need an identifier.
|
|
</p>
|
|
<router-link
|
|
:to="{ name: 'start' }"
|
|
class="inline-block text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
|
>
|
|
Create An Identifier
|
|
</router-link>
|
|
</div>
|
|
|
|
<!-- Identity Details -->
|
|
<div
|
|
id="sectionIdentityDetails"
|
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4"
|
|
>
|
|
<div v-if="givenName">
|
|
<h2 class="text-xl font-semibold mb-2">
|
|
<span class="whitespace-nowrap">
|
|
<router-link
|
|
:to="{ name: 'contact-qr' }"
|
|
class="bg-slate-500 text-white px-1.5 py-1 rounded-md"
|
|
>
|
|
<fa icon="qrcode" class="fa-fw text-xl"></fa>
|
|
</router-link>
|
|
</span>
|
|
{{ givenName }}
|
|
<router-link :to="{ name: 'new-edit-account' }">
|
|
<fa icon="pen" class="text-xs text-blue-500 ml-2 mb-1"></fa>
|
|
</router-link>
|
|
</h2>
|
|
</div>
|
|
<span
|
|
v-else
|
|
class="block w-full text-center text-md bg-amber-200 border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
|
|
>
|
|
<button
|
|
@click="
|
|
() =>
|
|
(this.$refs.userNameDialog as UserNameDialog).open(
|
|
(name) => (this.givenName = name),
|
|
)
|
|
"
|
|
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
|
>
|
|
Set Your Name
|
|
</button>
|
|
<UserNameDialog ref="userNameDialog" />
|
|
</span>
|
|
<div class="flex justify-center mt-4">
|
|
<span v-if="profileImageUrl" class="flex justify-between">
|
|
<EntityIcon
|
|
:icon-size="96"
|
|
:profileImageUrl="profileImageUrl"
|
|
class="inline-block align-text-bottom border border-slate-300 rounded"
|
|
@click="showLargeIdenticonUrl = profileImageUrl"
|
|
/>
|
|
<fa
|
|
icon="trash-can"
|
|
@click="confirmDeleteImage"
|
|
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
|
|
/>
|
|
</span>
|
|
<div v-else class="text-center">
|
|
<div class @click="openImageDialog()">
|
|
<fa
|
|
icon="image-portrait"
|
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-l"
|
|
/>
|
|
<fa
|
|
icon="camera"
|
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-r"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<ImageMethodDialog ref="imageMethodDialog" />
|
|
</div>
|
|
<div class="mt-6">
|
|
<div class="flex justify-center text-center">
|
|
People {{ profileImageUrl ? "without your image" : "" }} see this
|
|
<br />
|
|
(if you've let them see your activity):
|
|
</div>
|
|
<div class="flex justify-center">
|
|
<EntityIcon
|
|
:entityId="activeDid"
|
|
:iconSize="64"
|
|
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
|
@click="showLargeIdenticonId = activeDid"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="showLargeIdenticonId || showLargeIdenticonUrl"
|
|
class="fixed z-[100] top-0 inset-x-0 w-full"
|
|
>
|
|
<div
|
|
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
|
>
|
|
<EntityIcon
|
|
:entityId="showLargeIdenticonId"
|
|
:iconSize="512"
|
|
:profileImageUrl="showLargeIdenticonUrl"
|
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
|
@click="
|
|
showLargeIdenticonId = undefined;
|
|
showLargeIdenticonUrl = undefined;
|
|
"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-slate-500 text-sm font-bold">ID</div>
|
|
<div
|
|
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
|
data-testId="didWrapper"
|
|
>
|
|
<code class="truncate">{{ activeDid }}</code>
|
|
<button
|
|
@click="
|
|
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
|
|
"
|
|
class="ml-2"
|
|
>
|
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
</button>
|
|
<span v-show="showDidCopy">Copied</span>
|
|
</div>
|
|
|
|
<div class="text-blue-500 text-sm font-bold">
|
|
<router-link :to="{ path: '/did/' + encodeURIComponent(activeDid) }">
|
|
Your Activity
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Registration notice -->
|
|
<!--
|
|
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="!isRegistered"
|
|
id="noticeBeforeAnnounce"
|
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
|
|
>
|
|
<p class="mb-4">
|
|
<b>Note:</b> Before you can publicly announce a new project or time
|
|
commitment, a friend needs to register you.
|
|
</p>
|
|
<router-link
|
|
:to="{ name: 'contact-qr' }"
|
|
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
|
>
|
|
Share Your Info
|
|
</router-link>
|
|
</div>
|
|
|
|
<div
|
|
v-if="isRegistered"
|
|
id="sectionNotifications"
|
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
|
>
|
|
<!-- label -->
|
|
<div class="mb-2 font-bold">Notifications</div>
|
|
<div class="flex items-center justify-between">
|
|
<!-- label -->
|
|
<div>
|
|
Reminder Notification
|
|
<fa
|
|
icon="question-circle"
|
|
class="text-slate-400 fa-fw ml-2 cursor-pointer"
|
|
@click.stop="showReminderNotificationInfo"
|
|
/>
|
|
</div>
|
|
<!-- toggle -->
|
|
<div
|
|
class="relative ml-2 cursor-pointer"
|
|
@click="showReminderNotificationChoice()"
|
|
>
|
|
<!-- input -->
|
|
<input type="checkbox" v-model="notifyingReminder" 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>
|
|
</div>
|
|
<div v-if="notifyingReminder" class="w-full flex justify-between">
|
|
<span class="ml-8 mr-8">Message: "{{ notifyingReminderMessage }}"</span>
|
|
<span>{{ notifyingReminderTime.replace(" ", " ") }}</span>
|
|
</div>
|
|
<div class="mt-2 flex items-center justify-between">
|
|
<!-- label -->
|
|
<div>
|
|
New Activity Notification
|
|
<fa
|
|
icon="question-circle"
|
|
class="text-slate-400 fa-fw ml-2 cursor-pointer"
|
|
@click.stop="showNewActivityNotificationInfo"
|
|
/>
|
|
</div>
|
|
<!-- toggle -->
|
|
<div
|
|
class="relative ml-2 cursor-pointer"
|
|
@click="showNewActivityNotificationChoice()"
|
|
>
|
|
<!-- input -->
|
|
<input
|
|
type="checkbox"
|
|
v-model="notifyingNewActivity"
|
|
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>
|
|
</div>
|
|
<div v-if="notifyingNewActivityTime" class="w-full text-right">
|
|
{{ notifyingNewActivityTime.replace(" ", " ") }}
|
|
</div>
|
|
<router-link class="pl-4 text-sm text-blue-500" to="/help-notifications">
|
|
Troubleshoot your notifications.
|
|
</router-link>
|
|
</div>
|
|
<PushNotificationPermission ref="pushNotificationPermission" />
|
|
|
|
<div
|
|
id="sectionSearchLocation"
|
|
class="flex justify-between bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
|
>
|
|
<!-- label -->
|
|
<span class="mb-2 font-bold">Location for Searches</span>
|
|
<router-link
|
|
:to="{ name: 'search-area' }"
|
|
class="text-m bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2"
|
|
>
|
|
{{ isSearchAreasSet ? "Change" : "Set" }} Search Area…
|
|
</router-link>
|
|
</div>
|
|
|
|
<!-- User Profile -->
|
|
<div
|
|
v-if="isRegistered"
|
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
|
>
|
|
<div v-if="loadingProfile" class="text-center mb-2">
|
|
<fa icon="spinner" class="fa-spin text-slate-400"></fa> Loading
|
|
profile...
|
|
</div>
|
|
<div v-else class="flex items-center mb-2">
|
|
<span class="font-bold">Public Profile</span>
|
|
<fa
|
|
icon="circle-info"
|
|
class="text-slate-400 fa-fw ml-2 cursor-pointer"
|
|
@click="showProfileInfo"
|
|
/>
|
|
</div>
|
|
<textarea
|
|
v-model="userProfileDesc"
|
|
class="w-full h-32 p-2 border border-slate-300 rounded-md"
|
|
placeholder="Write something about yourself for the public..."
|
|
:readonly="loadingProfile || savingProfile"
|
|
:class="{ 'bg-slate-100': loadingProfile || savingProfile }"
|
|
></textarea>
|
|
|
|
<div class="flex items-center mb-4" @click="toggleUserProfileLocation">
|
|
<input
|
|
type="checkbox"
|
|
class="mr-2"
|
|
v-model="includeUserProfileLocation"
|
|
/>
|
|
<label for="includeUserProfileLocation">Include Location</label>
|
|
</div>
|
|
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
|
|
<p class="text-sm mb-2 text-slate-500">
|
|
For your security, choose a location nearby but not exactly at your
|
|
place.
|
|
</p>
|
|
|
|
<l-map
|
|
ref="profileMap"
|
|
class="!z-40 rounded-md"
|
|
@click="
|
|
(event: LeafletMouseEvent) => {
|
|
userProfileLatitude = event.latlng.lat;
|
|
userProfileLongitude = event.latlng.lng;
|
|
}
|
|
"
|
|
@ready="onMapReady"
|
|
>
|
|
<l-tile-layer
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
layer-type="base"
|
|
name="OpenStreetMap"
|
|
/>
|
|
<l-marker
|
|
v-if="userProfileLatitude && userProfileLongitude"
|
|
:lat-lng="[userProfileLatitude, userProfileLongitude]"
|
|
@click="confirmEraseLatLong()"
|
|
/>
|
|
</l-map>
|
|
</div>
|
|
<div v-if="!loadingProfile && !savingProfile">
|
|
<div class="flex justify-between items-center">
|
|
<button
|
|
@click="saveProfile"
|
|
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
|
:disabled="loadingProfile || savingProfile"
|
|
:class="{
|
|
'opacity-50 cursor-not-allowed': loadingProfile || savingProfile,
|
|
}"
|
|
>
|
|
Save Profile
|
|
</button>
|
|
<button
|
|
@click="confirmDeleteProfile"
|
|
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
|
:disabled="loadingProfile || savingProfile"
|
|
:class="{
|
|
'opacity-50 cursor-not-allowed':
|
|
loadingProfile ||
|
|
savingProfile ||
|
|
(!userProfileDesc && !includeUserProfileLocation),
|
|
}"
|
|
>
|
|
Delete Profile
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="loadingProfile">Loading...</div>
|
|
<div v-else>Saving...</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="activeDid"
|
|
id="sectionUsageLimits"
|
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
|
>
|
|
<div class="mb-2 font-bold">Usage Limits</div>
|
|
<!-- show spinner if loading limits -->
|
|
<div v-if="loadingLimits" class="text-center">
|
|
Checking… <fa icon="spinner" class="fa-spin"></fa>
|
|
</div>
|
|
<div class="mb-4 text-center">
|
|
{{ limitsMessage }}
|
|
</div>
|
|
<div>
|
|
<p class="text-sm">
|
|
You have done
|
|
<b>{{ endorserLimits?.doneClaimsThisWeek || "?" }} claims</b> out of
|
|
<b>{{ endorserLimits?.maxClaimsPerWeek || "?" }}</b> for this week.
|
|
Your claims counter resets at
|
|
<b class="whitespace-nowrap">{{
|
|
readableDate(endorserLimits?.nextWeekBeginDateTime)
|
|
}}</b>
|
|
</p>
|
|
<p class="mt-3 text-sm">
|
|
You have done
|
|
<b
|
|
>{{
|
|
endorserLimits?.doneRegistrationsThisMonth || "?"
|
|
}}
|
|
registrations</b
|
|
>
|
|
out of
|
|
<b>{{ endorserLimits?.maxRegistrationsPerMonth || "?" }}</b> for this
|
|
this month.
|
|
<i>(You cannot register anyone on your first day.)</i>
|
|
Your registration counter resets at
|
|
<b class="whitespace-nowrap">
|
|
{{ readableDate(endorserLimits?.nextMonthBeginDateTime) }}
|
|
</b>
|
|
</p>
|
|
<p class="mt-3 text-sm">
|
|
You have uploaded
|
|
<b>{{ imageLimits?.doneImagesThisWeek || "?" }} images</b> out of
|
|
<b>{{ imageLimits?.maxImagesPerWeek || "?" }}</b> for this week. Your
|
|
image counter resets at
|
|
<b class="whitespace-nowrap">{{
|
|
readableDate(imageLimits?.nextWeekBeginDateTime)
|
|
}}</b>
|
|
</p>
|
|
</div>
|
|
<button
|
|
class="block float-right w-fit text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mt-2"
|
|
@click="checkLimits()"
|
|
>
|
|
Recheck Limits
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
id="sectionDataExport"
|
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
|
>
|
|
<div class="mb-2 font-bold">Data Export</div>
|
|
<router-link
|
|
:to="{ name: 'seed-backup' }"
|
|
v-if="activeDid"
|
|
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
|
|
>
|
|
Backup Identifier Seed
|
|
</router-link>
|
|
|
|
<button
|
|
v-bind:class="computedStartDownloadLinkClassNames()"
|
|
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
|
@click="exportDatabase()"
|
|
>
|
|
Download Settings & Contacts
|
|
<br />
|
|
(excluding Identifier Data)
|
|
</button>
|
|
<a
|
|
ref="downloadLink"
|
|
v-bind:class="computedDownloadLinkClassNames()"
|
|
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
|
>
|
|
If no download happened yet, click again here to download now.
|
|
</a>
|
|
<div class="mt-4">
|
|
<p>
|
|
After the download, you can save the file in your preferred storage
|
|
location.
|
|
</p>
|
|
<ul>
|
|
<li class="list-disc list-outside ml-4">
|
|
On iOS: Choose "More..." and select a place in iCloud, or go "Back"
|
|
and save to another location.
|
|
</li>
|
|
<li class="list-disc list-outside ml-4">
|
|
On Android: Choose "Open" and then share
|
|
<fa icon="share-nodes" class="fa-fw" />
|
|
to your prefered place.
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- id used by puppeteer test script -->
|
|
<h3
|
|
id="advanced"
|
|
class="text-sm uppercase font-semibold mb-3"
|
|
@click="showAdvanced = !showAdvanced"
|
|
>
|
|
Advanced
|
|
</h3>
|
|
<div id="sectionAdvanced" v-if="showAdvanced || showGeneralAdvanced">
|
|
<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 freedom!
|
|
</p>
|
|
|
|
<!-- Deep Identity Details -->
|
|
<span class="text-slate-500 text-sm font-bold mb-2">
|
|
Deep Identifier Details
|
|
</span>
|
|
<div
|
|
id="sectionDeepIdentifier"
|
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"
|
|
>
|
|
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
|
<div
|
|
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
|
>
|
|
<code class="truncate">{{ publicBase64 }}</code>
|
|
<button
|
|
@click="
|
|
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
|
|
"
|
|
class="ml-2"
|
|
>
|
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
</button>
|
|
<span v-show="showB64Copy">Copied</span>
|
|
</div>
|
|
|
|
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
|
|
<div
|
|
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
|
>
|
|
<code class="truncate">{{ publicHex }}</code>
|
|
<button
|
|
@click="
|
|
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
|
|
"
|
|
class="ml-2"
|
|
>
|
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
</button>
|
|
<span v-show="showPubCopy">Copied</span>
|
|
</div>
|
|
|
|
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
|
|
<div
|
|
v-if="derivationPath"
|
|
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
|
>
|
|
<code class="truncate">{{ derivationPath }}</code>
|
|
<button
|
|
@click="
|
|
doCopyTwoSecRedo(
|
|
derivationPath,
|
|
() => (showDerCopy = !showDerCopy),
|
|
)
|
|
"
|
|
class="ml-2"
|
|
>
|
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
</button>
|
|
<span v-show="showDerCopy">Copied</span>
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
|
>
|
|
(none)
|
|
</div>
|
|
</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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2"
|
|
>
|
|
Switch Identifier
|
|
</router-link>
|
|
|
|
<div id="sectionImportContactsSettings" class="mt-4">
|
|
<h2 class="text-slate-500 text-sm font-bold">
|
|
Import Contacts & Settings Database
|
|
</h2>
|
|
|
|
<div class="ml-4 mt-2">
|
|
<input type="file" @change="uploadImportFile" class="ml-2" />
|
|
<transition
|
|
enter-active-class="transform ease-out duration-300 transition"
|
|
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
|
|
enter-to-class="translate-y-0 opacity-100 sm:translate-y-0"
|
|
leave-active-class="transition ease-in duration-500"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div v-if="showContactImport()" class="mt-4">
|
|
<div class="flex justify-center">
|
|
<button
|
|
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
|
@click="confirmSubmitImportFile()"
|
|
>
|
|
Overwrite Settings & Contacts
|
|
<br />
|
|
(which doesn't include Identifier Data)
|
|
</button>
|
|
</div>
|
|
<div class="flex justify-center">
|
|
<button
|
|
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
|
@click="checkContactImports()"
|
|
>
|
|
Import Only Contacts
|
|
<br />
|
|
after comparing
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</div>
|
|
|
|
<label
|
|
for="toggleShowAmounts"
|
|
class="flex items-center justify-between cursor-pointer my-4"
|
|
@click="toggleShowContactAmounts"
|
|
>
|
|
<!-- label -->
|
|
<span class="text-slate-500 text-sm font-bold">Contacts Display</span>
|
|
<span class="ml-2">Show hours given & received</span>
|
|
<!-- toggle -->
|
|
<div class="relative ml-2">
|
|
<!-- input -->
|
|
<input
|
|
type="checkbox"
|
|
v-model="showContactGives"
|
|
name="showContactGives"
|
|
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 id="sectionClaimServer">
|
|
<h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2>
|
|
<div class="px-4 py-4">
|
|
<input
|
|
type="text"
|
|
class="block w-full rounded border border-slate-400 px-4 py-2"
|
|
v-model="apiServerInput"
|
|
/>
|
|
<button
|
|
v-if="apiServerInput != apiServer"
|
|
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
|
@click="onClickSaveApiServer()"
|
|
>
|
|
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
|
</button>
|
|
<button
|
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
|
|
>
|
|
Use Prod
|
|
</button>
|
|
<button
|
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
|
|
>
|
|
Use Test
|
|
</button>
|
|
<button
|
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
|
|
>
|
|
Use Local
|
|
</button>
|
|
</div>
|
|
|
|
<label
|
|
for="toggleProdWarningMessage"
|
|
class="flex items-center justify-between cursor-pointer px-4 py-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 px-4 py-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>
|
|
|
|
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
|
Notification Push Server
|
|
</h2>
|
|
<div class="px-3 py-4">
|
|
<input
|
|
type="text"
|
|
class="block w-full rounded border border-slate-400 px-3 py-2"
|
|
v-model="webPushServerInput"
|
|
/>
|
|
<button
|
|
v-if="webPushServerInput != webPushServer"
|
|
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
|
@click="onClickSavePushServer()"
|
|
>
|
|
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
|
</button>
|
|
<button
|
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
@click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
|
|
>
|
|
Use Prod
|
|
</button>
|
|
<button
|
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
@click="webPushServerInput = AppConstants.TEST1_PUSH_SERVER"
|
|
>
|
|
Use Test 1
|
|
</button>
|
|
<button
|
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
@click="webPushServerInput = AppConstants.TEST2_PUSH_SERVER"
|
|
>
|
|
Use Test 2
|
|
</button>
|
|
</div>
|
|
<span class="px-4 text-sm" v-if="!webPushServerInput">
|
|
When that setting is blank, this app will use the default web push
|
|
server URL:
|
|
{{ DEFAULT_PUSH_SERVER }}
|
|
</span>
|
|
|
|
<h2 class="text-slate-500 text-sm font-bold mb-2">Partner Server URL</h2>
|
|
<div class="px-3 py-4">
|
|
<input
|
|
type="text"
|
|
class="block w-full rounded border border-slate-400 px-3 py-2"
|
|
v-model="partnerApiServerInput"
|
|
/>
|
|
<button
|
|
v-if="partnerApiServerInput != partnerApiServer"
|
|
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
|
@click="onClickSavePartnerServer()"
|
|
>
|
|
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
|
</button>
|
|
<button
|
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
@click="partnerApiServerInput = AppConstants.PROD_PARTNER_API_SERVER"
|
|
>
|
|
Use Prod
|
|
</button>
|
|
<button
|
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
@click="partnerApiServerInput = AppConstants.TEST_PARTNER_API_SERVER"
|
|
>
|
|
Use Test
|
|
</button>
|
|
<button
|
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
@click="partnerApiServerInput = AppConstants.LOCAL_PARTNER_API_SERVER"
|
|
>
|
|
Use Local
|
|
</button>
|
|
</div>
|
|
<span class="px-4 text-sm" v-if="!partnerApiServerInput">
|
|
When that setting is blank, this app will use the default partner server
|
|
URL:
|
|
{{ DEFAULT_PARTNER_API_SERVER }}
|
|
</span>
|
|
|
|
<div class="mt-2">
|
|
<span class="text-slate-500 text-sm font-bold">Image Server URL</span>
|
|
|
|
<span class="text-sm">{{ DEFAULT_IMAGE_API_SERVER }}</span>
|
|
</div>
|
|
|
|
<label
|
|
for="toggleHideRegisterPromptOnNewContact"
|
|
class="flex items-center justify-between cursor-pointer mt-4"
|
|
@click="toggleHideRegisterPromptOnNewContact()"
|
|
>
|
|
<!-- label -->
|
|
<span class="text-slate-500 text-sm font-bold">
|
|
Hide Register Prompt on New Contact
|
|
</span>
|
|
<!-- toggle -->
|
|
<div class="relative ml-2">
|
|
<!-- input -->
|
|
<input
|
|
type="checkbox"
|
|
v-model="hideRegisterPromptOnNewContact"
|
|
class="sr-only"
|
|
/>
|
|
<!-- line -->
|
|
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
|
|
<!-- dot -->
|
|
<div
|
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
/>
|
|
</div>
|
|
</label>
|
|
|
|
<label
|
|
for="toggleShowShortcutBvc"
|
|
class="flex items-center justify-between cursor-pointer mt-4"
|
|
@click="toggleShowShortcutBvc"
|
|
>
|
|
<!-- label -->
|
|
<span class="text-slate-500 text-sm font-bold">
|
|
Show BVC Shortcut on Home Page
|
|
</span>
|
|
<!-- toggle -->
|
|
<div class="relative ml-2">
|
|
<!-- input -->
|
|
<input type="checkbox" v-model="showShortcutBvc" class="sr-only" />
|
|
<!-- line -->
|
|
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
|
|
<!-- dot -->
|
|
<div
|
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
/>
|
|
</div>
|
|
</label>
|
|
|
|
<div class="flex mt-4">
|
|
<button>
|
|
<router-link
|
|
:to="{ name: 'statistics' }"
|
|
class="block w-fit text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2"
|
|
>
|
|
See Global Animated History of Giving
|
|
</router-link>
|
|
</button>
|
|
</div>
|
|
|
|
<div id="sectionPasskeyExpiration" class="flex justify-between">
|
|
<span>
|
|
<span class="text-slate-500 text-sm font-bold mb-2">
|
|
Passkey Expiration Minutes
|
|
</span>
|
|
<br />
|
|
<span class="text-sm ml-2">
|
|
{{ passkeyExpirationDescription }}
|
|
</span>
|
|
</span>
|
|
<div class="relative ml-2">
|
|
<input
|
|
type="number"
|
|
class="border border-slate-400 rounded px-2 py-2 text-center w-20"
|
|
v-model="passkeyExpirationMinutes"
|
|
@change="updatePasskeyExpiration"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<label
|
|
for="toggleShowGeneralAdvanced"
|
|
class="flex items-center justify-between cursor-pointer mt-4"
|
|
@click="toggleShowGeneralAdvanced"
|
|
>
|
|
<!-- label -->
|
|
<span class="text-slate-500 text-sm font-bold">
|
|
Show All General Advanced Functions
|
|
</span>
|
|
<!-- toggle -->
|
|
<div class="relative ml-2">
|
|
<!-- input -->
|
|
<input
|
|
type="checkbox"
|
|
v-model="showGeneralAdvanced"
|
|
class="sr-only"
|
|
/>
|
|
<!-- line -->
|
|
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
|
|
<!-- dot -->
|
|
<div
|
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
/>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import "leaflet/dist/leaflet.css";
|
|
|
|
import { AxiosError } from "axios";
|
|
import { Buffer } from "buffer/";
|
|
import Dexie from "dexie";
|
|
import "dexie-export-import";
|
|
import { ImportProgress } from "dexie-export-import";
|
|
import { LeafletMouseEvent } from "leaflet";
|
|
import * as R from "ramda";
|
|
import { IIdentifier } from "@veramo/core";
|
|
import { ref } from "vue";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { Router } from "vue-router";
|
|
import { useClipboard } from "@vueuse/core";
|
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
|
|
|
import EntityIcon from "../components/EntityIcon.vue";
|
|
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
|
|
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
|
|
import QuickNav from "../components/QuickNav.vue";
|
|
import TopMessage from "../components/TopMessage.vue";
|
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
|
import {
|
|
AppString,
|
|
DEFAULT_IMAGE_API_SERVER,
|
|
DEFAULT_PARTNER_API_SERVER,
|
|
DEFAULT_PUSH_SERVER,
|
|
IMAGE_TYPE_PROFILE,
|
|
NotificationIface,
|
|
} from "../constants/app";
|
|
import {
|
|
db,
|
|
logConsoleAndDb,
|
|
retrieveSettingsForActiveAccount,
|
|
updateAccountSettings,
|
|
} from "../db/index";
|
|
import { Account } from "../db/tables/accounts";
|
|
import { Contact } from "../db/tables/contacts";
|
|
import {
|
|
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
|
MASTER_SETTINGS_KEY,
|
|
} from "../db/tables/settings";
|
|
import {
|
|
clearPasskeyToken,
|
|
EndorserRateLimits,
|
|
ErrorResponse,
|
|
errorStringForLog,
|
|
fetchEndorserRateLimits,
|
|
fetchImageRateLimits,
|
|
getHeaders,
|
|
ImageRateLimits,
|
|
tokenExpiryTimeDescription,
|
|
} from "../libs/endorserServer";
|
|
import {
|
|
DAILY_CHECK_TITLE,
|
|
DIRECT_PUSH_TITLE,
|
|
retrieveAccountMetadata,
|
|
} from "../libs/util";
|
|
|
|
const inputImportFileNameRef = ref<Blob>();
|
|
|
|
@Component({
|
|
components: {
|
|
EntityIcon,
|
|
ImageMethodDialog,
|
|
LeafletMouseEvent,
|
|
LMap,
|
|
LMarker,
|
|
LTileLayer,
|
|
PushNotificationPermission,
|
|
QuickNav,
|
|
TopMessage,
|
|
UserNameDialog,
|
|
},
|
|
})
|
|
export default class AccountViewView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
AppConstants = AppString;
|
|
DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;
|
|
DEFAULT_IMAGE_API_SERVER = DEFAULT_IMAGE_API_SERVER;
|
|
DEFAULT_PARTNER_API_SERVER = DEFAULT_PARTNER_API_SERVER;
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
apiServerInput = "";
|
|
derivationPath = "";
|
|
downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
|
|
endorserLimits: EndorserRateLimits | null = null;
|
|
givenName = "";
|
|
hideRegisterPromptOnNewContact = false;
|
|
imageLimits: ImageRateLimits | null = null;
|
|
includeUserProfileLocation = false;
|
|
isRegistered = false;
|
|
isSearchAreasSet = false;
|
|
limitsMessage = "";
|
|
loadingLimits = false;
|
|
loadingProfile = true;
|
|
notifyingNewActivity = false;
|
|
notifyingNewActivityTime = "";
|
|
notifyingReminder = false;
|
|
notifyingReminderMessage = "";
|
|
notifyingReminderTime = "";
|
|
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
|
partnerApiServerInput = DEFAULT_PARTNER_API_SERVER;
|
|
passkeyExpirationDescription = "";
|
|
passkeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
|
|
previousPasskeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
|
|
profileImageUrl?: string;
|
|
publicHex = "";
|
|
publicBase64 = "";
|
|
savingProfile = false;
|
|
showAdvanced = false;
|
|
showB64Copy = false;
|
|
showContactGives = false;
|
|
showDidCopy = false;
|
|
showDerCopy = false;
|
|
showGeneralAdvanced = false;
|
|
showLargeIdenticonId?: string;
|
|
showLargeIdenticonUrl?: string;
|
|
showPubCopy = false;
|
|
showShortcutBvc = false;
|
|
subscription: PushSubscription | null = null;
|
|
warnIfProdServer = false;
|
|
warnIfTestServer = false;
|
|
webPushServer = DEFAULT_PUSH_SERVER;
|
|
webPushServerInput = DEFAULT_PUSH_SERVER;
|
|
userProfileDesc = "";
|
|
userProfileLatitude = 0;
|
|
userProfileLongitude = 0;
|
|
zoom = 2;
|
|
|
|
/**
|
|
* Async function executed when the component is mounted.
|
|
* 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 mounted() {
|
|
try {
|
|
// Initialize component state with values from the database or defaults
|
|
await this.initializeState();
|
|
await this.processIdentity();
|
|
|
|
// Load the user profile
|
|
if (this.isRegistered) {
|
|
try {
|
|
const headers = await getHeaders(this.activeDid);
|
|
const response = await this.axios.get(
|
|
this.apiServer +
|
|
"/api/partner/userProfileForIssuer/" +
|
|
this.activeDid,
|
|
{ headers },
|
|
);
|
|
if (response.status === 200) {
|
|
this.userProfileDesc = response.data.data.description || "";
|
|
this.userProfileLatitude = response.data.data.locLat || 0;
|
|
this.userProfileLongitude = response.data.data.locLon || 0;
|
|
if (this.userProfileLatitude && this.userProfileLongitude) {
|
|
this.includeUserProfileLocation = true;
|
|
}
|
|
} else {
|
|
// won't get here because axios throws an error instead
|
|
throw Error("Unable to load profile.");
|
|
}
|
|
} catch (error) {
|
|
if (error.status === 404) {
|
|
// this is ok: the profile is not yet created
|
|
} else {
|
|
logConsoleAndDb(
|
|
"Error loading profile: " + errorStringForLog(error),
|
|
);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Loading Profile",
|
|
text: "Your server profile is not available.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} finally {
|
|
this.loadingProfile = false;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// this can happen when running automated tests in dev mode because notifications don't work
|
|
console.error(
|
|
"Telling user to clear cache at page create because:",
|
|
error,
|
|
);
|
|
// this sometimes gives different information on the error
|
|
console.error(
|
|
"To repeat with concatenated error: telling user to clear cache at page create because: " +
|
|
error,
|
|
);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Loading Profile",
|
|
text: "See the Help page about errors with your personal data.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
try {
|
|
/**
|
|
* Beware! I've seen where this "ready" never resolves.
|
|
*/
|
|
const registration = await navigator.serviceWorker?.ready;
|
|
this.subscription = await registration.pushManager.getSubscription();
|
|
if (!this.subscription) {
|
|
if (this.notifyingNewActivity || this.notifyingReminder) {
|
|
// the app thought there was a subscription but there isn't, so fix the settings
|
|
this.turnOffNotifyingFlags();
|
|
}
|
|
}
|
|
// console.log("Got to the end of 'mounted' call in AccountViewView.");
|
|
/**
|
|
* Beware! I've seen where we never get to this point because "ready" never resolves.
|
|
*/
|
|
} catch (error) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "Cannot Set Notifications",
|
|
text: "This browser does not support notifications. Use Chrome, or install this to the home screen, or try other suggestions on the 'Troubleshoot your notifications' page.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
|
|
}
|
|
|
|
beforeUnmount() {
|
|
if (this.downloadUrl) {
|
|
URL.revokeObjectURL(this.downloadUrl);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes component state with values from the database or defaults.
|
|
*/
|
|
async initializeState() {
|
|
await db.open();
|
|
const settings = await retrieveSettingsForActiveAccount();
|
|
|
|
this.activeDid = settings.activeDid || "";
|
|
this.apiServer = settings.apiServer || "";
|
|
this.apiServerInput = settings.apiServer || "";
|
|
this.givenName =
|
|
(settings?.firstName || "") +
|
|
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
|
this.hideRegisterPromptOnNewContact =
|
|
!!settings.hideRegisterPromptOnNewContact;
|
|
this.isRegistered = !!settings?.isRegistered;
|
|
this.isSearchAreasSet = !!settings.searchBoxes;
|
|
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
|
|
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
|
|
this.notifyingReminder = !!settings.notifyingReminderTime;
|
|
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
|
|
this.notifyingReminderTime = settings.notifyingReminderTime || "";
|
|
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
|
|
this.partnerApiServerInput =
|
|
settings.partnerApiServer || this.partnerApiServerInput;
|
|
this.profileImageUrl = settings.profileImageUrl;
|
|
this.showContactGives = !!settings.showContactGivesInline;
|
|
this.passkeyExpirationMinutes =
|
|
settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES;
|
|
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
|
|
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
|
|
this.showShortcutBvc = !!settings.showShortcutBvc;
|
|
this.warnIfProdServer = !!settings.warnIfProdServer;
|
|
this.warnIfTestServer = !!settings.warnIfTestServer;
|
|
this.webPushServer = settings.webPushServer || this.webPushServer;
|
|
this.webPushServerInput = settings.webPushServer || this.webPushServerInput;
|
|
}
|
|
|
|
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
|
doCopyTwoSecRedo(text: string, fn: () => void) {
|
|
fn();
|
|
useClipboard()
|
|
.copy(text)
|
|
.then(() => setTimeout(fn, 2000));
|
|
}
|
|
|
|
async toggleShowContactAmounts() {
|
|
this.showContactGives = !this.showContactGives;
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
showContactGivesInline: this.showContactGives,
|
|
});
|
|
}
|
|
|
|
async toggleShowGeneralAdvanced() {
|
|
this.showGeneralAdvanced = !this.showGeneralAdvanced;
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
showGeneralAdvanced: this.showGeneralAdvanced,
|
|
});
|
|
}
|
|
|
|
async toggleProdWarning() {
|
|
this.warnIfProdServer = !this.warnIfProdServer;
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
warnIfProdServer: this.warnIfProdServer,
|
|
});
|
|
}
|
|
|
|
async toggleTestWarning() {
|
|
this.warnIfTestServer = !this.warnIfTestServer;
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
warnIfTestServer: this.warnIfTestServer,
|
|
});
|
|
}
|
|
|
|
async toggleShowShortcutBvc() {
|
|
this.showShortcutBvc = !this.showShortcutBvc;
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
showShortcutBvc: this.showShortcutBvc,
|
|
});
|
|
}
|
|
|
|
readableDate(timeStr: string) {
|
|
return timeStr ? timeStr.substring(0, timeStr.indexOf("T")) : "?";
|
|
}
|
|
|
|
/**
|
|
* Processes the identity and updates the component's state.
|
|
*/
|
|
async processIdentity() {
|
|
const account: Account | undefined = await retrieveAccountMetadata(
|
|
this.activeDid,
|
|
);
|
|
if (account?.identity) {
|
|
const identity = JSON.parse(account.identity as string) as IIdentifier;
|
|
this.publicHex = identity.keys[0].publicKeyHex;
|
|
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
|
this.derivationPath = identity.keys[0].meta?.derivationPath as string;
|
|
await this.checkLimits();
|
|
} else if (account?.publicKeyHex) {
|
|
this.publicHex = account.publicKeyHex as string;
|
|
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
|
await this.checkLimits();
|
|
}
|
|
}
|
|
|
|
async showNewActivityNotificationInfo() {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "New Activity Notification",
|
|
text: `
|
|
This will only notify you when there is new relevant activity for you personally.
|
|
Note that it runs on your device and many factors may affect delivery,
|
|
so if you want a reliable but simple daily notification then choose a 'Reminder'.
|
|
Do you want more details?
|
|
`,
|
|
onYes: async () => {
|
|
await (this.$router as Router).push({
|
|
name: "help-notification-types",
|
|
});
|
|
},
|
|
yesText: "tell me more.",
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
async showNewActivityNotificationChoice() {
|
|
if (!this.notifyingNewActivity) {
|
|
(
|
|
this.$refs.pushNotificationPermission as PushNotificationPermission
|
|
).open(DAILY_CHECK_TITLE, async (success: boolean, timeText: string) => {
|
|
if (success) {
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
notifyingNewActivityTime: timeText,
|
|
});
|
|
this.notifyingNewActivity = true;
|
|
this.notifyingNewActivityTime = timeText;
|
|
}
|
|
});
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "notification-off",
|
|
title: DAILY_CHECK_TITLE, // repurposed to indicate the type of notification
|
|
text: "", // unused, only here to satisfy type check
|
|
callback: async (success) => {
|
|
if (success) {
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
notifyingNewActivityTime: "",
|
|
});
|
|
this.notifyingNewActivity = false;
|
|
this.notifyingNewActivityTime = "";
|
|
}
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
}
|
|
|
|
async showReminderNotificationInfo() {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Reminder Notification",
|
|
text: `
|
|
This will notify you at a specific time each day.
|
|
Note that it does not give you personalized notifications,
|
|
so if you want less reliable but personalized notification then choose a 'New Activity' Notification.
|
|
Do you want more details?
|
|
`,
|
|
onYes: async () => {
|
|
await (this.$router as Router).push({
|
|
name: "help-notification-types",
|
|
});
|
|
},
|
|
yesText: "tell me more.",
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
async showReminderNotificationChoice() {
|
|
if (!this.notifyingReminder) {
|
|
(
|
|
this.$refs.pushNotificationPermission as PushNotificationPermission
|
|
).open(
|
|
DIRECT_PUSH_TITLE,
|
|
async (success: boolean, timeText: string, message?: string) => {
|
|
if (success) {
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
notifyingReminderMessage: message,
|
|
notifyingReminderTime: timeText,
|
|
});
|
|
this.notifyingReminder = true;
|
|
this.notifyingReminderMessage = message || "";
|
|
this.notifyingReminderTime = timeText;
|
|
}
|
|
},
|
|
);
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "notification-off",
|
|
title: DIRECT_PUSH_TITLE, // repurposed to indicate the type of notification
|
|
text: "", // unused, only here to satisfy type check
|
|
callback: async (success) => {
|
|
if (success) {
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
notifyingReminderMessage: "",
|
|
notifyingReminderTime: "",
|
|
});
|
|
this.notifyingReminder = false;
|
|
this.notifyingReminderMessage = "";
|
|
this.notifyingReminderTime = "";
|
|
}
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
}
|
|
|
|
public async toggleHideRegisterPromptOnNewContact() {
|
|
const newSetting = !this.hideRegisterPromptOnNewContact;
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
hideRegisterPromptOnNewContact: newSetting,
|
|
});
|
|
this.hideRegisterPromptOnNewContact = newSetting;
|
|
}
|
|
|
|
public async updatePasskeyExpiration() {
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
passkeyExpirationMinutes: this.passkeyExpirationMinutes,
|
|
});
|
|
clearPasskeyToken();
|
|
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
|
|
}
|
|
|
|
public async turnOffNotifyingFlags() {
|
|
// should tell the push server as well
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
notifyingNewActivityTime: "",
|
|
notifyingReminderMessage: "",
|
|
notifyingReminderTime: "",
|
|
});
|
|
this.notifyingNewActivity = false;
|
|
this.notifyingNewActivityTime = "";
|
|
this.notifyingReminder = false;
|
|
this.notifyingReminderMessage = "";
|
|
this.notifyingReminderTime = "";
|
|
}
|
|
|
|
/**
|
|
* Asynchronously exports the database into a downloadable JSON file.
|
|
*
|
|
* @throws Will notify the user if there is an export error.
|
|
*/
|
|
public async exportDatabase() {
|
|
try {
|
|
// Generate the blob from the database
|
|
const blob = await this.generateDatabaseBlob();
|
|
|
|
// Create a temporary URL for the blob
|
|
this.downloadUrl = this.createBlobURL(blob);
|
|
|
|
// Trigger the download
|
|
this.downloadDatabaseBackup(this.downloadUrl);
|
|
|
|
// Notify the user that the download has started
|
|
this.notifyDownloadStarted();
|
|
|
|
// Revoke the temporary URL -- after a pause to avoid DuckDuckGo download failure
|
|
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
|
} catch (error) {
|
|
this.handleExportError(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a blob object representing the database.
|
|
*
|
|
* @returns {Promise<Blob>} The generated blob object.
|
|
*/
|
|
private async generateDatabaseBlob(): Promise<Blob> {
|
|
return await db.export({ prettyJson: true });
|
|
}
|
|
|
|
/**
|
|
* Creates a temporary URL for a blob object.
|
|
*
|
|
* @param {Blob} blob - The blob object.
|
|
* @returns {string} The temporary URL for the blob.
|
|
*/
|
|
private createBlobURL(blob: Blob): string {
|
|
return URL.createObjectURL(blob);
|
|
}
|
|
|
|
/**
|
|
* Triggers the download of the database backup.
|
|
*
|
|
* @param {string} url - The temporary URL for the blob.
|
|
*/
|
|
private downloadDatabaseBackup(url: string) {
|
|
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
|
downloadAnchor.href = url;
|
|
downloadAnchor.download = `${db.name}-backup.json`;
|
|
downloadAnchor.click(); // doesn't work for some browsers, eg. DuckDuckGo
|
|
}
|
|
|
|
public computedStartDownloadLinkClassNames() {
|
|
return {
|
|
hidden: this.downloadUrl,
|
|
};
|
|
}
|
|
|
|
public computedDownloadLinkClassNames() {
|
|
return {
|
|
hidden: !this.downloadUrl,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Notifies the user that the download has started.
|
|
*/
|
|
private notifyDownloadStarted() {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Download Started",
|
|
text: "See your downloads directory for the backup. It is in the Dexie format.",
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handles errors during the database export process.
|
|
*
|
|
* @param {Error} error - The error object.
|
|
*/
|
|
private handleExportError(error: unknown) {
|
|
console.error("Export Error:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Export Error",
|
|
text: "There was an error exporting the data.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
|
|
async uploadImportFile(event: Event) {
|
|
inputImportFileNameRef.value = (event.target as EventTarget).files[0];
|
|
}
|
|
|
|
showContactImport() {
|
|
return !!inputImportFileNameRef.value;
|
|
}
|
|
|
|
confirmSubmitImportFile() {
|
|
if (inputImportFileNameRef.value != null) {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Replace All",
|
|
text:
|
|
"This will replace all settings and contacts, so we recommend you first do the backup step above." +
|
|
" Are you sure you want to import and replace all contacts and settings?",
|
|
onYes: this.submitImportFile,
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asynchronously imports the database from a downloadable JSON file.
|
|
*
|
|
* @throws Will notify the user if there is an export error.
|
|
*/
|
|
async submitImportFile() {
|
|
if (inputImportFileNameRef.value != null) {
|
|
await db.delete();
|
|
await Dexie.import(inputImportFileNameRef.value as Blob, {
|
|
progressCallback: this.progressCallback,
|
|
});
|
|
}
|
|
}
|
|
|
|
async checkContactImports() {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const fileContent: string = (event.target?.result as string) || "{}";
|
|
try {
|
|
const contents = JSON.parse(fileContent);
|
|
const contactTableRows: Array<Contact> = (
|
|
contents.data?.data as [{ tableName: string; rows: Array<Contact> }]
|
|
)?.find((table) => table.tableName === "contacts")
|
|
?.rows as Array<Contact>;
|
|
const contactRows = contactTableRows.map(
|
|
// @ts-expect-error for omitting this field that is found in the Dexie format
|
|
(contact) => R.omit(["$types"], contact) as Contact,
|
|
);
|
|
(this.$router as Router).push({
|
|
name: "contact-import",
|
|
query: { contacts: JSON.stringify(contactRows) },
|
|
});
|
|
} catch (error) {
|
|
console.error("Error checking contact imports:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Importing",
|
|
text: "There was an error reading that Dexie file.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
};
|
|
reader.readAsText(inputImportFileNameRef.value as Blob);
|
|
}
|
|
|
|
private progressCallback(progress: ImportProgress) {
|
|
console.log(
|
|
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
|
|
);
|
|
if (progress.done) {
|
|
// console.log(`Imported ${progress.completedTables} tables.`);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Import Complete",
|
|
text: "",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async checkLimits() {
|
|
if (this.activeDid) {
|
|
this.checkLimitsFor(this.activeDid);
|
|
} else {
|
|
this.limitsMessage =
|
|
"You have no identifier, or your data has been corrupted.";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use "checkLimits" instead.
|
|
*
|
|
* Asynchronously checks rate limits for the given identity.
|
|
*
|
|
* Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
|
|
*/
|
|
private async checkLimitsFor(did: string) {
|
|
this.loadingLimits = true;
|
|
this.limitsMessage = "";
|
|
|
|
try {
|
|
const resp = await fetchEndorserRateLimits(
|
|
this.apiServer,
|
|
this.axios,
|
|
did,
|
|
);
|
|
if (resp.status === 200) {
|
|
this.endorserLimits = resp.data;
|
|
if (!this.isRegistered) {
|
|
// the user was not known to be registered, but now they are (because we got no error) so let's record it
|
|
try {
|
|
await updateAccountSettings(did, { isRegistered: true });
|
|
this.isRegistered = true;
|
|
} catch (err) {
|
|
console.error("Got an error updating settings:", err);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Update Error",
|
|
text: "Unable to update your settings. Check claim limits again.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
try {
|
|
const imageResp = await fetchImageRateLimits(this.axios, did);
|
|
if (imageResp.status === 200) {
|
|
this.imageLimits = imageResp.data;
|
|
} else {
|
|
this.limitsMessage = "You don't have access to upload images.";
|
|
}
|
|
} catch {
|
|
this.limitsMessage = "You cannot upload images.";
|
|
}
|
|
}
|
|
} catch (error) {
|
|
this.handleRateLimitsError(error);
|
|
}
|
|
|
|
this.loadingLimits = false;
|
|
}
|
|
|
|
/**
|
|
* Handles errors that occur while fetching rate limits.
|
|
*
|
|
* @param {AxiosError | Error} error - The error object.
|
|
*/
|
|
private handleRateLimitsError(error: unknown) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.status == 400 || error.status == 404) {
|
|
// no worries: they probably just aren't registered and don't have any limits
|
|
console.log(
|
|
"Got 400 or 404 response retrieving limits which probably means they're not registered:",
|
|
error,
|
|
);
|
|
this.limitsMessage = "No limits were found, so no actions are allowed.";
|
|
} else {
|
|
const data = error.response?.data as ErrorResponse;
|
|
this.limitsMessage =
|
|
(data?.error?.message as string) || "Bad server response.";
|
|
console.error("Got bad response retrieving limits:", error);
|
|
}
|
|
} else {
|
|
this.limitsMessage = "Got an error retrieving limits.";
|
|
console.error("Got some error retrieving limits:", error);
|
|
}
|
|
}
|
|
|
|
async onClickSaveApiServer() {
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
apiServer: this.apiServerInput,
|
|
});
|
|
this.apiServer = this.apiServerInput;
|
|
}
|
|
|
|
async onClickSavePartnerServer() {
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
partnerApiServer: this.partnerApiServerInput,
|
|
});
|
|
this.partnerApiServer = this.partnerApiServerInput;
|
|
}
|
|
|
|
async onClickSavePushServer() {
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
webPushServer: this.webPushServerInput,
|
|
});
|
|
this.webPushServer = this.webPushServerInput;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "Reload",
|
|
text: "Now reload the app to get a new VAPID to use with this push server.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
openImageDialog() {
|
|
(this.$refs.imageMethodDialog as ImageMethodDialog).open(
|
|
async (imgUrl) => {
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
profileImageUrl: imgUrl,
|
|
});
|
|
this.profileImageUrl = imgUrl;
|
|
//console.log("Got image URL:", imgUrl);
|
|
},
|
|
IMAGE_TYPE_PROFILE,
|
|
true,
|
|
);
|
|
}
|
|
|
|
confirmDeleteImage() {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title:
|
|
"Note that anyone with you already as a contact will no longer see a picture, and you will have to reshare your data with them if you save a new picture. Are you sure you want to delete your profile picture?",
|
|
text: "",
|
|
onYes: this.deleteImage,
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
async deleteImage() {
|
|
if (!this.profileImageUrl) {
|
|
return;
|
|
}
|
|
try {
|
|
const headers = await getHeaders(this.activeDid);
|
|
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
|
|
if (
|
|
window.location.hostname === "localhost" &&
|
|
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
|
) {
|
|
console.log(
|
|
"Using shared image API server, so only users on that server can play with images.",
|
|
);
|
|
}
|
|
const response = await this.axios.delete(
|
|
DEFAULT_IMAGE_API_SERVER +
|
|
"/image/" +
|
|
encodeURIComponent(this.profileImageUrl),
|
|
{ headers },
|
|
);
|
|
if (response.status === 204) {
|
|
// don't bother with a notification
|
|
// (either they'll simply continue or they're canceling and going back)
|
|
} else {
|
|
console.error("Non-success deleting image:", response);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "There was a problem deleting the image. Contact support if you want it removed from the servers.",
|
|
},
|
|
5000,
|
|
);
|
|
// keep the imageUrl in localStorage so the user can try again if they want
|
|
}
|
|
|
|
await db.open();
|
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
profileImageUrl: undefined,
|
|
});
|
|
|
|
this.profileImageUrl = undefined;
|
|
} catch (error) {
|
|
console.error("Error deleting image:", error);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if ((error as any).response.status === 404) {
|
|
console.error("The image was already deleted:", error);
|
|
|
|
await updateAccountSettings(this.activeDid, {
|
|
profileImageUrl: undefined,
|
|
});
|
|
|
|
this.profileImageUrl = undefined;
|
|
|
|
// it already doesn't exist so we won't say anything to the user
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "There was an error deleting the image.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
onMapReady(map: L.Map) {
|
|
// doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
|
|
const zoom = this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
|
|
map.setView([this.userProfileLatitude, this.userProfileLongitude], zoom);
|
|
}
|
|
|
|
showProfileInfo() {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "info",
|
|
title: "Public Profile Information",
|
|
text: "This data will be published for all to see, so be careful what your write. Your ID will only be shared with people who you allow to see your activity.",
|
|
},
|
|
7000,
|
|
);
|
|
}
|
|
|
|
async saveProfile() {
|
|
this.savingProfile = true;
|
|
try {
|
|
const headers = await getHeaders(this.activeDid);
|
|
const payload: UserProfile = {
|
|
description: this.userProfileDesc,
|
|
};
|
|
if (this.userProfileLatitude && this.userProfileLongitude) {
|
|
payload.locLat = this.userProfileLatitude;
|
|
payload.locLon = this.userProfileLongitude;
|
|
} else if (this.includeUserProfileLocation) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "toast",
|
|
title: "",
|
|
text: "No profile location is saved.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
const response = await this.axios.post(
|
|
this.apiServer + "/api/partner/userProfile",
|
|
payload,
|
|
{ headers },
|
|
);
|
|
if (response.status === 201) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Profile Saved",
|
|
text: "Your profile has been updated successfully.",
|
|
},
|
|
3000,
|
|
);
|
|
} else {
|
|
// won't get here because axios throws an error on non-success
|
|
throw Error("Profile not saved");
|
|
}
|
|
} catch (error) {
|
|
logConsoleAndDb("Error saving profile: " + errorStringForLog(error));
|
|
const errorMessage: string =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
"There was an error saving your profile.";
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Saving Profile",
|
|
text: errorMessage,
|
|
},
|
|
3000,
|
|
);
|
|
} finally {
|
|
this.savingProfile = false;
|
|
}
|
|
}
|
|
|
|
toggleUserProfileLocation() {
|
|
this.includeUserProfileLocation = !this.includeUserProfileLocation;
|
|
if (!this.includeUserProfileLocation) {
|
|
this.userProfileLatitude = 0;
|
|
this.userProfileLongitude = 0;
|
|
this.zoom = 2;
|
|
}
|
|
}
|
|
|
|
confirmEraseLatLong() {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Erase Marker",
|
|
text: "Are you sure you don't want to mark a location? This will erase the current location.",
|
|
onYes: async () => {
|
|
this.eraseLatLong();
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
eraseLatLong() {
|
|
this.userProfileLatitude = 0;
|
|
this.userProfileLongitude = 0;
|
|
this.zoom = 2;
|
|
this.includeUserProfileLocation = false;
|
|
}
|
|
|
|
async confirmDeleteProfile() {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Delete Profile",
|
|
text: "Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.",
|
|
onYes: this.deleteProfile,
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
async deleteProfile() {
|
|
this.savingProfile = true;
|
|
try {
|
|
const headers = await getHeaders(this.activeDid);
|
|
const response = await this.axios.delete(
|
|
this.apiServer + "/api/partner/userProfile",
|
|
{ headers },
|
|
);
|
|
if (response.status === 204) {
|
|
this.userProfileDesc = "";
|
|
this.userProfileLatitude = 0;
|
|
this.userProfileLongitude = 0;
|
|
this.includeUserProfileLocation = false;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Profile Deleted",
|
|
text: "Your profile has been deleted successfully.",
|
|
},
|
|
3000,
|
|
);
|
|
} else {
|
|
throw Error("Profile not deleted");
|
|
}
|
|
} catch (error) {
|
|
logConsoleAndDb("Error deleting profile: " + errorStringForLog(error));
|
|
const errorMessage: string =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
"There was an error deleting your profile.";
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Deleting Profile",
|
|
text: errorMessage,
|
|
},
|
|
3000,
|
|
);
|
|
} finally {
|
|
this.savingProfile = false;
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|