make a backup download for browsers that don't get it automatically #101

Merged
trentlarson merged 5 commits from another-download into master 11 months ago
  1. 149
      src/views/AccountViewView.vue
  2. 9
      src/views/ContactsView.vue

149
src/views/AccountViewView.vue

@ -100,6 +100,7 @@
<router-link <router-link
:to="{ name: 'new-edit-account' }" :to="{ name: 'new-edit-account' }"
class="block text-center text-lg font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md mb-2" class="block text-center text-lg font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md mb-2"
v-if="activeDid"
> >
Edit Identity Edit Identity
</router-link> </router-link>
@ -138,7 +139,7 @@
</router-link> </router-link>
</div> </div>
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3> <h3 class="text-sm uppercase font-semibold mb-3">Data Export</h3>
<router-link <router-link
:to="{ name: 'seed-backup' }" :to="{ name: 'seed-backup' }"
@ -147,7 +148,9 @@
> >
Backup Identifier Seed Backup Identifier Seed
</router-link> </router-link>
<button <button
v-bind:class="computedStartDownloadLinkClassNames()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="exportDatabase()" @click="exportDatabase()"
> >
@ -155,21 +158,27 @@
<br /> <br />
(excluding Identifier Data) (excluding Identifier Data)
</button> </button>
<a ref="downloadLink" /> <a
ref="downloadLink"
v-bind:class="computedDownloadLinkClassNames()"
class="block w-full text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
>
If no download happened yet, click again here to download now.
</a>
<div v-if="activeDid" class="my-8"> <div v-if="activeDid" class="flex mt-8 py-2">
<h3 class="text-sm uppercase font-semibold mb-3">Rate Limits</h3> <h3 class="text-sm uppercase font-semibold">Rate Limits</h3>
<button <button
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md ml-2 mr-2 mb-2"
@click="checkLimits()" @click="checkLimits()"
> >
Check Limits Check Limits
</button> </button>
<!-- show spinner if loading limits --> <!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="text-center mb-4"> <div v-if="loadingLimits" class="text-center">
Checking&hellip; <fa icon="spinner" class="fa-spin"></fa> Checking&hellip; <fa icon="spinner" class="fa-spin"></fa>
</div> </div>
<div class="mb-4"> <div>
{{ limitsMessage }} {{ limitsMessage }}
</div> </div>
<div v-if="!!limits?.nextWeekBeginDateTime"> <div v-if="!!limits?.nextWeekBeginDateTime">
@ -293,18 +302,37 @@
</div> </div>
</label> </label>
<div class="grid-cols-2 mb-4">
<span class="text-slate-500 text-sm font-bold mb-2">Data Import</span>
<input type="file" @change="uploadFile" class="ml-2" />
<div v-if="showContactImport()">
<button
class="block text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
@click="submitFile()"
>
Import Settings & Contacts
<br />
(excluding Identifier Data)
</button>
</div>
</div>
<div class="flex py-2">
<button>
<router-link <router-link
:to="{ name: 'statistics' }" :to="{ name: 'statistics' }"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
> >
See Animated Global History of Giving See Global Animated History of Giving
</router-link> </router-link>
</button>
</div>
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
<router-link <router-link
id="switch-identity-link" id="switch-identity-link"
:to="{ name: 'identity-switcher' }" :to="{ name: 'identity-switcher' }"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
> >
Switch Identity Switch Identity
</router-link> </router-link>
@ -429,7 +457,9 @@
<script lang="ts"> <script lang="ts">
import { AxiosError, AxiosRequestConfig } from "axios"; import { AxiosError, AxiosRequestConfig } from "axios";
import Dexie from "dexie";
import "dexie-export-import"; import "dexie-export-import";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
@ -441,6 +471,7 @@ import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { ErrorResponse, RateLimits } from "@/libs/endorserServer"; import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
import { ImportProgress } from "dexie-export-import/dist/import";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
@ -459,6 +490,8 @@ interface IAccount {
derivationPath: string; derivationPath: string;
} }
const inputFileNameRef = ref<Blob>();
@Component({ components: { QuickNav, TopMessage } }) @Component({ components: { QuickNav, TopMessage } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@ -469,6 +502,7 @@ export default class AccountViewView extends Vue {
apiServer = ""; apiServer = "";
apiServerInput = ""; apiServerInput = "";
derivationPath = ""; derivationPath = "";
downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
givenName = ""; givenName = "";
isRegistered = false; isRegistered = false;
isSubscribed = false; isSubscribed = false;
@ -494,6 +528,11 @@ export default class AccountViewView extends Vue {
warnIfProdServer = false; warnIfProdServer = false;
warnIfTestServer = false; warnIfTestServer = false;
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
/** /**
* Async function executed when the component is created. * Async function executed when the component is created.
* Initializes the component's state with values from the database, * Initializes the component's state with values from the database,
@ -530,6 +569,12 @@ export default class AccountViewView extends Vue {
} }
} }
beforeUnmount() {
if (this.downloadUrl) {
URL.revokeObjectURL(this.downloadUrl);
}
}
/** /**
* Initializes component state with values from the database or defaults. * Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database. * @param {SettingsType} settings - Object containing settings from the database.
@ -553,26 +598,17 @@ export default class AccountViewView extends Vue {
try { try {
// Open the accounts database // Open the accounts database
await accountsDB.open(); await accountsDB.open();
} catch (error) {
console.error("Failed to open accounts database:", error);
return null;
}
let account: { identity?: string } | undefined;
try {
// Search for the account with the matching DID (decentralized identifier) // Search for the account with the matching DID (decentralized identifier)
account = await accountsDB.accounts const account: { identity?: string } | undefined =
.where("did") await accountsDB.accounts.where("did").equals(activeDid).first();
.equals(activeDid)
.first(); // Return parsed identity or null if not found
return JSON.parse((account?.identity as string) || "null");
} catch (error) { } catch (error) {
console.error("Failed to find account:", error); console.error("Failed to find account:", error);
return null; return null;
} }
// Return parsed identity or null if not found
return JSON.parse((account?.identity as string) || "null");
} }
/** /**
@ -628,11 +664,6 @@ export default class AccountViewView extends Vue {
return timeStr.substring(0, timeStr.indexOf("T")); return timeStr.substring(0, timeStr.indexOf("T"));
} }
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
/** /**
* Processes the identity and updates the component's state. * Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information. * @param {IdentityType} identity - Object containing identity information.
@ -788,13 +819,13 @@ export default class AccountViewView extends Vue {
const blob = await this.generateDatabaseBlob(); const blob = await this.generateDatabaseBlob();
// Create a temporary URL for the blob // Create a temporary URL for the blob
const url = this.createBlobURL(blob); this.downloadUrl = this.createBlobURL(blob);
// Trigger the download // Trigger the download
this.downloadDatabaseBackup(url); this.downloadDatabaseBackup(this.downloadUrl);
// Revoke the temporary URL // Revoke the temporary URL -- not yet because of DuckDuckGo download failure
URL.revokeObjectURL(url); //URL.revokeObjectURL(this.downloadUrl);
// Notify the user that the download has started // Notify the user that the download has started
this.notifyDownloadStarted(); this.notifyDownloadStarted();
@ -831,7 +862,19 @@ export default class AccountViewView extends Vue {
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement; const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
downloadAnchor.href = url; downloadAnchor.href = url;
downloadAnchor.download = `${db.name}-backup.json`; downloadAnchor.download = `${db.name}-backup.json`;
downloadAnchor.click(); downloadAnchor.click(); // doesn't work for some browsers, eg. DuckDuckGo
}
public computedStartDownloadLinkClassNames() {
return {
invisible: this.downloadUrl,
};
}
public computedDownloadLinkClassNames() {
return {
invisible: !this.downloadUrl,
};
} }
/** /**
@ -867,6 +910,43 @@ export default class AccountViewView extends Vue {
console.error("Export Error:", error); console.error("Export Error:", error);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async uploadFile(event: any) {
inputFileNameRef.value = event.target.files[0];
}
showContactImport() {
return !!inputFileNameRef.value;
}
/**
* Asynchronously imports the database from a downloadable JSON file.
*
* @throws Will notify the user if there is an export error.
*/
async submitFile() {
if (inputFileNameRef.value != null) {
if (
confirm(
"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?",
)
) {
await db.delete();
await Dexie.import(inputFileNameRef.value, {
progressCallback: this.progressCallback,
});
}
}
}
private progressCallback(progress: ImportProgress) {
console.log(
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
);
return true;
}
async checkLimits() { async checkLimits() {
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
if (identity) { if (identity) {
@ -996,6 +1076,7 @@ export default class AccountViewView extends Vue {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = accounts[accountNum - 1]; const account = accounts[accountNum - 1];
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did }); await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });
this.updateActiveAccountProperties(account); this.updateActiveAccountProperties(account);

9
src/views/ContactsView.vue

@ -593,12 +593,19 @@ export default class ContactsView extends Vue {
}) })
.catch((err) => { .catch((err) => {
console.error("Error when adding contact to storage:", err); console.error("Error when adding contact to storage:", err);
let message = "An error prevented this import.";
if (
err.message?.indexOf("Key already exists in the object store.") > -1
) {
message =
"A contact with that DID is already in your contact list. Edit them directly below.";
}
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Contact Not Added", title: "Contact Not Added",
text: "An error prevented this import.", text: message,
}, },
-1, -1,
); );

Loading…
Cancel
Save