forked from trent_larson/crowd-funder-for-time-pwa
Merge pull request 'make a backup download for browsers that don't get it automatically' (#101) from another-download into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#101
This commit is contained in:
@@ -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… <fa icon="spinner" class="fa-spin"></fa>
|
Checking… <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>
|
||||||
|
|
||||||
<router-link
|
<div class="grid-cols-2 mb-4">
|
||||||
:to="{ name: 'statistics' }"
|
<span class="text-slate-500 text-sm font-bold mb-2">Data Import</span>
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
<input type="file" @change="uploadFile" class="ml-2" />
|
||||||
>
|
<div v-if="showContactImport()">
|
||||||
See Animated Global History of Giving
|
<button
|
||||||
</router-link>
|
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
|
||||||
|
:to="{ name: 'statistics' }"
|
||||||
|
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
See Global Animated History of Giving
|
||||||
|
</router-link>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 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);
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user