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.
 
 
 
 
 
 

220 lines
7.1 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="containerClasses">
<div :class="titleClasses">Data Export</div>
<router-link
v-if="activeDid"
:to="{ name: 'seed-backup' }"
:class="backupButtonClasses"
>
Backup Identifier Seed
</router-link>
<button
:disabled="isExporting"
:class="exportButtonClasses"
@click="exportDatabase()"
>
{{ isExporting ? "Exporting..." : "Download Contacts" }}
</button>
<div
v-if="capabilities.needsFileHandlingInstructions"
:class="instructionsContainerClasses"
>
<p>
After the export, you can save the file in your preferred storage
location.
</p>
<ul>
<li v-if="capabilities.isIOS" :class="listItemClasses">
On iOS: You will be prompted to choose a location to save your backup
file.
</li>
<li
v-if="capabilities.isMobile && !capabilities.isIOS"
:class="listItemClasses"
>
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 * as R from "ramda";
import { AppString, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
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
*
* Features:
* - Automatic date stamping of backup files (YYYY-MM-DD format)
* - Platform-specific export handling with proper abstraction
* - Robust error handling and user notifications
* - Template streamlined with computed CSS properties
*/
@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;
/**
* Flag indicating if export is currently in progress
* Used to show loading state and prevent multiple simultaneous exports
*/
isExporting = false;
/**
* Notification helper for consistent notification patterns
* Created as a getter to ensure $notify is available when called
*/
notify!: ReturnType<typeof createNotifyHelpers>;
/**
* 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;
/**
* CSS classes for the main container
*/
get containerClasses(): string {
return "bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8";
}
/**
* CSS classes for the section title
*/
get titleClasses(): string {
return "mb-2 font-bold";
}
/**
* CSS classes for the backup button (router link)
*/
get backupButtonClasses(): string {
return "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";
}
/**
* CSS classes for the export button
*/
get exportButtonClasses(): string {
return "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 disabled:opacity-50 disabled:cursor-not-allowed";
}
/**
* CSS classes for the instructions container
*/
get instructionsContainerClasses(): string {
return "mt-4";
}
/**
* CSS classes for list items in instructions
*/
get listItemClasses(): string {
return "list-disc list-outside ml-4";
}
/**
* Computed property for the export file name
* Includes today's date for easy identification of backup files
*/
private get fileName(): string {
const today = new Date();
const dateString = today.toISOString().split("T")[0]; // YYYY-MM-DD format
return `${AppString.APP_NAME_NO_SPACES}-backup-contacts-${dateString}.json`;
}
/**
* Exports the database to a JSON file
* Uses the platform service to handle platform-specific export logic
* Shows success/error notifications to user
*
* @throws {Error} If export fails
*/
public async exportDatabase(): Promise<void> {
if (this.isExporting) {
return; // Prevent multiple simultaneous exports
}
try {
this.isExporting = true;
// Fetch contacts from database using mixin's cached method
const allContacts = await this.$contacts();
// Convert contacts to export format
const processedContacts: Contact[] = allContacts.map((contact) => {
// first remove the contactMethods field, mostly to cast to a clear type (that will end up with JSON objects)
const exContact: Contact = R.omit(["contactMethods"], contact);
// now add contactMethods as a true array of ContactMethod objects
exContact.contactMethods = contact.contactMethods
? (typeof contact.contactMethods === 'string' && contact.contactMethods.trim() !== ''
? JSON.parse(contact.contactMethods)
: [])
: [];
return exContact;
});
const exportData = contactsToExportJson(processedContacts);
const jsonStr = JSON.stringify(exportData, null, 2);
// Use platform service to handle export (no platform-specific logic here!)
await this.platformService.writeAndShareFile(this.fileName, jsonStr);
this.notify.success(
"Contact export completed successfully. Check your downloads or share dialog.",
);
} catch (error) {
logger.error("Export Error:", error);
this.notify.error(
`There was an error exporting the data: ${error instanceof Error ? error.message : "Unknown error"}`,
);
} finally {
this.isExporting = false;
}
}
created() {
this.notify = createNotifyHelpers(this.$notify);
}
}
</script>