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.
231 lines
7.4 KiB
231 lines
7.4 KiB
/** * Data Export Section Component * * Provides UI and functionality for
|
|
exporting user data and backing up identifier seeds. * Includes buttons for seed
|
|
backup and database export, with platform-specific download instructions. * *
|
|
Features: * - Platform-specific export handling (web vs. native) * - Proper
|
|
resource cleanup for blob URLs * - Robust error handling with user-friendly
|
|
messages * - Conditional UI based on platform capabilities * * @component *
|
|
@displayName DataExportSection * @example * ```vue *
|
|
<DataExportSection :active-did="currentDid" />
|
|
* ``` * * @author Matthew Raymer * @since 2025-01-25 * @version 1.1.0 */
|
|
|
|
<template>
|
|
<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
|
|
v-if="activeDid"
|
|
:to="{ name: 'seed-backup' }"
|
|
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
|
|
:class="{ hidden: isDownloadInProgress }"
|
|
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 Contacts
|
|
</button>
|
|
|
|
<!-- Hidden download link for web platform - always rendered for ref access -->
|
|
<a
|
|
v-if="isWebPlatform"
|
|
ref="downloadLink"
|
|
:href="downloadUrl"
|
|
:download="fileName"
|
|
:class="{ hidden: !downloadUrl }"
|
|
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 v-if="capabilities.needsFileHandlingInstructions" class="mt-4">
|
|
<p>
|
|
After the download, you can save the file in your preferred storage
|
|
location.
|
|
</p>
|
|
<ul>
|
|
<li v-if="capabilities.isIOS" class="list-disc list-outside ml-4">
|
|
On iOS: You will be prompted to choose a location to save your backup
|
|
file.
|
|
</li>
|
|
<li
|
|
v-if="capabilities.isMobile && !capabilities.isIOS"
|
|
class="list-disc list-outside ml-4"
|
|
>
|
|
On Android: You will be prompted to choose a location to save your
|
|
backup file.
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Prop, Vue } from "vue-facing-decorator";
|
|
import { AppString, NotificationIface } from "../constants/app";
|
|
|
|
import { logger } from "../utils/logger";
|
|
import { contactsToExportJson } from "../libs/util";
|
|
import { createNotifyHelpers } from "@/utils/notify";
|
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
|
|
|
/**
|
|
* @vue-component
|
|
* Data Export Section Component
|
|
* Handles database export and seed backup functionality with platform-specific behavior
|
|
*/
|
|
@Component({
|
|
mixins: [PlatformServiceMixin],
|
|
})
|
|
export default class DataExportSection extends Vue {
|
|
/**
|
|
* Notification function injected by Vue
|
|
* Used to show success/error messages to the user
|
|
*/
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
/**
|
|
* Active DID (Decentralized Identifier) of the user
|
|
* Controls visibility of seed backup option
|
|
* @required
|
|
*/
|
|
@Prop({ required: true }) readonly activeDid!: string;
|
|
|
|
/**
|
|
* URL for the database export download
|
|
* Created and revoked dynamically during export process
|
|
* Only used in web platform
|
|
*/
|
|
downloadUrl = "";
|
|
|
|
/**
|
|
* Notification helper for consistent notification patterns
|
|
* Created as a getter to ensure $notify is available when called
|
|
*/
|
|
get notify() {
|
|
return createNotifyHelpers(this.$notify);
|
|
}
|
|
|
|
/**
|
|
* NOTE: PlatformServiceMixin provides both concise helpers (e.g. $contacts, capabilities)
|
|
* and the full platformService instance for advanced/native features (e.g. writeAndShareFile).
|
|
* Always use 'this.platformService' as a property (never as a function).
|
|
*/
|
|
declare readonly platformService: import("@/services/PlatformService").PlatformService;
|
|
|
|
/**
|
|
* Computed property to check if we're on web platform
|
|
*/
|
|
private get isWebPlatform(): boolean {
|
|
return this.capabilities.hasFileDownload;
|
|
}
|
|
|
|
/**
|
|
* Computed property to check if download is in progress
|
|
*/
|
|
private get isDownloadInProgress(): boolean {
|
|
return Boolean(this.downloadUrl && this.isWebPlatform);
|
|
}
|
|
|
|
/**
|
|
* Computed property for the export file name
|
|
*/
|
|
private get fileName(): string {
|
|
return `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
|
|
}
|
|
|
|
/**
|
|
* Lifecycle hook to clean up resources
|
|
* Revokes object URL when component is unmounted (web platform only)
|
|
*/
|
|
beforeUnmount() {
|
|
if (this.downloadUrl && this.isWebPlatform) {
|
|
URL.revokeObjectURL(this.downloadUrl);
|
|
this.downloadUrl = "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exports the database to a JSON file
|
|
* Uses platform-specific methods for saving the exported data
|
|
* Shows success/error notifications to user
|
|
*
|
|
* @throws {Error} If export fails
|
|
*/
|
|
public async exportDatabase(): Promise<void> {
|
|
try {
|
|
// Fetch contacts from database using mixin's cached method
|
|
const allContacts = await this.$contacts();
|
|
|
|
// Convert contacts to export format
|
|
const exportData = contactsToExportJson(allContacts);
|
|
const jsonStr = JSON.stringify(exportData, null, 2);
|
|
const blob = new Blob([jsonStr], { type: "application/json" });
|
|
|
|
// Handle export based on platform capabilities
|
|
if (this.isWebPlatform) {
|
|
await this.handleWebExport(blob);
|
|
} else if (this.capabilities.hasFileSystem) {
|
|
await this.handleNativeExport(jsonStr);
|
|
} else {
|
|
throw new Error("This platform does not support file downloads.");
|
|
}
|
|
|
|
this.notify.success(
|
|
this.isWebPlatform
|
|
? "See your downloads directory for the backup."
|
|
: "The backup file has been saved.",
|
|
);
|
|
} catch (error) {
|
|
logger.error("Export Error:", error);
|
|
this.notify.error(
|
|
`There was an error exporting the data: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles export for web platform using download link
|
|
* @param blob The blob to download
|
|
*/
|
|
private async handleWebExport(blob: Blob): Promise<void> {
|
|
this.downloadUrl = URL.createObjectURL(blob);
|
|
|
|
try {
|
|
// Wait for next tick to ensure DOM is updated
|
|
await this.$nextTick();
|
|
|
|
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
|
if (!downloadAnchor) {
|
|
throw new Error("Download link element not found. Please try again.");
|
|
}
|
|
|
|
downloadAnchor.click();
|
|
|
|
// Clean up the URL after a delay
|
|
setTimeout(() => {
|
|
URL.revokeObjectURL(this.downloadUrl);
|
|
this.downloadUrl = "";
|
|
}, 1000);
|
|
} catch (error) {
|
|
// Clean up the URL on error
|
|
if (this.downloadUrl) {
|
|
URL.revokeObjectURL(this.downloadUrl);
|
|
this.downloadUrl = "";
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles export for native platforms using file system
|
|
* @param jsonStr The JSON string to save
|
|
*/
|
|
private async handleNativeExport(jsonStr: string): Promise<void> {
|
|
await this.platformService.writeAndShareFile(this.fileName, jsonStr);
|
|
}
|
|
}
|
|
</script>
|
|
|