Migrate HelpView.vue to PlatformServiceMixin - extract 7 inline handlers to methods

- Replace databaseUtil calls with PlatformServiceMixin for settings operations
- Extract toggleAlpha/Group/Community/Verifiable/Governance/Basics methods
- Add copyBitcoinAddress method with clipboard feedback
- Enhance onboarding reset with error handling and logging
- Human tested: all help sections, clipboard ops, platform navigation work
- 6 minutes (3x faster than estimate), technically compliant
This commit is contained in:
Matthew Raymer
2025-07-09 04:56:31 +00:00
parent d8e7fc90e5
commit 80f8f6e9a1
7 changed files with 626 additions and 65 deletions

View File

@@ -1332,7 +1332,8 @@ export const NOTIFY_QR_DID_COPIED = {
// Used in: ContactQRScanShowView.vue (onScanDetect method - invalid QR code)
export const NOTIFY_QR_INVALID_QR_CODE = {
title: "Invalid QR Code",
message: "This QR code does not contain valid contact information. Scan a TimeSafari contact QR code.",
message:
"This QR code does not contain valid contact information. Scan a TimeSafari contact QR code.",
};
// Used in: ContactQRScanShowView.vue (onScanDetect method - invalid contact info)
@@ -1350,7 +1351,8 @@ export const NOTIFY_QR_MISSING_DID = {
// Used in: ContactQRScanShowView.vue (onScanDetect method - unknown contact type)
export const NOTIFY_QR_UNKNOWN_CONTACT_TYPE = {
title: "Error",
message: "Could not determine the type of contact info. Try again, or tap the QR code to copy it and send it to them.",
message:
"Could not determine the type of contact info. Try again, or tap the QR code to copy it and send it to them.",
};
// Used in: ContactQRScanShowView.vue (onScanDetect method - processing error)
@@ -1376,7 +1378,7 @@ export function createQRRegistrationSuccessMessage(
}
// ContactQRScanShowView.vue timeout constants
export const QR_TIMEOUT_SHORT = 1000; // Short operations like registration submission
export const QR_TIMEOUT_MEDIUM = 2000; // Medium operations like URL copy
export const QR_TIMEOUT_STANDARD = 3000; // Standard success messages
export const QR_TIMEOUT_LONG = 5000; // Error messages and warnings
export const QR_TIMEOUT_SHORT = 1000; // Short operations like registration submission
export const QR_TIMEOUT_MEDIUM = 2000; // Medium operations like URL copy
export const QR_TIMEOUT_STANDARD = 3000; // Standard success messages
export const QR_TIMEOUT_LONG = 5000; // Error messages and warnings

View File

@@ -23,17 +23,11 @@
</h1>
</div>
<div
v-if="!givenName"
:class="nameWarningClasses"
>
<div v-if="!givenName" :class="nameWarningClasses">
<p class="mb-2">
<b>Note:</b> your identity currently does <b>not</b> include a name.
</p>
<button
:class="setNameButtonClasses"
@click="openUserNameDialog"
>
<button :class="setNameButtonClasses" @click="openUserNameDialog">
Set Your Name
</button>
</div>
@@ -79,14 +73,9 @@
</div>
<div class="text-center mt-6">
<div
v-if="isScanning"
:class="scannerContainerClasses"
>
<div v-if="isScanning" :class="scannerContainerClasses">
<!-- Status Message -->
<div
:class="statusMessageClasses"
>
<div :class="statusMessageClasses">
<div
v-if="cameraState === 'initializing'"
class="flex items-center justify-center space-x-2"
@@ -125,9 +114,7 @@
</p>
<p v-else-if="error" class="text-red-400">Error: {{ error }}</p>
<p v-else class="flex items-center justify-center space-x-2">
<span
:class="cameraStatusIndicatorClasses"
></span>
<span :class="cameraStatusIndicatorClasses"></span>
<span>{{ cameraStateMessage || "Ready to scan" }}</span>
</p>
</div>
@@ -290,14 +277,14 @@ export default class ContactQRScanShow extends Vue {
get cameraStatusIndicatorClasses(): Record<string, boolean> {
return {
'inline-block w-2 h-2 rounded-full': true,
'bg-green-500': this.cameraState === 'ready',
'bg-yellow-500': this.cameraState === 'in_use',
'bg-red-500':
this.cameraState === 'error' ||
this.cameraState === 'permission_denied' ||
this.cameraState === 'not_found',
'bg-blue-500': this.cameraState === 'off',
"inline-block w-2 h-2 rounded-full": true,
"bg-green-500": this.cameraState === "ready",
"bg-yellow-500": this.cameraState === "in_use",
"bg-red-500":
this.cameraState === "error" ||
this.cameraState === "permission_denied" ||
this.cameraState === "not_found",
"bg-blue-500": this.cameraState === "off",
};
}
@@ -365,12 +352,18 @@ export default class ContactQRScanShow extends Vue {
case "in_use":
this.error = "Camera is in use by another application";
this.isScanning = false;
this.notify.warning(NOTIFY_QR_CAMERA_IN_USE.message, QR_TIMEOUT_LONG);
this.notify.warning(
NOTIFY_QR_CAMERA_IN_USE.message,
QR_TIMEOUT_LONG,
);
break;
case "permission_denied":
this.error = "Camera permission denied";
this.isScanning = false;
this.notify.warning(NOTIFY_QR_CAMERA_ACCESS_REQUIRED.message, QR_TIMEOUT_LONG);
this.notify.warning(
NOTIFY_QR_CAMERA_ACCESS_REQUIRED.message,
QR_TIMEOUT_LONG,
);
break;
case "not_found":
this.error = "No camera found";
@@ -529,7 +522,7 @@ export default class ContactQRScanShow extends Vue {
this.notify.error(
error instanceof Error
? error.message
: NOTIFY_QR_PROCESSING_ERROR.message
: NOTIFY_QR_PROCESSING_ERROR.message,
);
}
}
@@ -554,7 +547,10 @@ export default class ContactQRScanShow extends Vue {
did: contact.did,
name: contact.name,
});
this.notify.toast(NOTIFY_QR_REGISTRATION_SUBMITTED.message, QR_TIMEOUT_SHORT);
this.notify.toast(
NOTIFY_QR_REGISTRATION_SUBMITTED.message,
QR_TIMEOUT_SHORT,
);
try {
const regResult = await register(
@@ -719,7 +715,10 @@ export default class ContactQRScanShow extends Vue {
contact.seesMe = true;
}
this.notify.success(createQRContactAddedMessage(!!this.activeDid), QR_TIMEOUT_STANDARD);
this.notify.success(
createQRContactAddedMessage(!!this.activeDid),
QR_TIMEOUT_STANDARD,
);
if (
this.isRegistered &&

View File

@@ -63,7 +63,7 @@
<h2 class="text-xl font-semibold">I want to know more because...</h2>
<ul class="list-disc list-outside ml-4">
<li class="p-2">
<div class="text-blue-500" @click="showAlpha = !showAlpha">... I'm a member of Alpha chat.</div>
<div class="text-blue-500" @click="toggleAlpha">... I'm a member of Alpha chat.</div>
<div v-if="showAlpha">
<p>
This is a project for public benefit. You are invited to add your gratitude
@@ -100,7 +100,7 @@
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="showGroup = !showGroup">... I want to find a group I'll enjoy working with.</div>
<div class="text-blue-500" @click="toggleGroup">... I want to find a group I'll enjoy working with.</div>
<div v-if="showGroup">
<p>
This app encourages people to offer small bits of time to one another. It's a way to
@@ -116,7 +116,7 @@
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="showCommunity = !showCommunity">... I want to participate in community projects.</div>
<div class="text-blue-500" @click="toggleCommunity">... I want to participate in community projects.</div>
<div v-if="showCommunity">
<p>
These are mostly at the beginning stages, so any of them will appreciate your offers that show interest.
@@ -129,7 +129,7 @@
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="showVerifiable = !showVerifiable">... I want to build with verifiable, private data.</div>
<div class="text-blue-500" @click="toggleVerifiable">... I want to build with verifiable, private data.</div>
<div v-if="showVerifiable">
<p>
Make your claims and get others to confirm them. Then you can use the API to pull your copy of all that
@@ -155,7 +155,7 @@
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="showGovernance = !showGovernance">... I want to build governance organically.</div>
<div class="text-blue-500" @click="toggleGovernance">... I want to build governance organically.</div>
<div v-if="showGovernance">
<p>
This requires motivated, dedicated citizens. The good thing is that dedication the primary ingredient;
@@ -174,7 +174,7 @@
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="showBasics = !showBasics">... I want to supply life's basics freely.</div>
<div class="text-blue-500" @click="toggleBasics">... I want to supply life's basics freely.</div>
<div v-if="showBasics">
<p>
This platform is not optimal for balancing needs and resources at this point,
@@ -532,12 +532,7 @@
If you have Bitcoin, donate to
<button
class="text-blue-500 ml-2"
@click="
doCopyTwoSecRedo(
'bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma',
() => (showDidCopy = !showDidCopy)
)
"
@click="copyBitcoinAddress"
>
bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma
<font-awesome v-show="!showDidCopy" icon="copy" class="text-sm text-slate-400 fa-fw" />
@@ -601,12 +596,40 @@ import { Capacitor } from "@capacitor/core";
import * as Package from "../../package.json";
import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER, NotificationIface } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { APP_SERVER } from "../constants/app";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@Component({ components: { QuickNav } })
/**
* HelpView.vue - Comprehensive Help System Component
*
* This component provides extensive documentation, troubleshooting guides, and support
* information for TimeSafari users. It serves as the primary user support resource with
* detailed explanations of features, data backup/restore procedures, and platform-specific
* guidance.
*
* Key Features:
* - Interactive help sections for different user types and interests
* - Onboarding management with reset functionality
* - Context-aware navigation to different app sections
* - Clipboard operations for copying addresses and data
* - Platform detection for iOS, Android, and desktop guidance
* - Version display showing current app version and commit hash
*
* Migration Status: Enhanced Triple Migration Pattern Applied
* - Database Migration: PlatformServiceMixin integrated for settings management
* - SQL Abstraction: No raw SQL queries (component uses high-level operations)
* - Notification Migration: No notifications used (clean component)
* - Template Streamlining: Interactive handlers extracted to methods
*
* @author Matthew Raymer
* @component HelpView
* @vue-facing-decorator
*/
@Component({
components: { QuickNav },
mixins: [PlatformServiceMixin],
})
export default class HelpView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
package = Package;
@@ -625,31 +648,129 @@ export default class HelpView extends Vue {
// Ideally, we put no functionality in here, especially in the setup,
// because we never want this page to have a chance of throwing an error.
// call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text: string, fn: () => void) {
/**
* Copy text to clipboard with visual feedback
*
* This method executes a callback function, copies the provided text to the clipboard,
* and then re-executes the callback after 2 seconds. This is typically used for
* showing a temporary visual indicator (like a checkmark) after copying.
*
* @param {string} text - The text to copy to clipboard
* @param {Function} fn - Callback function to execute before and after copying
*/
doCopyTwoSecRedo(text: string, fn: () => void): void {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
}
async unsetFinishedOnboarding() {
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
/**
* Reset onboarding state for the active account
*
* This method allows users to restart the onboarding process by setting
* the finishedOnboarding flag to false for their active DID. This is useful
* when users want to see the page-by-page help guide again.
*
* @returns {Promise<void>} Promise that resolves when onboarding is reset
*/
async unsetFinishedOnboarding(): Promise<void> {
try {
const settings = await this.$accountSettings();
if (settings.activeDid) {
await databaseUtil.updateDidSpecificSettings(settings.activeDid, {
finishedOnboarding: false,
});
if (settings.activeDid) {
await this.$updateSettings({
...settings,
finishedOnboarding: false,
});
this.$log(
"[HelpView] Onboarding reset successfully for DID: " +
settings.activeDid,
);
}
this.$router.push({ name: "home" });
} catch (error) {
this.$log(
"[HelpView] Error resetting onboarding: " + (error as Error).message,
);
// Still navigate to home even if settings update fails
this.$router.push({ name: "home" });
}
this.$router.push({ name: "home" });
}
private handleQRCodeClick() {
/**
* Handle QR code scanner navigation
*
* This method provides platform-aware navigation to the appropriate QR code
* scanner component. On native platforms (iOS/Android), it navigates to the
* full-screen scanner. On web platforms, it navigates to the web-based scanner.
*
* @private
*/
private handleQRCodeClick(): void {
if (Capacitor.isNativePlatform()) {
this.$router.push({ name: "contact-qr-scan-full" });
} else {
this.$router.push({ name: "contact-qr" });
}
}
/**
* Toggle Alpha chat section visibility
*/
toggleAlpha(): void {
this.showAlpha = !this.showAlpha;
}
/**
* Toggle Group section visibility
*/
toggleGroup(): void {
this.showGroup = !this.showGroup;
}
/**
* Toggle Community section visibility
*/
toggleCommunity(): void {
this.showCommunity = !this.showCommunity;
}
/**
* Toggle Verifiable data section visibility
*/
toggleVerifiable(): void {
this.showVerifiable = !this.showVerifiable;
}
/**
* Toggle Governance section visibility
*/
toggleGovernance(): void {
this.showGovernance = !this.showGovernance;
}
/**
* Toggle Basics section visibility
*/
toggleBasics(): void {
this.showBasics = !this.showBasics;
}
/**
* Copy Bitcoin address to clipboard with visual feedback
*
* This method copies the Bitcoin donation address to the clipboard and
* shows a visual indicator (checkmark) for 2 seconds.
*/
copyBitcoinAddress(): void {
this.doCopyTwoSecRedo(
"bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma",
() => (this.showDidCopy = !this.showDidCopy),
);
}
}
</script>