diff --git a/src/app/shared/services/share/share.service.ts b/src/app/shared/services/share/share.service.ts index 2dca5edd8..cd8e78527 100644 --- a/src/app/shared/services/share/share.service.ts +++ b/src/app/shared/services/share/share.service.ts @@ -7,13 +7,22 @@ import { FileManagerService } from "../file-manager/file-manager.service"; import { TemplateAssetService } from "../../components/template/services/template-asset.service"; import { Capacitor } from "@capacitor/core"; -const SHARE_NOT_SUPPORTED_ON_PLATFORM_ERROR_MESSAGE = - "[SHARE] Sharing is not supported on this platform"; +interface IShareActionParams { + file?: string; + text?: string; + url?: string; + title?: string; + dialog_title?: string; +} + +type IShareParams = Omit & { dialogTitle?: string }; @Injectable({ providedIn: "root", }) export class ShareService extends SyncServiceBase { + /** Temporary local storage path on native devices for file being shared */ + private localFilepath: string; constructor( private errorHandler: ErrorHandlerService, private fileManagerService: FileManagerService, @@ -30,87 +39,127 @@ export class ShareService extends SyncServiceBase { private registerTemplateActionHandlers() { this.templateActionRegistry.register({ - share: async ({ args }) => { - const [actionId, ...shareArgs] = args; - const childActions = { - file: async () => await this.shareFile(shareArgs[0]), - text: () => this.share({ text: shareArgs[0] }), - url: () => this.share({ url: shareArgs[0] }), - }; - // To support deprecated "share" action (previously used to share text only), - // assume text is being shared if first arg is not an actionId - if (!(actionId in childActions)) { - return await this.share({ text: args[0] }); + share: async (action) => { + let { args, params } = action as { args: string[]; params: IShareActionParams }; + + // Handle legacy arg-based syntax, where action is called as `share: data_type: data` + if (args) { + console.warn("[SHARE] Deprecated action syntax. Use `share | data_type: data` instead."); + const [dataType, ...shareArgs] = args; + if (dataType && shareArgs?.[0]) { + params = { + [dataType]: shareArgs[0], + }; + } + } + + if (params) { + await this.handleShare(params); + } else { + return console.error("[SHARE] No params provided to `share` action"); } - return childActions[actionId](); }, }); } - async share(options: ShareOptions) { - const { value: canShare } = await Share.canShare(); - if (canShare) { - try { - const { activityType } = await Share.share(options); - console.log("[SHARE] Content shared to", activityType); - } catch (error) { - this.handleShareError(error); - } - } else console.error(SHARE_NOT_SUPPORTED_ON_PLATFORM_ERROR_MESSAGE); + private async handleShare(options: IShareActionParams) { + // Rename `dialog_title` to `dialogTitle` + const { dialog_title, ...shareOptions } = options; + let parsedOptions = { dialogTitle: dialog_title, ...shareOptions }; + + // Convert file reference to platform-relative shareable file data + if (parsedOptions?.file) { + const fileData = await this.getFileData(parsedOptions.file); + delete parsedOptions.file; + parsedOptions = { ...parsedOptions, ...fileData }; + } + await this.share(parsedOptions); } - async shareFile(relativePath: string) { - let localFilepath: string; + private async share(options: IShareParams) { try { - if (relativePath) { - await this.templateAssetService.ready(); - // On native platforms, try to share file using @capacitor/share - if (Capacitor.isNativePlatform()) { - const { value: canShare } = await Share.canShare(); - if (canShare) { - this.fileManagerService.ready(); - const blob = (await this.templateAssetService.fetchAsset(relativePath, "blob")) as Blob; - // @capacitor/share can only share files saved to "Cache" directory - ({ localFilepath } = await this.fileManagerService.saveFile({ - data: blob, - targetPath: relativePath, - directory: "Cache", - })); - if (localFilepath) { - const { activityType } = await Share.share({ url: localFilepath }); - console.log("[SHARE] Content shared to", activityType); - } - } else console.error(SHARE_NOT_SUPPORTED_ON_PLATFORM_ERROR_MESSAGE); - } - // On web platforms, try to share file using Web Share API - else { - if (navigator.canShare) { - const blob = (await this.templateAssetService.fetchAsset(relativePath, "blob")) as Blob; - const filename = relativePath.split("/").pop(); - const data = { files: [new File([blob], filename, { type: blob.type })] }; - if (navigator.canShare(data)) { - await navigator.share(data); - } else { - console.error("[SHARE] Unable to share file:", data); - } - } else { - console.error(SHARE_NOT_SUPPORTED_ON_PLATFORM_ERROR_MESSAGE); - } - } + if (Capacitor.isNativePlatform()) { + await this.shareNative(options); + } + // Capacitor's Share API does not support sharing files on web, so use Web Share API directly + else { + await this.shareWeb(options); } } catch (error) { this.handleShareError(error); } finally { - // If a temporary file was saved for sharing, delete it - if (localFilepath) { - this.fileManagerService.deleteFile(localFilepath); + this.cleanupLocalFile(); + } + } + + private async shareNative(options: ShareOptions) { + const { value: canShare } = await Share.canShare(); + if (canShare) { + const { activityType } = await Share.share(options); + console.log("[SHARE] Content shared to", activityType); + } else { + console.error("[SHARE] Sharing is not supported on this platform"); + } + } + + private async shareWeb(options: ShareData) { + if (navigator.canShare(options)) { + await navigator.share(options); + } else { + console.error("[SHARE] Unable to share this data on this platform,", options); + } + } + + /** + * Fetch the requested file and format file data for sharing, appropriate to platform + * - On Web platforms, the file is shared directly as a blob + * - On Native platforms, file is temporarily saved to app's internal cache, and a local URL is shared + */ + private async getFileData(relativePath: string) { + if (!relativePath) return {}; + + let shareAbleFileData: { url?: string; files?: File[] } = {}; + + await this.templateAssetService.ready(); + + // On native platforms, temporarily save file locally in order to share URL + if (Capacitor.isNativePlatform()) { + this.fileManagerService.ready(); + const blob = (await this.templateAssetService.fetchAsset(relativePath, "blob")) as Blob; + const saveFileResponse = await this.fileManagerService.saveFile({ + data: blob, + targetPath: relativePath, + // @capacitor/share can only share files saved to "Cache" directory + directory: "Cache", + }); + this.localFilepath = saveFileResponse.localFilepath; + if (this.localFilepath) { + shareAbleFileData = { url: this.localFilepath }; } } + // On web platforms, format the data for the Web Share API + else { + const blob = (await this.templateAssetService.fetchAsset(relativePath, "blob")) as Blob; + const filename = relativePath.split("/").pop(); + shareAbleFileData = { files: [new File([blob], filename, { type: blob.type })] }; + } + return shareAbleFileData; + } + + /** + * If a local file was saved temporarily for sharing, delete it + * */ + private async cleanupLocalFile() { + if (this.localFilepath) { + await this.fileManagerService.deleteFile(this.localFilepath); + this.localFilepath = null; + } } private handleShareError(error: Error) { - const cancellationMessages = ["Abort due to cancellation of share.", "Share canceled"]; - if (cancellationMessages.includes(error.message)) { + // Handle known errors resulting from user cancelling share + const CANCELLATION_MESSAGES = ["Abort due to cancellation of share.", "Share canceled"]; + if (CANCELLATION_MESSAGES.includes(error.message)) { console.warn("[SHARE] Share cancelled by user"); } else { this.errorHandler.handleError(error);