From 5fc5b958afe1287c34f4ac768cd1f80af9347881 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Wed, 10 Sep 2025 21:04:36 +0800 Subject: [PATCH 1/2] fix(ios): resolve clipboard and notification issues in ContactQRScanFullView - Replace useClipboard() with ClipboardService for iOS compatibility - Fix notification helper initialization timing issue - Add proper error handling for clipboard operations - Ensure consistent behavior across all platforms Fixes clipboard copy functionality on iOS builds where QR code clicks failed to copy content and showed notification errors. The ClipboardService provides platform-specific handling using Capacitor's clipboard plugin, while moving notification initialization to created() lifecycle hook prevents undefined function errors. Resolves: iOS clipboard copy failure and notification system errors --- src/views/ContactQRScanFullView.vue | 69 ++++++++++++++++++----------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/src/views/ContactQRScanFullView.vue b/src/views/ContactQRScanFullView.vue index 42358b65..2664c03d 100644 --- a/src/views/ContactQRScanFullView.vue +++ b/src/views/ContactQRScanFullView.vue @@ -104,7 +104,6 @@ import { Buffer } from "buffer/"; import QRCodeVue3 from "qr-code-generator-vue3"; import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; -import { useClipboard } from "@vueuse/core"; import { logger } from "../utils/logger"; import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory"; @@ -196,7 +195,7 @@ export default class ContactQRScanFull extends Vue { $router!: Router; // Notification helper system - private notify = createNotifyHelpers(this.$notify); + private notify!: ReturnType; isScanning = false; error: string | null = null; @@ -264,6 +263,9 @@ export default class ContactQRScanFull extends Vue { * Loads user settings and generates QR code for contact sharing */ async created() { + // Initialize notification helper system + this.notify = createNotifyHelpers(this.$notify); + try { const settings = await this.$accountSettings(); this.activeDid = settings.activeDid || ""; @@ -646,36 +648,53 @@ export default class ContactQRScanFull extends Vue { * Copies contact URL to clipboard for sharing */ async onCopyUrlToClipboard() { - const account = (await libsUtil.retrieveFullyDecryptedAccount( - this.activeDid, - )) as Account; - const jwtUrl = await generateEndorserJwtUrlForAccount( - account, - this.isRegistered, - this.givenName, - this.profileImageUrl, - true, - ); - useClipboard() - .copy(jwtUrl) - .then(() => { - this.notify.toast( - NOTIFY_QR_URL_COPIED.title, - NOTIFY_QR_URL_COPIED.message, - QR_TIMEOUT_MEDIUM, - ); + try { + const account = (await libsUtil.retrieveFullyDecryptedAccount( + this.activeDid, + )) as Account; + const jwtUrl = await generateEndorserJwtUrlForAccount( + account, + this.isRegistered, + this.givenName, + this.profileImageUrl, + true, + ); + + // Use the platform-specific ClipboardService for reliable iOS support + const { copyToClipboard } = await import("../services/ClipboardService"); + await copyToClipboard(jwtUrl); + + this.notify.toast( + NOTIFY_QR_URL_COPIED.title, + NOTIFY_QR_URL_COPIED.message, + QR_TIMEOUT_MEDIUM, + ); + } catch (error) { + logger.error("Error copying URL to clipboard:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, }); + this.notify.error("Failed to copy URL to clipboard."); + } } /** * Copies DID to clipboard for manual sharing */ - onCopyDidToClipboard() { - useClipboard() - .copy(this.activeDid) - .then(() => { - this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG); + async onCopyDidToClipboard() { + try { + // Use the platform-specific ClipboardService for reliable iOS support + const { copyToClipboard } = await import("../services/ClipboardService"); + await copyToClipboard(this.activeDid); + + this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG); + } catch (error) { + logger.error("Error copying DID to clipboard:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, }); + this.notify.error("Failed to copy DID to clipboard."); + } } /** From 4c218c47866d6f9c426fb4098f73dfc6672d44f6 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Fri, 12 Sep 2025 14:33:09 +0800 Subject: [PATCH 2/2] feat: migrate all clipboard operations from useClipboard to ClipboardService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace useClipboard with platform-agnostic ClipboardService across 13 files - Add proper error handling with user notifications for all clipboard operations - Fix naming conflicts between method names and imported function names - Ensure consistent async/await patterns throughout the codebase - Add notification system to HelpView.vue for user feedback on clipboard errors - Remove unnecessary wrapper methods for cleaner code Files migrated: - View components: UserProfileView, QuickActionBvcEndView, ProjectViewView, InviteOneView, SeedBackupView, HelpView, AccountViewView, DatabaseMigration, ConfirmGiftView, ClaimView, OnboardMeetingSetupView - Utility functions: libs/util.ts (doCopyTwoSecRedo) - Components: HiddenDidDialog Naming conflicts resolved: - DatabaseMigration: copyToClipboard() → copyExportedDataToClipboard() - ShareMyContactInfoView: copyToClipboard() → copyContactMessageToClipboard() → removed - HiddenDidDialog: copyToClipboard() → copyTextToClipboard() - ClaimView: copyToClipboard() → copyTextToClipboard() - ConfirmGiftView: copyToClipboard() → copyTextToClipboard() This migration ensures reliable clipboard functionality across iOS, Android, and web platforms with proper error handling and user feedback. Closes: Platform-specific clipboard issues on mobile devices --- src/components/HiddenDidDialog.vue | 29 ++++++++++-------- src/libs/util.ts | 18 +++++++---- src/views/AccountViewView.vue | 14 +++++---- src/views/ClaimView.vue | 31 +++++++++++-------- src/views/ConfirmGiftView.vue | 43 ++++++++++++++++----------- src/views/ContactQRScanFullView.vue | 3 +- src/views/ContactQRScanShowView.vue | 3 +- src/views/DatabaseMigration.vue | 12 +++----- src/views/HelpView.vue | 26 ++++++++++++---- src/views/InviteOneView.vue | 30 ++++++++++++------- src/views/OnboardMeetingSetupView.vue | 19 +++++++----- src/views/ProjectViewView.vue | 16 +++++----- src/views/QuickActionBvcEndView.vue | 32 ++++++++++---------- src/views/SeedBackupView.vue | 14 +++++---- src/views/ShareMyContactInfoView.vue | 11 ++----- src/views/UserProfileView.vue | 16 +++++----- 16 files changed, 186 insertions(+), 131 deletions(-) diff --git a/src/components/HiddenDidDialog.vue b/src/components/HiddenDidDialog.vue index dfbc352d..e48c1b27 100644 --- a/src/components/HiddenDidDialog.vue +++ b/src/components/HiddenDidDialog.vue @@ -74,7 +74,7 @@ If you'd like an introduction, click here to copy this page, paste it into a message, and ask if they'll tell you more about the {{ roleName }}. @@ -110,7 +110,7 @@ * @since 2024-12-19 */ import { Component, Vue } from "vue-facing-decorator"; -import { useClipboard } from "@vueuse/core"; +import { copyToClipboard } from "../services/ClipboardService"; import * as R from "ramda"; import * as serverUtil from "../libs/endorserServer"; import { Contact } from "../db/tables/contacts"; @@ -197,19 +197,24 @@ export default class HiddenDidDialog extends Vue { ); } - copyToClipboard(name: string, text: string) { - useClipboard() - .copy(text) - .then(() => { - this.notify.success( - NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"), - TIMEOUTS.SHORT, - ); - }); + async copyTextToClipboard(name: string, text: string) { + try { + await copyToClipboard(text); + this.notify.success( + NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"), + TIMEOUTS.SHORT, + ); + } catch (error) { + this.$logAndConsole( + `Error copying ${name || "content"} to clipboard: ${error}`, + true, + ); + this.notify.error(`Failed to copy ${name || "content"} to clipboard.`); + } } onClickShareClaim() { - this.copyToClipboard("A link to this page", this.deepLinkUrl); + this.copyTextToClipboard("A link to this page", this.deepLinkUrl); window.navigator.share({ title: "Help Connect Me", text: "I'm trying to find the people who recorded this. Can you help me?", diff --git a/src/libs/util.ts b/src/libs/util.ts index c64916cc..d5f720d7 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -3,7 +3,7 @@ import axios, { AxiosResponse } from "axios"; import { Buffer } from "buffer"; import * as R from "ramda"; -import { useClipboard } from "@vueuse/core"; +import { copyToClipboard } from "../services/ClipboardService"; import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app"; import { Account, AccountEncrypted } from "../db/tables/accounts"; @@ -232,11 +232,19 @@ export const nameForContact = ( ); }; -export const doCopyTwoSecRedo = (text: string, fn: () => void) => { +export const doCopyTwoSecRedo = async ( + text: string, + fn: () => void, +): Promise => { fn(); - useClipboard() - .copy(text) - .then(() => setTimeout(fn, 2000)); + try { + await copyToClipboard(text); + setTimeout(fn, 2000); + } catch (error) { + // Note: This utility function doesn't have access to notification system + // The calling component should handle error notifications + // Error is silently caught to avoid breaking the 2-second redo pattern + } }; export interface ConfirmerData { diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 19872da6..9f23a955 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -764,7 +764,7 @@ import { IIdentifier } from "@veramo/core"; import { ref } from "vue"; import { Component, Vue } from "vue-facing-decorator"; import { RouteLocationNormalizedLoaded, Router } from "vue-router"; -import { useClipboard } from "@vueuse/core"; +import { copyToClipboard } from "../services/ClipboardService"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { Capacitor } from "@capacitor/core"; @@ -1084,11 +1084,15 @@ export default class AccountViewView extends Vue { } // call fn, copy text to the clipboard, then redo fn after 2 seconds - doCopyTwoSecRedo(text: string, fn: () => void): void { + async doCopyTwoSecRedo(text: string, fn: () => void): Promise { fn(); - useClipboard() - .copy(text) - .then(() => setTimeout(fn, 2000)); + try { + await copyToClipboard(text); + setTimeout(fn, 2000); + } catch (error) { + this.$logAndConsole(`Error copying to clipboard: ${error}`, true); + this.notify.error("Failed to copy to clipboard."); + } } async toggleShowContactAmounts(): Promise { diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 2c441687..77b8ed75 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -58,7 +58,7 @@ title="Copy Printable Certificate Link" aria-label="Copy printable certificate link" @click=" - copyToClipboard( + copyTextToClipboard( 'A link to the certificate page', `${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`, ) @@ -72,7 +72,9 @@ @@ -399,7 +401,7 @@ contacts can see more details: click to copy this page info and see if they can make an introduction. Someone is connected to @@ -422,7 +424,7 @@ If you'd like an introduction, share this page with them and ask if they'll tell you more about about the participants. @@ -532,7 +534,7 @@ import * as yaml from "js-yaml"; import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator"; import { Router, RouteLocationNormalizedLoaded } from "vue-router"; -import { useClipboard } from "@vueuse/core"; +import { copyToClipboard } from "../services/ClipboardService"; import { GenericVerifiableCredential } from "../interfaces"; import GiftedDialog from "../components/GiftedDialog.vue"; import QuickNav from "../components/QuickNav.vue"; @@ -1129,16 +1131,21 @@ export default class ClaimView extends Vue { ); } - copyToClipboard(name: string, text: string) { - useClipboard() - .copy(text) - .then(() => { - this.notify.copied(name || "That"); - }); + async copyTextToClipboard(name: string, text: string) { + try { + await copyToClipboard(text); + this.notify.copied(name || "That"); + } catch (error) { + this.$logAndConsole( + `Error copying ${name || "content"} to clipboard: ${error}`, + true, + ); + this.notify.error(`Failed to copy ${name || "content"} to clipboard.`); + } } onClickShareClaim() { - this.copyToClipboard("A link to this page", this.windowDeepLink); + this.copyTextToClipboard("A link to this page", this.windowDeepLink); window.navigator.share({ title: "Help Connect Me", text: "I'm trying to find the people who recorded this. Can you help me?", diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index 95632bb7..4369f04d 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -192,7 +192,7 @@