From c18a6b334fba7f01cb895497dc3a586bdfb3f8d2 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 28 Jul 2025 15:58:57 +0800 Subject: [PATCH] Added: native clipboard service - Install @capacitor/clipboard@^6.0.0 for native clipboard support - Create platform-agnostic ClipboardService with iOS/Android native API --- android/app/capacitor.build.gradle | 1 + .../src/main/assets/capacitor.plugins.json | 4 + android/capacitor.settings.gradle | 3 + ios/App/Podfile | 1 + ios/App/Podfile.lock | 8 +- package-lock.json | 9 + package.json | 1 + src/services/ClipboardService.ts | 185 ++++++++++++++++++ 8 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 src/services/ClipboardService.ts diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index ec740be6..e1e9924e 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -13,6 +13,7 @@ dependencies { implementation project(':capacitor-mlkit-barcode-scanning') implementation project(':capacitor-app') implementation project(':capacitor-camera') + implementation project(':capacitor-clipboard') implementation project(':capacitor-filesystem') implementation project(':capacitor-share') implementation project(':capawesome-capacitor-file-picker') diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json index a95bd42f..6f389366 100644 --- a/android/app/src/main/assets/capacitor.plugins.json +++ b/android/app/src/main/assets/capacitor.plugins.json @@ -15,6 +15,10 @@ "pkg": "@capacitor/camera", "classpath": "com.capacitorjs.plugins.camera.CameraPlugin" }, + { + "pkg": "@capacitor/clipboard", + "classpath": "com.capacitorjs.plugins.clipboard.ClipboardPlugin" + }, { "pkg": "@capacitor/filesystem", "classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin" diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 3c06dfe7..188fdd40 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -14,6 +14,9 @@ project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/ include ':capacitor-camera' project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android') +include ':capacitor-clipboard' +project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android') + include ':capacitor-filesystem' project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android') diff --git a/ios/App/Podfile b/ios/App/Podfile index da98dfe6..5c94c24a 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -15,6 +15,7 @@ def capacitor_pods pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera' + pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard' pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem' pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker' diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index fdd82e86..eb9dd861 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -5,6 +5,8 @@ PODS: - Capacitor - CapacitorCamera (6.1.2): - Capacitor + - CapacitorClipboard (6.0.2): + - Capacitor - CapacitorCommunitySqlite (6.0.2): - Capacitor - SQLCipher @@ -88,6 +90,7 @@ DEPENDENCIES: - "Capacitor (from `../../node_modules/@capacitor/ios`)" - "CapacitorApp (from `../../node_modules/@capacitor/app`)" - "CapacitorCamera (from `../../node_modules/@capacitor/camera`)" + - "CapacitorClipboard (from `../../node_modules/@capacitor/clipboard`)" - "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)" - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" - "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)" @@ -119,6 +122,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/app" CapacitorCamera: :path: "../../node_modules/@capacitor/camera" + CapacitorClipboard: + :path: "../../node_modules/@capacitor/clipboard" CapacitorCommunitySqlite: :path: "../../node_modules/@capacitor-community/sqlite" CapacitorCordova: @@ -136,6 +141,7 @@ SPEC CHECKSUMS: Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7 CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79 + CapacitorClipboard: 4443c3cdb7c77b1533dfe3ff0f9f7756aa8579df CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74 @@ -157,6 +163,6 @@ SPEC CHECKSUMS: SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c -PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924 +PODFILE CHECKSUM: 60f54b19c5a7a07343ab5ba9e5db49019fd86aa0 COCOAPODS: 1.16.2 diff --git a/package-lock.json b/package-lock.json index 6913cede..636af6ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@capacitor/app": "^6.0.0", "@capacitor/camera": "^6.0.0", "@capacitor/cli": "^6.2.0", + "@capacitor/clipboard": "^6.0.2", "@capacitor/core": "^6.2.0", "@capacitor/filesystem": "^6.0.0", "@capacitor/ios": "^6.2.0", @@ -2157,6 +2158,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@capacitor/clipboard": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/clipboard/-/clipboard-6.0.2.tgz", + "integrity": "sha512-jQ6UeFra5NP58THNZNb7HtzOZU7cHsjgrbQGVuMTgsK1uTILZpNeh+pfqHbKggba6KaNh5DAsJvEVQGpIR1VBA==", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@capacitor/core": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz", diff --git a/package.json b/package.json index 97c49f85..a39acd3f 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "@capacitor/app": "^6.0.0", "@capacitor/camera": "^6.0.0", "@capacitor/cli": "^6.2.0", + "@capacitor/clipboard": "^6.0.2", "@capacitor/core": "^6.2.0", "@capacitor/filesystem": "^6.0.0", "@capacitor/ios": "^6.2.0", diff --git a/src/services/ClipboardService.ts b/src/services/ClipboardService.ts new file mode 100644 index 00000000..3ab9de19 --- /dev/null +++ b/src/services/ClipboardService.ts @@ -0,0 +1,185 @@ +import { Capacitor } from "@capacitor/core"; +import { Clipboard } from "@capacitor/clipboard"; +import { useClipboard } from "@vueuse/core"; +import { logger } from "@/utils/logger"; + +/** + * Platform-agnostic clipboard service that handles both web and native platforms + * Provides reliable clipboard functionality across all platforms including iOS + */ +export class ClipboardService { + private static instance: ClipboardService | null = null; + + /** + * Get singleton instance of ClipboardService + */ + public static getInstance(): ClipboardService { + if (!ClipboardService.instance) { + ClipboardService.instance = new ClipboardService(); + } + return ClipboardService.instance; + } + + /** + * Copy text to clipboard with platform-specific handling + * + * @param text - The text to copy to clipboard + * @returns Promise that resolves when copy is complete + * @throws Error if copy operation fails + */ + public async copyToClipboard(text: string): Promise { + const platform = Capacitor.getPlatform(); + const isNative = Capacitor.isNativePlatform(); + + logger.log("[ClipboardService] Copying to clipboard:", { + text: text.substring(0, 50) + (text.length > 50 ? "..." : ""), + platform, + isNative, + timestamp: new Date().toISOString(), + }); + + try { + if (isNative && (platform === "ios" || platform === "android")) { + // Use native Capacitor clipboard for mobile platforms + await this.copyNative(text); + } else { + // Use web clipboard API for web/desktop platforms + await this.copyWeb(text); + } + + logger.log("[ClipboardService] Copy successful", { + platform, + timestamp: new Date().toISOString(), + }); + } catch (error) { + logger.error("[ClipboardService] Copy failed:", { + error: error instanceof Error ? error.message : String(error), + platform, + timestamp: new Date().toISOString(), + }); + throw error; + } + } + + /** + * Copy text using native Capacitor clipboard API + * + * @param text - The text to copy + * @returns Promise that resolves when copy is complete + */ + private async copyNative(text: string): Promise { + try { + await Clipboard.write({ + string: text, + }); + } catch (error) { + logger.error("[ClipboardService] Native copy failed:", { + error: error instanceof Error ? error.message : String(error), + timestamp: new Date().toISOString(), + }); + throw new Error( + `Native clipboard copy failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + /** + * Copy text using web clipboard API with fallback + * + * @param text - The text to copy + * @returns Promise that resolves when copy is complete + */ + private async copyWeb(text: string): Promise { + try { + // Try VueUse clipboard first (handles some edge cases) + const { copy } = useClipboard(); + await copy(text); + } catch (error) { + logger.warn( + "[ClipboardService] VueUse clipboard failed, trying native API:", + { + error: error instanceof Error ? error.message : String(error), + timestamp: new Date().toISOString(), + }, + ); + + // Fallback to native navigator.clipboard + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text); + } else { + throw new Error("Clipboard API not supported in this browser"); + } + } + } + + /** + * Read text from clipboard (platform-specific) + * + * @returns Promise that resolves to the clipboard text + * @throws Error if read operation fails + */ + public async readFromClipboard(): Promise { + const platform = Capacitor.getPlatform(); + const isNative = Capacitor.isNativePlatform(); + + try { + if (isNative && (platform === "ios" || platform === "android")) { + // Use native Capacitor clipboard for mobile platforms + const result = await Clipboard.read(); + return result.value || ""; + } else { + // Use web clipboard API for web/desktop platforms + if (navigator.clipboard && navigator.clipboard.readText) { + return await navigator.clipboard.readText(); + } else { + throw new Error("Clipboard read API not supported in this browser"); + } + } + } catch (error) { + logger.error("[ClipboardService] Read from clipboard failed:", { + error: error instanceof Error ? error.message : String(error), + platform, + timestamp: new Date().toISOString(), + }); + throw error; + } + } + + /** + * Check if clipboard is supported on current platform + * + * @returns boolean indicating if clipboard is supported + */ + public isSupported(): boolean { + const platform = Capacitor.getPlatform(); + const isNative = Capacitor.isNativePlatform(); + + if (isNative && (platform === "ios" || platform === "android")) { + return true; // Capacitor clipboard should work on native platforms + } + + // Check web clipboard support + return !!(navigator.clipboard && navigator.clipboard.writeText); + } +} + +/** + * Convenience function to copy text to clipboard + * Uses the singleton ClipboardService instance + * + * @param text - The text to copy to clipboard + * @returns Promise that resolves when copy is complete + */ +export async function copyToClipboard(text: string): Promise { + return ClipboardService.getInstance().copyToClipboard(text); +} + +/** + * Convenience function to read text from clipboard + * Uses the singleton ClipboardService instance + * + * @returns Promise that resolves to the clipboard text + */ +export async function readFromClipboard(): Promise { + return ClipboardService.getInstance().readFromClipboard(); +}