< 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 pt-4 mb-4" >
Your Identity
< / h1 >
< div class = "flex justify-between" >
< span / >
< span class = "whitespace-nowrap" >
< router -link
: to = "{ name: 'contact-qr' }"
class = "text-xs bg-slate-500 text-white px-1.5 py-1 rounded-md"
>
< fa icon = "qrcode" class = "fa-fw" > < / fa >
< / r o u t e r - l i n k >
< / span >
< span / >
< / div >
< div class = "flex justify-between py-2" >
< span / >
< span >
< router -link
: to = "{ name: 'help' }"
class = "text-xs 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-1.5 py-1 rounded-md ml-1"
>
Help
< / r o u t e r - l i n k >
< / 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 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
< / r o u t e r - l i n k >
< / div >
<!-- Identity Details -- >
< div class = "bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4" >
< div v-if ="givenName" >
< h2 class = "text-xl font-semibold mb-2" >
{ { givenName } }
< router -link : to = "{ name: 'new-edit-account' }" >
< fa icon = "pen" class = "text-xs text-blue-500 ml-2 mb-1" > < / fa >
< / r o u t e r - l i n k >
< / h2 >
< / div >
< span
v - else
class = "block w-full text-center text-md bg-amber-200 text-blue-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
>
< router -link
: to = "{ name: 'new-edit-account' }"
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"
>
Set Your Name
< / r o u t e r - l i n k >
< / 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 = "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-l"
/ >
< 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-r"
@ click = "openImageDialog()"
/ >
< / 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" >
< 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) }" >
Activity
< / r o u t e r - l i n k >
< / 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 = "!loadingLimits && !endorserLimits?.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"
>
< 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
< / r o u t e r - l i n k >
< / div >
< div 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
v - if = "!notificationMaybeChanged"
class = "flex items-center justify-between cursor-pointer"
@ click = "showNotificationChoice()"
>
<!-- label -- >
< div > App Notifications < / div >
<!-- toggle -- >
< div class = "relative ml-2" >
<!-- input -- >
< input
type = "checkbox"
v - model = "isSubscribed"
name = "toggleNotificationsInput"
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-else >
Notification status may have changed . Refresh this page to see the
latest setting .
< / div >
< router -link class = "pl-4 text-sm text-blue-500" to = "/help-notifications" >
Troubleshoot your notification setup .
< / r o u t e r - l i n k >
< / div >
< div class = "bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8" >
<!-- label -- >
< div class = "mb-2 font-bold" > Location < / div >
< router -link
: to = "{ name: 'search-area' }"
v - if = "activeDid"
class = "block w-full text-center text-m bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-6"
>
Set Search Area …
<!-- If already set , change button label to "Change Search Area" -- >
< / r o u t e r - l i n k >
< / div >
< div
v - if = "activeDid"
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 & hellip ; < fa icon = "spinner" class = "fa-spin" > < / fa >
< / div >
< div >
{ { limitsMessage } }
< / div >
< div v-if ="!!endorserLimits?.nextWeekBeginDateTime" >
< 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
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" >
{ { readableDate ( endorserLimits . nextMonthBeginDateTime ) } }
< / b >
< / p >
< p class = "mt-3 text-sm" v-if ="!!imageLimits" >
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 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-slate-400 to-slate-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
< / r o u t e r - l i n k >
< button
v - bind : class = "computedStartDownloadLinkClassNames()"
class = "block w-full 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-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 >
<!-- id used by puppeteer test script -- >
< h3
id = "advanced"
class = "text-sm uppercase font-semibold mb-3"
@ click = "showAdvanced = !showAdvanced"
>
Advanced
< / h3 >
< div 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 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
< / r o u t e r - l i n k >
< div class = "mt-4" >
< h2 class = "text-slate-500 text-sm font-bold" >
Contacts & Settings Database
< / h2 >
< div class = "ml-4 mt-2" >
Import
< input type = "file" @change ="uploadImportFile" class = "ml-2" / >
< div v-if ="showContactImport()" >
< 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()"
>
Import Settings & Contacts
< br / >
( excluding Identifier Data )
< / button >
< / div >
< / 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 >
< 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 >
< div class = "mt-2" >
< span class = "text-slate-500 text-sm font-bold" > Image Server URL < / span >
& nbsp ;
< 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
< / r o u t e r - l i n k >
< / button >
< / 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 { AxiosError } from "axios" ;
import { Buffer } from "buffer/" ;
import Dexie from "dexie" ;
import "dexie-export-import" ;
import { ImportProgress } from "dexie-export-import/dist/import" ;
import { IIdentifier } from "@veramo/core" ;
import { ref } from "vue" ;
import { Component , Vue } from "vue-facing-decorator" ;
import { useClipboard } from "@vueuse/core" ;
import EntityIcon from "@/components/EntityIcon.vue" ;
import ImageMethodDialog from "@/components/ImageMethodDialog.vue" ;
import QuickNav from "@/components/QuickNav.vue" ;
import TopMessage from "@/components/TopMessage.vue" ;
import {
AppString ,
DEFAULT_IMAGE_API_SERVER ,
DEFAULT_PUSH_SERVER ,
IMAGE_TYPE_PROFILE ,
NotificationIface ,
} from "@/constants/app" ;
import { db , accountsDB } from "@/db/index" ;
import { Account } from "@/db/tables/accounts" ;
import { MASTER_SETTINGS_KEY , Settings } from "@/db/tables/settings" ;
import { accessToken } from "@/libs/crypto" ;
import {
ErrorResponse ,
EndorserRateLimits ,
ImageRateLimits ,
fetchEndorserRateLimits ,
fetchImageRateLimits ,
} from "@/libs/endorserServer" ;
import { getAccount } from "@/libs/util" ;
const inputImportFileNameRef = ref < Blob > ( ) ;
@ Component ( {
components : { EntityIcon , ImageMethodDialog , QuickNav , TopMessage } ,
} )
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 ;
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 ;
imageServer = "" ;
isRegistered = false ;
isSubscribed = false ;
limitsMessage = "" ;
loadingLimits = false ;
notificationMaybeChanged = false ;
profileImageUrl ? : string ;
publicHex = "" ;
publicBase64 = "" ;
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 = "" ;
webPushServerInput = "" ;
/ * *
* 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 ( ) ;
const registration = await navigator . serviceWorker . ready ;
this . subscription = await registration . pushManager . getSubscription ( ) ;
this . isSubscribed = ! ! this . subscription ;
} catch ( error ) {
console . error ( "Mount error:" , error ) ;
this . handleError ( error ) ;
}
}
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 : Settings | undefined =
await db . settings . get ( MASTER_SETTINGS_KEY ) ;
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 . imageServer = ( settings ? . imageServer as string ) || "" ;
this . profileImageUrl = settings ? . profileImageUrl as string ;
this . showContactGives = ! ! settings ? . showContactGivesInline ;
this . hideRegisterPromptOnNewContact =
! ! settings ? . hideRegisterPromptOnNewContact ;
this . showGeneralAdvanced = ! ! settings ? . showGeneralAdvanced ;
this . showShortcutBvc = ! ! settings ? . showShortcutBvc ;
this . warnIfProdServer = ! ! settings ? . warnIfProdServer ;
this . warnIfTestServer = ! ! settings ? . warnIfTestServer ;
this . webPushServer = ( settings ? . webPushServer as string ) || "" ;
this . webPushServerInput = ( settings ? . webPushServer as string ) || "" ;
}
// 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 ) ) ;
}
toggleShowContactAmounts ( ) {
this . showContactGives = ! this . showContactGives ;
this . updateShowContactAmounts ( ) ;
}
toggleShowGeneralAdvanced ( ) {
this . showGeneralAdvanced = ! this . showGeneralAdvanced ;
this . updateShowGeneralAdvanced ( ) ;
}
toggleProdWarning ( ) {
this . warnIfProdServer = ! this . warnIfProdServer ;
this . updateWarnIfProdServer ( this . warnIfProdServer ) ;
}
toggleTestWarning ( ) {
this . warnIfTestServer = ! this . warnIfTestServer ;
this . updateWarnIfTestServer ( this . warnIfTestServer ) ;
}
toggleShowShortcutBvc ( ) {
this . showShortcutBvc = ! this . showShortcutBvc ;
this . updateShowShortcutBvc ( this . showShortcutBvc ) ;
}
readableDate ( timeStr : string ) {
return timeStr . substring ( 0 , timeStr . indexOf ( "T" ) ) ;
}
/ * *
* Processes the identity and updates the component ' s state .
* /
async processIdentity ( ) {
const account : Account | undefined = await getAccount ( 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 ;
this . checkLimitsFor ( this . activeDid ) ;
} else if ( account ? . publicKeyHex ) {
this . publicHex = account . publicKeyHex as string ;
this . publicBase64 = Buffer . from ( this . publicHex , "hex" ) . toString ( "base64" ) ;
this . checkLimitsFor ( this . activeDid ) ;
}
}
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 .
* /
handleError ( err : unknown ) {
if (
err instanceof Error &&
err . message ===
"Attempted to load account records with no identifier available."
) {
this . limitsMessage = "No identifier." ;
} else {
console . error ( "Telling user to clear cache at page create because:" , err ) ;
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Error Loading Account" ,
text : "Clear your cache and start over (after data backup)." ,
} ,
- 1 ,
) ;
}
}
public async updateShowContactAmounts ( ) {
try {
await db . open ( ) ;
await db . settings . update ( MASTER_SETTINGS_KEY , {
showContactGivesInline : this . showContactGives ,
} ) ;
} catch ( err ) {
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Error Updating Contact Setting" ,
text : "The setting may not have saved. Try again, maybe after restarting the app." ,
} ,
- 1 ,
) ;
console . error (
"Telling user to try again after contact-amounts setting update because:" ,
err ,
) ;
}
}
public async updateShowGeneralAdvanced ( ) {
try {
await db . open ( ) ;
await db . settings . update ( MASTER_SETTINGS_KEY , {
showGeneralAdvanced : this . showGeneralAdvanced ,
} ) ;
} catch ( err ) {
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Error Updating Advanced Setting" ,
text : "The setting may not have saved. Try again, maybe after restarting the app." ,
} ,
- 1 ,
) ;
console . error (
"Telling user to try again after general-advanced setting update because:" ,
err ,
) ;
}
}
public async updateWarnIfProdServer ( newSetting : boolean ) {
try {
await db . open ( ) ;
await 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 prod-server-warning setting update because:" ,
err ,
) ;
}
}
public async updateWarnIfTestServer ( newSetting : boolean ) {
try {
await db . open ( ) ;
await 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 test-server-warning setting update because:" ,
err ,
) ;
}
}
public async toggleHideRegisterPromptOnNewContact ( ) {
const newSetting = ! this . hideRegisterPromptOnNewContact ;
try {
await db . open ( ) ;
await db . settings . update ( MASTER_SETTINGS_KEY , {
hideRegisterPromptOnNewContact : newSetting ,
} ) ;
this . hideRegisterPromptOnNewContact = newSetting ;
} catch ( err ) {
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Error Updating Setting" ,
text : "The setting may not have saved. Try again, maybe after restarting the app." ,
} ,
- 1 ,
) ;
console . error ( "Telling user to try again because:" , err ) ;
}
}
public async updateShowShortcutBvc ( newSetting : boolean ) {
try {
await db . open ( ) ;
await db . settings . update ( MASTER_SETTINGS_KEY , {
showShortcutBvc : newSetting ,
} ) ;
} catch ( err ) {
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Error Updating BVC Shortcut Setting" ,
text : "The setting may not have saved. Try again, maybe after restarting the app." ,
} ,
- 1 ,
) ;
console . error (
"Telling user to try again after BVC-shortcut setting update because:" ,
err ,
) ;
}
}
/ * *
* 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 ) {
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Export Error" ,
text : "See console logs for more info." ,
} ,
- 1 ,
) ;
console . error ( "Export Error:" , error ) ;
}
async uploadImportFile ( event : Event ) {
inputImportFileNameRef . value = event . target . 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 ,
} ) ;
}
}
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." ;
}
}
/ * *
* Asynchronously checks rate limits for the given identity .
*
* Updates component state variables ` limits ` , ` limitsMessage ` , and ` loadingLimits ` .
* /
public 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 db . open ( ) ;
db . settings . update ( MASTER_SETTINGS_KEY , {
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." ,
} ,
- 1 ,
) ;
}
}
const imageResp = await fetchImageRateLimits ( this . axios , did ) ;
if ( imageResp . status === 200 ) {
this . imageLimits = imageResp . data ;
}
}
} catch ( error ) {
this . handleRateLimitsError ( error ) ;
try {
await db . open ( ) ;
db . settings . update ( MASTER_SETTINGS_KEY , {
isRegistered : false ,
} ) ;
this . isRegistered = false ;
} catch ( err ) {
console . error ( "Got an error marking user not registered:" , err ) ;
// already set an error notification for the user
}
}
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 ) {
const data = error . response ? . data as ErrorResponse ;
this . limitsMessage =
( data ? . error ? . message as string ) || "Bad server response." ;
console . error (
"Got bad response retrieving limits, which usually means user isn't registered." ,
) ;
//console.error(error);
} else {
this . limitsMessage = "Got an error retrieving limits." ;
console . error ( "Got some error retrieving limits:" , error ) ;
}
}
/ * *
* Asynchronously switches the active account based on the provided account number .
*
* @ param { number } accountNum - The account number to switch to . 0 means none .
* /
public async switchAccount ( accountNum : number ) {
await db . open ( ) ; // Assumes db needs to be open for both cases
if ( accountNum === 0 ) {
this . switchToNoAccount ( ) ;
} else {
await this . switchToAccountNumber ( accountNum ) ;
}
}
/ * *
* Switches to no active account and clears relevant properties .
* /
private async switchToNoAccount ( ) {
await db . settings . update ( MASTER_SETTINGS_KEY , { activeDid : undefined } ) ;
this . clearActiveAccountProperties ( ) ;
}
/ * *
* Clears properties related to the active account .
* /
private clearActiveAccountProperties ( ) {
this . activeDid = "" ;
this . derivationPath = "" ;
this . publicHex = "" ;
this . publicBase64 = "" ;
}
/ * *
* Switches to an account based on its number in the list .
*
* @ param { number } accountNum - The account number to switch to .
* /
private async switchToAccountNumber ( accountNum : number ) {
await accountsDB . open ( ) ;
const accounts = await accountsDB . accounts . toArray ( ) ;
const account = accounts [ accountNum - 1 ] ;
await db . open ( ) ;
await db . settings . update ( MASTER_SETTINGS_KEY , { activeDid : account . did } ) ;
this . updateActiveAccountProperties ( account ) ;
}
/ * *
* Updates properties related to the active account .
*
* @ param { AccountType } account - The account object .
* /
private updateActiveAccountProperties ( account : Account ) {
this . activeDid = account . did ;
this . derivationPath = account . derivationPath || "" ;
this . publicHex = account . publicKeyHex ;
this . publicBase64 = Buffer . from ( this . publicHex , "hex" ) . toString ( "base64" ) ;
}
public showContactGivesClassNames ( ) {
return {
"bg-slate-900" : ! this . showContactGives ,
"bg-green-600" : this . showContactGives ,
} ;
}
async onClickSaveApiServer ( ) {
await db . open ( ) ;
db . settings . update ( MASTER_SETTINGS_KEY , {
apiServer : this . apiServerInput ,
} ) ;
this . apiServer = this . apiServerInput ;
}
async onClickSavePushServer ( ) {
await db . open ( ) ;
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." ,
} ,
- 1 ,
) ;
}
openImageDialog ( ) {
( this . $refs . imageMethodDialog as ImageMethodDialog ) . open (
async ( imgUrl ) => {
await db . open ( ) ;
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 token = await accessToken ( this . activeDid ) ;
const response = await this . axios . delete (
DEFAULT_IMAGE_API_SERVER +
"/image/" +
encodeURIComponent ( this . profileImageUrl ) ,
{
headers : {
Authorization : ` Bearer ${ token } ` ,
} ,
} ,
) ;
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." ,
} ,
- 1 ,
) ;
// keep the imageUrl in localStorage so the user can try again if they want
}
await db . open ( ) ;
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 db . open ( ) ;
db . settings . update ( MASTER_SETTINGS_KEY , {
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." ,
} ,
5000 ,
) ;
}
}
}
}
< / script >