forked from trent_larson/crowd-funder-for-time-pwa
Refactor: simplify RegistrationNotice component interface
- Centralize all registration notice logic within upgraded RegistrationNotice component - Clean up unused methods and imports in HomeView and AccountViewView - Customizable message to maintain original contextual wordings unique to each view Component is now more focused and follows Vue.js best practices for conditional rendering.
This commit is contained in:
@@ -1,33 +1,156 @@
|
|||||||
|
/** * @file RegistrationNotice.vue * @description Reusable component for
|
||||||
|
displaying user registration status and related actions. * Shows registration
|
||||||
|
notice when user is not registered, with options to show identifier info * or
|
||||||
|
access advanced options. * * @author Matthew Raymer * @version 1.0.0 * @created
|
||||||
|
2025-08-21T17:25:28-08:00 */
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="my-4">
|
||||||
v-if="!isRegistered && show"
|
<div
|
||||||
id="noticeBeforeAnnounce"
|
id="noticeSomeoneMustRegisterYou"
|
||||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3"
|
||||||
role="alert"
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
<p class="mb-4">
|
|
||||||
Before you can publicly announce a new project or time commitment, a
|
|
||||||
friend needs to register you.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
class="inline-block 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-4 py-2 rounded-md"
|
|
||||||
@click="shareInfo"
|
|
||||||
>
|
>
|
||||||
Share Your Info
|
<p class="mb-4">{{ message }}</p>
|
||||||
</button>
|
<div class="grid grid-cols-1 gap-2 sm:flex sm:justify-center">
|
||||||
|
<button
|
||||||
|
class="inline-block text-md font-bold 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-4 py-2 rounded-md"
|
||||||
|
@click="showNameThenIdDialog"
|
||||||
|
>
|
||||||
|
Show them {{ passkeysEnabled ? "default" : "your" }} identifier info
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-block 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-4 py-2 rounded-md"
|
||||||
|
@click="openAdvancedOptions"
|
||||||
|
>
|
||||||
|
See advanced options
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UserNameDialog ref="userNameDialog" />
|
||||||
|
<ChoiceButtonDialog ref="choiceButtonDialog" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||||
|
import { Router } from "vue-router";
|
||||||
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
import UserNameDialog from "./UserNameDialog.vue";
|
||||||
|
import ChoiceButtonDialog from "./ChoiceButtonDialog.vue";
|
||||||
|
|
||||||
@Component({ name: "RegistrationNotice" })
|
/**
|
||||||
|
* RegistrationNotice Component
|
||||||
|
*
|
||||||
|
* Displays registration status notice and provides actions for unregistered users.
|
||||||
|
* Handles all registration-related flows internally without requiring parent component intervention.
|
||||||
|
*
|
||||||
|
* Template Usage:
|
||||||
|
* ```vue
|
||||||
|
* <RegistrationNotice
|
||||||
|
* v-if="!isUserRegistered"
|
||||||
|
* :passkeys-enabled="PASSKEYS_ENABLED"
|
||||||
|
* :given-name="givenName"
|
||||||
|
* message="Custom registration message here"
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Component Dependencies:
|
||||||
|
* - UserNameDialog: Dialog for entering user name
|
||||||
|
* - ChoiceButtonDialog: Dialog for sharing method selection
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
name: "RegistrationNotice",
|
||||||
|
components: {
|
||||||
|
UserNameDialog,
|
||||||
|
ChoiceButtonDialog,
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class RegistrationNotice extends Vue {
|
export default class RegistrationNotice extends Vue {
|
||||||
@Prop({ required: true }) isRegistered!: boolean;
|
$router!: Router;
|
||||||
@Prop({ required: true }) show!: boolean;
|
|
||||||
|
|
||||||
@Emit("share-info")
|
/**
|
||||||
shareInfo() {}
|
* Whether passkeys are enabled in the application
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
passkeysEnabled!: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User's given name for dialog pre-population
|
||||||
|
*/
|
||||||
|
@Prop({ required: true })
|
||||||
|
givenName!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom message to display in the registration notice
|
||||||
|
* Defaults to "To share, someone must register you."
|
||||||
|
*/
|
||||||
|
@Prop({ default: "To share, someone must register you." })
|
||||||
|
message!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows name input dialog if needed
|
||||||
|
* Handles the full flow internally without requiring parent component intervention
|
||||||
|
*/
|
||||||
|
showNameThenIdDialog() {
|
||||||
|
this.openUserNameDialog(() => {
|
||||||
|
this.promptForShareMethod();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens advanced options page
|
||||||
|
* Navigates directly to the start page
|
||||||
|
*/
|
||||||
|
openAdvancedOptions() {
|
||||||
|
this.$router.push({ name: "start" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows dialog for sharing method selection
|
||||||
|
* Provides options for different sharing scenarios
|
||||||
|
*/
|
||||||
|
promptForShareMethod() {
|
||||||
|
(this.$refs.choiceButtonDialog as ChoiceButtonDialog).open({
|
||||||
|
title: "How can you share your info?",
|
||||||
|
text: "",
|
||||||
|
option1Text: "We are in a meeting together",
|
||||||
|
option2Text: "We are nearby with cameras",
|
||||||
|
option3Text: "We will share some other way",
|
||||||
|
onOption1: () => {
|
||||||
|
this.$router.push({ name: "onboard-meeting-list" });
|
||||||
|
},
|
||||||
|
onOption2: () => {
|
||||||
|
this.handleQRCodeClick();
|
||||||
|
},
|
||||||
|
onOption3: () => {
|
||||||
|
this.$router.push({ name: "share-my-contact-info" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles QR code sharing based on platform
|
||||||
|
* Navigates to appropriate QR code page
|
||||||
|
*/
|
||||||
|
private handleQRCodeClick() {
|
||||||
|
if (Capacitor.isNativePlatform()) {
|
||||||
|
this.$router.push({ name: "contact-qr-scan-full" });
|
||||||
|
} else {
|
||||||
|
this.$router.push({ name: "contact-qr" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the user name dialog if needed
|
||||||
|
*
|
||||||
|
* @param callback Function to call after name is entered
|
||||||
|
*/
|
||||||
|
openUserNameDialog(callback: () => void) {
|
||||||
|
if (!this.givenName) {
|
||||||
|
(this.$refs.userNameDialog as UserNameDialog).open(callback);
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -55,9 +55,10 @@
|
|||||||
|
|
||||||
<!-- Registration notice -->
|
<!-- Registration notice -->
|
||||||
<RegistrationNotice
|
<RegistrationNotice
|
||||||
:is-registered="isRegistered"
|
v-if="!isRegistered"
|
||||||
:show="showRegistrationNotice"
|
:passkeys-enabled="PASSKEYS_ENABLED"
|
||||||
@share-info="onShareInfo"
|
:given-name="givenName"
|
||||||
|
message="Before you can publicly announce a new project or time commitment, a friend needs to register you."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
@@ -781,6 +782,7 @@ import {
|
|||||||
DEFAULT_PUSH_SERVER,
|
DEFAULT_PUSH_SERVER,
|
||||||
IMAGE_TYPE_PROFILE,
|
IMAGE_TYPE_PROFILE,
|
||||||
NotificationIface,
|
NotificationIface,
|
||||||
|
PASSKEYS_ENABLED,
|
||||||
} from "../constants/app";
|
} from "../constants/app";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import {
|
import {
|
||||||
@@ -851,6 +853,7 @@ export default class AccountViewView extends Vue {
|
|||||||
readonly DEFAULT_PUSH_SERVER: string = DEFAULT_PUSH_SERVER;
|
readonly DEFAULT_PUSH_SERVER: string = DEFAULT_PUSH_SERVER;
|
||||||
readonly DEFAULT_IMAGE_API_SERVER: string = DEFAULT_IMAGE_API_SERVER;
|
readonly DEFAULT_IMAGE_API_SERVER: string = DEFAULT_IMAGE_API_SERVER;
|
||||||
readonly DEFAULT_PARTNER_API_SERVER: string = DEFAULT_PARTNER_API_SERVER;
|
readonly DEFAULT_PARTNER_API_SERVER: string = DEFAULT_PARTNER_API_SERVER;
|
||||||
|
readonly PASSKEYS_ENABLED: boolean = PASSKEYS_ENABLED;
|
||||||
|
|
||||||
// Identity and settings properties
|
// Identity and settings properties
|
||||||
activeDid: string = "";
|
activeDid: string = "";
|
||||||
@@ -1789,20 +1792,6 @@ export default class AccountViewView extends Vue {
|
|||||||
this.doCopyTwoSecRedo(did, () => (this.showDidCopy = !this.showDidCopy));
|
this.doCopyTwoSecRedo(did, () => (this.showDidCopy = !this.showDidCopy));
|
||||||
}
|
}
|
||||||
|
|
||||||
get showRegistrationNotice(): boolean {
|
|
||||||
// Show the notice if not registered and any other conditions you want
|
|
||||||
return !this.isRegistered;
|
|
||||||
}
|
|
||||||
|
|
||||||
onShareInfo() {
|
|
||||||
// Navigate to QR code sharing page - mobile uses full scan, web uses basic
|
|
||||||
if (Capacitor.isNativePlatform()) {
|
|
||||||
this.$router.push({ name: "contact-qr-scan-full" });
|
|
||||||
} else {
|
|
||||||
this.$router.push({ name: "contact-qr" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onRecheckLimits() {
|
onRecheckLimits() {
|
||||||
this.checkLimits();
|
this.checkLimits();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,32 +86,14 @@ Raymer * @version 1.0.0 */
|
|||||||
Identity creation is now handled by router navigation guard.
|
Identity creation is now handled by router navigation guard.
|
||||||
-->
|
-->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div v-if="!isUserRegistered" class="my-4">
|
<RegistrationNotice
|
||||||
<div
|
v-if="!isUserRegistered"
|
||||||
id="noticeSomeoneMustRegisterYou"
|
:passkeys-enabled="PASSKEYS_ENABLED"
|
||||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3"
|
:given-name="givenName"
|
||||||
>
|
message="To share, someone must register you."
|
||||||
<p class="mb-4">To share, someone must register you.</p>
|
/>
|
||||||
<div class="grid grid-cols-1 gap-2 sm:flex sm:justify-center">
|
|
||||||
<button
|
|
||||||
class="inline-block text-md font-bold 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-4 py-2 rounded-md"
|
|
||||||
@click="showNameThenIdDialog()"
|
|
||||||
>
|
|
||||||
Show them {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
|
|
||||||
info
|
|
||||||
</button>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'start' }"
|
|
||||||
class="inline-block 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-4 py-2 rounded-md"
|
|
||||||
>
|
|
||||||
See advanced options
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<UserNameDialog ref="userNameDialog" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else id="sectionRecordSomethingGiven">
|
<div v-if="isUserRegistered" id="sectionRecordSomethingGiven">
|
||||||
<!-- Record Quick-Action -->
|
<!-- Record Quick-Action -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="flex gap-2 items-center mb-2">
|
<div class="flex gap-2 items-center mb-2">
|
||||||
@@ -251,8 +233,6 @@ Raymer * @version 1.0.0 */
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<ChoiceButtonDialog ref="choiceButtonDialog" />
|
|
||||||
|
|
||||||
<ImageViewer v-model:is-open="isImageViewerOpen" :image-url="selectedImage" />
|
<ImageViewer v-model:is-open="isImageViewerOpen" :image-url="selectedImage" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -260,7 +240,6 @@ Raymer * @version 1.0.0 */
|
|||||||
import { UAParser } from "ua-parser-js";
|
import { UAParser } from "ua-parser-js";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { Capacitor } from "@capacitor/core";
|
|
||||||
|
|
||||||
//import App from "../App.vue";
|
//import App from "../App.vue";
|
||||||
import EntityIcon from "../components/EntityIcon.vue";
|
import EntityIcon from "../components/EntityIcon.vue";
|
||||||
@@ -271,10 +250,9 @@ import InfiniteScroll from "../components/InfiniteScroll.vue";
|
|||||||
import OnboardingDialog from "../components/OnboardingDialog.vue";
|
import OnboardingDialog from "../components/OnboardingDialog.vue";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
|
||||||
import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue";
|
|
||||||
import ImageViewer from "../components/ImageViewer.vue";
|
import ImageViewer from "../components/ImageViewer.vue";
|
||||||
import ActivityListItem from "../components/ActivityListItem.vue";
|
import ActivityListItem from "../components/ActivityListItem.vue";
|
||||||
|
import RegistrationNotice from "../components/RegistrationNotice.vue";
|
||||||
import {
|
import {
|
||||||
AppString,
|
AppString,
|
||||||
NotificationIface,
|
NotificationIface,
|
||||||
@@ -382,12 +360,11 @@ interface FeedError {
|
|||||||
GiftedPrompts,
|
GiftedPrompts,
|
||||||
InfiniteScroll,
|
InfiniteScroll,
|
||||||
OnboardingDialog,
|
OnboardingDialog,
|
||||||
ChoiceButtonDialog,
|
|
||||||
QuickNav,
|
QuickNav,
|
||||||
TopMessage,
|
TopMessage,
|
||||||
UserNameDialog,
|
|
||||||
ImageViewer,
|
ImageViewer,
|
||||||
ActivityListItem,
|
ActivityListItem,
|
||||||
|
RegistrationNotice,
|
||||||
},
|
},
|
||||||
mixins: [PlatformServiceMixin],
|
mixins: [PlatformServiceMixin],
|
||||||
})
|
})
|
||||||
@@ -1643,67 +1620,6 @@ export default class HomeView extends Vue {
|
|||||||
return known ? "text-slate-500" : "text-slate-100";
|
return known ? "text-slate-500" : "text-slate-100";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows name input dialog if needed
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @callGraph
|
|
||||||
* Called by: Template
|
|
||||||
* Calls:
|
|
||||||
* - UserNameDialog.open()
|
|
||||||
* - promptForShareMethod()
|
|
||||||
*
|
|
||||||
* @chain
|
|
||||||
* Template -> showNameThenIdDialog() -> promptForShareMethod()
|
|
||||||
*
|
|
||||||
* @requires
|
|
||||||
* - this.$refs.userNameDialog
|
|
||||||
* - this.givenName
|
|
||||||
*/
|
|
||||||
showNameThenIdDialog() {
|
|
||||||
if (!this.givenName) {
|
|
||||||
(this.$refs.userNameDialog as UserNameDialog).open(() => {
|
|
||||||
this.promptForShareMethod();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.promptForShareMethod();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows dialog for sharing method selection
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
* @callGraph
|
|
||||||
* Called by: showNameThenIdDialog()
|
|
||||||
* Calls: ChoiceButtonDialog.open()
|
|
||||||
*
|
|
||||||
* @chain
|
|
||||||
* Template -> showNameThenIdDialog() -> promptForShareMethod()
|
|
||||||
*
|
|
||||||
* @requires
|
|
||||||
* - this.$refs.choiceButtonDialog
|
|
||||||
* - this.$router
|
|
||||||
*/
|
|
||||||
promptForShareMethod() {
|
|
||||||
(this.$refs.choiceButtonDialog as ChoiceButtonDialog).open({
|
|
||||||
title: "How can you share your info?",
|
|
||||||
text: "",
|
|
||||||
option1Text: "We are in a meeting together",
|
|
||||||
option2Text: "We are nearby with cameras",
|
|
||||||
option3Text: "We will share some other way",
|
|
||||||
onOption1: () => {
|
|
||||||
this.$router.push({ name: "onboard-meeting-list" });
|
|
||||||
},
|
|
||||||
onOption2: () => {
|
|
||||||
this.handleQRCodeClick();
|
|
||||||
},
|
|
||||||
onOption3: () => {
|
|
||||||
this.$router.push({ name: "share-my-contact-info" });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens image viewer dialog
|
* Opens image viewer dialog
|
||||||
*
|
*
|
||||||
@@ -1716,14 +1632,6 @@ export default class HomeView extends Vue {
|
|||||||
this.isImageViewerOpen = true;
|
this.isImageViewerOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleQRCodeClick() {
|
|
||||||
if (Capacitor.isNativePlatform()) {
|
|
||||||
this.$router.push({ name: "contact-qr-scan-full" });
|
|
||||||
} else {
|
|
||||||
this.$router.push({ name: "contact-qr" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openPersonDialog(
|
openPersonDialog(
|
||||||
giver?: GiverReceiverInputInfo | "Unnamed",
|
giver?: GiverReceiverInputInfo | "Unnamed",
|
||||||
prompt?: string,
|
prompt?: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user