Complete OnboardingDialog.vue Enhanced Triple Migration Pattern (3.5 minutes)

• Database Migration: Replace databaseUtil with PlatformServiceMixin methods
• SQL Abstraction: Replace raw SQL with $getAllContacts() and $accountSettings()
• Template Streamlining: Add 5 computed properties for consistent styling
• Vue Syntax Fix: Correct vue-facing-decorator mixin and computed property syntax

Migration Details:
- Removed: databaseUtil imports and PlatformServiceFactory usage
- Added: PlatformServiceMixin with $accountSettings(), $getAllContacts(), $updateSettings()
- Created: 5 computed properties (primaryButtonClasses, secondaryButtonClasses, etc.)
- Fixed: Proper @Component mixin declaration and class getter syntax
- Quality: Zero linting errors, full TypeScript compliance

Component provides 3-page onboarding flow (Home, Discover, Create) with
dynamic content based on user registration and contact status.
Ready for human testing across all platforms.
This commit is contained in:
Matthew Raymer
2025-07-08 02:32:15 +00:00
parent 071a3c59ce
commit 7d0486a4cf
6 changed files with 343 additions and 112 deletions

View File

@@ -1,4 +1,25 @@
<!-- similar to ContactNameDialog -->
<!--
OnboardingDialog.vue - Welcome & Help Dialog Component
Provides multi-page onboarding experience for new users with step-by-step
guidance through TimeSafari features including feed, discovery, and project creation.
@author Matthew Raymer
@since 2024-07-08
Features:
- Three-page onboarding flow (Home, Discover, Create)
- Dynamic content based on user registration status
- Contact-aware messaging when contacts exist
- Responsive design with mobile-first approach
- Automatic completion tracking in user settings
Migration Status: Enhanced Triple Migration Pattern Complete
- Uses PlatformServiceMixin for database operations
- Service methods for settings and contact operations
- No raw SQL queries (abstracted to service layer)
- Template streamlining with computed properties
-->
<template>
<div v-if="visible" class="dialog-overlay">
<div v-if="page === OnboardPage.Home" class="dialog">
@@ -6,10 +27,7 @@
Welcome to Time Safari
<br />
- Showcase Impact & Magnify Time
<div
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<div :class="closeButtonClasses" @click="onClickClose(true)">
<font-awesome icon="xmark" class="w-[1em]" />
</div>
</h1>
@@ -39,10 +57,7 @@
<p class="mt-4 flex items-center">
The
<font-awesome
icon="house-chimney"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
<font-awesome icon="house-chimney" :class="navigationIconClasses" />
button below brings you back to this feed screen.
</p>
@@ -51,14 +66,14 @@
<button
type="button"
data-testId="closeOnboardingAndFinish"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
:class="secondaryButtonClasses"
@click="onClickClose(true)"
>
That's enough help, thanks.
</button>
<button
type="button"
class="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-2 py-3 rounded-md mb-2"
:class="primaryButtonClasses"
@click="$router.push({ name: 'discover' })"
>
Show me more!
@@ -68,21 +83,14 @@
<p class="mt-4 flex items-center">
To see these instructions and more, click above on
<span
class="ml-1 mr-1 text-xs uppercase 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-1 rounded-md"
>
Help
</span>
<span :class="helpBadgeClasses"> Help </span>
</p>
</div>
<div v-if="page === OnboardPage.Discover" class="dialog">
<h1 class="text-xl font-bold text-center mb-4 relative">
Offer to Interesting Events & People
<div
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<div :class="closeButtonClasses" @click="onClickClose(true)">
<font-awesome icon="xmark" class="w-[1em]" />
</div>
</h1>
@@ -105,10 +113,7 @@
<p class="mt-4 flex items-center">
The
<font-awesome
icon="magnifying-glass"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
<font-awesome icon="magnifying-glass" :class="navigationIconClasses" />
button below brings you to this discovery screen.
</p>
@@ -117,14 +122,14 @@
<button
type="button"
data-testId="closeOnboardingAndFinish"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
:class="secondaryButtonClasses"
@click="onClickClose(true)"
>
No more help, thanks.
</button>
<button
type="button"
class="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-2 py-3 rounded-md mb-2"
:class="primaryButtonClasses"
@click="$router.push({ name: 'projects' })"
>
Show me even more.
@@ -136,10 +141,7 @@
<div v-if="page === OnboardPage.Create" class="dialog">
<h1 class="text-xl font-bold text-center mb-4 relative">
Fish for Others with Your Projects
<div
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<div :class="closeButtonClasses" @click="onClickClose(true)">
<font-awesome icon="xmark" class="w-[1em]" />
</div>
</h1>
@@ -156,10 +158,7 @@
<p class="mt-4 flex items-center">
The
<font-awesome
icon="hand"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
<font-awesome icon="hand" :class="navigationIconClasses" />
button below brings you here to see your ideas.
</p>
@@ -176,7 +175,7 @@
<button
type="button"
data-testId="closeOnboardingAndFinish"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
:class="secondaryButtonClasses"
@click="onClickClose(true, true)"
>
Let's go!
@@ -185,7 +184,7 @@
</button>
<button
type="button"
class="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-2 py-3 rounded-md mb-2"
:class="primaryButtonClasses"
@click="$router.push({ name: 'help' })"
>
I want to read more Help.
@@ -201,17 +200,11 @@ import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { NotificationIface } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { OnboardPage } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { Contact } from "@/db/tables/contacts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@Component({
computed: {
OnboardPage() {
return OnboardPage;
},
},
mixins: [PlatformServiceMixin],
components: { OnboardPage },
})
export default class OnboardingDialog extends Vue {
@@ -226,34 +219,85 @@ export default class OnboardingDialog extends Vue {
page = OnboardPage.Home;
visible = false;
/**
* Returns OnboardPage enum for template access
*/
get OnboardPage() {
return OnboardPage;
}
/**
* CSS classes for primary action buttons (blue gradient)
*/
get primaryButtonClasses() {
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-2 py-3 rounded-md mb-2";
}
/**
* CSS classes for secondary action buttons (slate gradient)
*/
get secondaryButtonClasses() {
return "block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2";
}
/**
* CSS classes for close button in dialog headers
*/
get closeButtonClasses() {
return "text-lg text-center leading-none absolute right-0 -top-1";
}
/**
* CSS classes for navigation icons in explanatory text
*/
get navigationIconClasses() {
return "ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded";
}
/**
* CSS classes for help badge styling
*/
get helpBadgeClasses() {
return "ml-1 mr-1 text-xs uppercase 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-1 rounded-md";
}
/**
* Opens the onboarding dialog on the specified page
* Loads user settings and contact data to customize the experience
*
* @param page - The onboarding page to display
*/
async open(page: OnboardPage) {
this.page = page;
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
this.isRegistered = !!settings.isRegistered;
const platformService = PlatformServiceFactory.getInstance();
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
if (dbContacts) {
this.numContacts = dbContacts.values.length;
const firstContact = dbContacts.values[0];
const fullContact = databaseUtil.mapColumnsToValues(dbContacts.columns, [
firstContact,
]) as unknown as Contact;
this.firstContactName = fullContact.name || "";
const contacts = await this.$getAllContacts();
this.numContacts = contacts.length;
if (contacts.length > 0) {
this.firstContactName = contacts[0].name || "";
}
this.visible = true;
if (this.page === OnboardPage.Create) {
// we'll assume that they've been through all the other pages
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await this.$updateSettings({
finishedOnboarding: true,
});
}
}
/**
* Closes the onboarding dialog with optional completion actions
*
* @param done - Whether to mark onboarding as complete
* @param goHome - Whether to navigate to home after closing
*/
async onClickClose(done?: boolean, goHome?: boolean) {
this.visible = false;
if (done) {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await this.$updateSettings({
finishedOnboarding: true,
});
if (goHome) {

View File

@@ -444,7 +444,8 @@ export const NOTIFY_CONTACT_NOT_FOUND = {
export const NOTIFY_CONTACT_METHODS_UPDATED = {
title: "Contact Methods Updated",
message: "Contact methods updated. Note that some methods have been updated, such as uppercasing 'email' to 'EMAIL'. Save again if the changes are acceptable.",
message:
"Contact methods updated. Note that some methods have been updated, such as uppercasing 'email' to 'EMAIL'. Save again if the changes are acceptable.",
};
export const NOTIFY_CONTACT_SAVED = {

View File

@@ -1,6 +1,6 @@
<template>
<QuickNav selected="Contacts" />
<section class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Header -->
<div class="mb-8">
@@ -10,7 +10,7 @@
>
<font-awesome icon="chevron-left" class="fa-fw" />
</router-link>
<h1 class="text-4xl text-center font-light pt-4">
Transferred with {{ contact?.name }}
</h1>
@@ -19,11 +19,15 @@
<!-- Info Messages -->
<div class="text-center text-sm text-slate-600 mb-6 space-y-1">
<p>(Only 50 most recent)</p>
<p>(This does not include claims by them if they're not visible to you.)</p>
<p>
(This does not include claims by them if they're not visible to you.)
</p>
</div>
<!-- Transfer History Table -->
<table class="table-auto w-full border-t border-slate-300 text-sm sm:text-base text-center">
<table
class="table-auto w-full border-t border-slate-300 text-sm sm:text-base text-center"
>
<thead class="bg-slate-100">
<tr class="border-b border-slate-300">
<th class="px-1 py-2">Date</th>
@@ -42,7 +46,7 @@
<td class="p-1 text-xs sm:text-sm text-left text-slate-500">
{{ new Date(record.issuedAt).toLocaleString() }}
</td>
<!-- From Them -->
<td class="p-1">
<div v-if="record.agentDid === contact?.did">
@@ -54,11 +58,7 @@
class="text-green-600 fa-fw"
title="Confirmed"
/>
<button
v-else
@click="confirm(record)"
title="Unconfirmed"
>
<button v-else title="Unconfirmed" @click="confirm(record)">
<font-awesome icon="circle" class="text-blue-600 fa-fw" />
</button>
</div>
@@ -67,15 +67,17 @@
</div>
</div>
</td>
<!-- Direction Arrow -->
<td class="p-1">
<font-awesome
:icon="record.agentDid === contact?.did ? 'arrow-left' : 'arrow-right'"
:icon="
record.agentDid === contact?.did ? 'arrow-left' : 'arrow-right'
"
class="text-slate-400 fa-fw"
/>
</td>
<!-- To Them -->
<td class="p-1">
<div v-if="record.agentDid !== contact?.did">
@@ -89,8 +91,8 @@
/>
<button
v-else
@click="cannotConfirmMessage()"
title="Unconfirmed"
@click="cannotConfirmMessage()"
>
<font-awesome icon="circle" class="text-slate-600 fa-fw" />
</button>
@@ -116,10 +118,10 @@ import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
import {
import {
NOTIFY_SETTINGS_RETRIEVAL_ERROR,
NOTIFY_SERVER_RETRIEVAL_ERROR,
NOTIFY_CONFIRMATION_RESTRICTION
NOTIFY_CONFIRMATION_RESTRICTION,
} from "../constants/notifications";
import { Contact } from "../db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
@@ -164,7 +166,7 @@ import { retrieveAccountCount } from "../libs/util";
* - Unconfirmed: Transfer pending confirmation
* - Cannot Confirm: User is not the recipient of the transfer
*/
@Component({
@Component({
components: { QuickNav },
mixins: [PlatformServiceMixin],
})
@@ -215,7 +217,7 @@ export default class ContactAmountssView extends Vue {
*/
async created() {
this.notify = createNotifyHelpers(this.$notify);
try {
const contactDid = this.$route.query["contactDid"] as string;
const contact = await this.$getContact(contactDid);
@@ -232,9 +234,8 @@ export default class ContactAmountssView extends Vue {
} catch (err: any) {
await this.$logError("Error retrieving settings or gives.");
this.notify.error(
err.userMessage ||
NOTIFY_SETTINGS_RETRIEVAL_ERROR.message,
TIMEOUTS.LONG
err.userMessage || NOTIFY_SETTINGS_RETRIEVAL_ERROR.message,
TIMEOUTS.LONG,
);
}
}
@@ -265,12 +266,9 @@ export default class ContactAmountssView extends Vue {
result = resp.data.data;
} else {
await this.$logError(
`Got bad response status & data of ${resp.status} ${JSON.stringify(resp.data)}`
);
this.notify.error(
NOTIFY_SERVER_RETRIEVAL_ERROR.message,
TIMEOUTS.LONG
`Got bad response status & data of ${resp.status} ${JSON.stringify(resp.data)}`,
);
this.notify.error(NOTIFY_SERVER_RETRIEVAL_ERROR.message, TIMEOUTS.LONG);
}
const url2 =
@@ -285,12 +283,9 @@ export default class ContactAmountssView extends Vue {
result = R.concat(result, resp2.data.data);
} else {
await this.$logError(
`Got bad response status & data of ${resp2.status} ${JSON.stringify(resp2.data)}`
);
this.notify.error(
NOTIFY_SERVER_RETRIEVAL_ERROR.message,
TIMEOUTS.LONG
`Got bad response status & data of ${resp2.status} ${JSON.stringify(resp2.data)}`,
);
this.notify.error(NOTIFY_SERVER_RETRIEVAL_ERROR.message, TIMEOUTS.LONG);
}
const sortedResult: Array<GiveSummaryRecord> = R.sort(
@@ -300,10 +295,7 @@ export default class ContactAmountssView extends Vue {
);
this.giveRecords = sortedResult;
} catch (error) {
this.notify.error(
error as string,
TIMEOUTS.LONG
);
this.notify.error(error as string, TIMEOUTS.LONG);
}
}
@@ -361,10 +353,7 @@ export default class ContactAmountssView extends Vue {
userMessage = error as string;
}
// Now set that error for the user to see.
this.notify.error(
userMessage,
TIMEOUTS.LONG
);
this.notify.error(userMessage, TIMEOUTS.LONG);
}
}
@@ -380,7 +369,7 @@ export default class ContactAmountssView extends Vue {
cannotConfirmMessage() {
this.notify.error(
NOTIFY_CONFIRMATION_RESTRICTION.message,
TIMEOUTS.STANDARD
TIMEOUTS.STANDARD,
);
}
}

View File

@@ -141,10 +141,10 @@ import TopMessage from "../components/TopMessage.vue";
import { NotificationIface } from "../constants/app";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
import {
import {
NOTIFY_CONTACT_NOT_FOUND,
NOTIFY_CONTACT_METHODS_UPDATED,
NOTIFY_CONTACT_SAVED
NOTIFY_CONTACT_SAVED,
} from "../constants/notifications";
import { Contact, ContactMethod } from "../db/tables/contacts";
import { AppString } from "../constants/app";
@@ -231,17 +231,20 @@ export default class ContactEditView extends Vue {
*/
async created() {
this.notify = createNotifyHelpers(this.$notify);
const contactDid = this.$route.params.did as string;
const contact = await this.$getContact(contactDid);
if (contact) {
this.contact = contact;
this.contactName = contact.name || "";
this.contactNotes = contact.notes || "";
this.contactMethods = contact.contactMethods || [];
} else {
this.notify.error(`${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`, TIMEOUTS.LONG);
this.notify.error(
`${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`,
TIMEOUTS.LONG,
);
(this.$router as Router).push({ path: "/contacts" });
return;
}
@@ -320,20 +323,17 @@ export default class ContactEditView extends Vue {
this.contactMethods = contactMethods;
this.notify.warning(
NOTIFY_CONTACT_METHODS_UPDATED.message,
TIMEOUTS.LONG
TIMEOUTS.LONG,
);
return;
}
// Save to database via PlatformServiceMixin
await this.$updateContact(
this.contact?.did || "",
{
name: this.contactName,
notes: this.contactNotes,
contactMethods: contactMethods
}
);
await this.$updateContact(this.contact?.did || "", {
name: this.contactName,
notes: this.contactNotes,
contactMethods: contactMethods,
});
// Notify success and redirect
this.notify.success(NOTIFY_CONTACT_SAVED.message, TIMEOUTS.STANDARD);

View File

@@ -1339,7 +1339,7 @@ export default class ProjectViewView extends Vue {
this.notify.confirm(
NOTIFY_CONFIRM_CLAIM.text,
async () => {
await this.confirmClaim(give);
await this.confirmClaim(give);
},
TIMEOUTS.MODAL,
);