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

/** * 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>