feat: implement seed phrase backup reminder modal
Add comprehensive seed phrase backup reminder system to encourage users to secure their identity after creating content. Core Features: - Modal dialog with "Backup Identifier Seed" and "Remind me Later" options - 24-hour localStorage cooldown to prevent notification fatigue - 1-second delay after success messages for better UX flow - Focuses on claim creation actions, not confirmations New Files: - src/utils/seedPhraseReminder.ts: Core utility for reminder logic - doc/seed-phrase-reminder-implementation.md: Comprehensive documentation Trigger Points Added: - Profile saving (AccountViewView) - Claim creation (ClaimAddRawView, GiftedDialog, GiftedDetailsView) - Offer creation (OfferDialog) - QR code view exit (ContactQRScanFullView, ContactQRScanShowView) Technical Implementation: - Uses existing notification group modal system from App.vue - Integrates with PlatformServiceMixin for account settings access - Graceful error handling with logging fallbacks - Non-blocking implementation that doesn't affect main functionality - Modal stays open indefinitely (timeout: -1) until user interaction User Experience: - Non-intrusive reminders that respect user preferences - Clear call-to-action for security-conscious users - Seamless integration with existing workflows - Maintains focus on content creation rather than confirmation actions
This commit is contained in:
@@ -82,6 +82,7 @@ import GiftDetailsStep from "../components/GiftDetailsStep.vue";
|
||||
import { PlanData } from "../interfaces/records";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
|
||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
import {
|
||||
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
|
||||
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
|
||||
@@ -411,6 +412,15 @@ export default class GiftedDialog extends Vue {
|
||||
);
|
||||
} else {
|
||||
this.safeNotify.success("That gift was recorded.", TIMEOUTS.VERY_LONG);
|
||||
|
||||
// Show seed phrase backup reminder if needed
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
||||
} catch (error) {
|
||||
logger.error("Error checking seed backup status:", error);
|
||||
}
|
||||
|
||||
if (this.callbackOnSuccess) {
|
||||
this.callbackOnSuccess(amount);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ import * as libsUtil from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
import {
|
||||
NOTIFY_OFFER_SETTINGS_ERROR,
|
||||
NOTIFY_OFFER_RECORDING,
|
||||
@@ -299,6 +300,14 @@ export default class OfferDialog extends Vue {
|
||||
);
|
||||
} else {
|
||||
this.notify.success(NOTIFY_OFFER_SUCCESS.message, TIMEOUTS.VERY_LONG);
|
||||
|
||||
// Show seed phrase backup reminder if needed
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
||||
} catch (error) {
|
||||
logger.error("Error checking seed backup status:", error);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
|
||||
90
src/utils/seedPhraseReminder.ts
Normal file
90
src/utils/seedPhraseReminder.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
|
||||
const SEED_REMINDER_KEY = "seedPhraseReminderLastShown";
|
||||
const REMINDER_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
|
||||
/**
|
||||
* Checks if the seed phrase backup reminder should be shown
|
||||
* @param hasBackedUpSeed - Whether the user has backed up their seed phrase
|
||||
* @returns true if the reminder should be shown, false otherwise
|
||||
*/
|
||||
export function shouldShowSeedReminder(hasBackedUpSeed: boolean): boolean {
|
||||
// Don't show if user has already backed up
|
||||
if (hasBackedUpSeed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check localStorage for last shown time
|
||||
const lastShown = localStorage.getItem(SEED_REMINDER_KEY);
|
||||
if (!lastShown) {
|
||||
return true; // First time, show the reminder
|
||||
}
|
||||
|
||||
try {
|
||||
const lastShownTime = parseInt(lastShown, 10);
|
||||
const now = Date.now();
|
||||
const timeSinceLastShown = now - lastShownTime;
|
||||
|
||||
// Show if more than 24 hours have passed
|
||||
return timeSinceLastShown >= REMINDER_COOLDOWN_MS;
|
||||
} catch (error) {
|
||||
// If there's an error parsing the timestamp, show the reminder
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the seed phrase reminder as shown by updating localStorage
|
||||
*/
|
||||
export function markSeedReminderShown(): void {
|
||||
localStorage.setItem(SEED_REMINDER_KEY, Date.now().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the seed phrase backup reminder notification
|
||||
* @returns NotificationIface configuration for the reminder modal
|
||||
*/
|
||||
export function createSeedReminderNotification(): NotificationIface {
|
||||
return {
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Backup Your Identifier Seed?",
|
||||
text: "It looks like you haven't backed up your identifier seed yet. It's important to back it up as soon as possible to secure your identity.",
|
||||
yesText: "Backup Identifier Seed",
|
||||
noText: "Remind me Later",
|
||||
onYes: async () => {
|
||||
// Navigate to seed backup page
|
||||
window.location.href = "/seed-backup";
|
||||
},
|
||||
onNo: async () => {
|
||||
// Mark as shown so it won't appear again for 24 hours
|
||||
markSeedReminderShown();
|
||||
},
|
||||
onCancel: async () => {
|
||||
// Mark as shown so it won't appear again for 24 hours
|
||||
markSeedReminderShown();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the seed phrase backup reminder if conditions are met
|
||||
* @param hasBackedUpSeed - Whether the user has backed up their seed phrase
|
||||
* @param notifyFunction - Function to show notifications
|
||||
* @returns true if the reminder was shown, false otherwise
|
||||
*/
|
||||
export function showSeedPhraseReminder(
|
||||
hasBackedUpSeed: boolean,
|
||||
notifyFunction: (notification: NotificationIface, timeout?: number) => void,
|
||||
): boolean {
|
||||
if (shouldShowSeedReminder(hasBackedUpSeed)) {
|
||||
const notification = createSeedReminderNotification();
|
||||
// Add 1-second delay before showing the modal to allow success message to be visible
|
||||
setTimeout(() => {
|
||||
// Pass -1 as timeout to ensure modal stays open until user interaction
|
||||
notifyFunction(notification, -1);
|
||||
}, 1000);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -811,6 +811,7 @@ import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
import {
|
||||
AccountSettings,
|
||||
isApiError,
|
||||
@@ -1695,6 +1696,14 @@ export default class AccountViewView extends Vue {
|
||||
);
|
||||
if (success) {
|
||||
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_SAVED);
|
||||
|
||||
// Show seed phrase backup reminder if needed
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
||||
} catch (error) {
|
||||
logger.error("Error checking seed backup status:", error);
|
||||
}
|
||||
} else {
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
|
||||
// Type guard for API responses
|
||||
function isApiResponse(response: unknown): response is AxiosResponse {
|
||||
@@ -223,6 +224,14 @@ export default class ClaimAddRawView extends Vue {
|
||||
);
|
||||
if (result.success) {
|
||||
this.notify.success("Claim submitted.", TIMEOUTS.LONG);
|
||||
|
||||
// Show seed phrase backup reminder if needed
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
||||
} catch (error) {
|
||||
logger.error("Error checking seed backup status:", error);
|
||||
}
|
||||
} else {
|
||||
logger.error("Got error submitting the claim:", result);
|
||||
this.notify.error(
|
||||
|
||||
@@ -144,6 +144,7 @@ import {
|
||||
QR_TIMEOUT_LONG,
|
||||
} from "@/constants/notifications";
|
||||
import { createNotifyHelpers, NotifyFunction } from "../utils/notify";
|
||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
|
||||
interface QRScanResult {
|
||||
rawValue?: string;
|
||||
@@ -622,6 +623,15 @@ export default class ContactQRScanFull extends Vue {
|
||||
*/
|
||||
async handleBack() {
|
||||
await this.cleanupScanner();
|
||||
|
||||
// Show seed phrase backup reminder if needed
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
||||
} catch (error) {
|
||||
logger.error("Error checking seed backup status:", error);
|
||||
}
|
||||
|
||||
this.$router.back();
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
||||
import { CameraState } from "@/services/QRScanner/types";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
import {
|
||||
NOTIFY_QR_INITIALIZATION_ERROR,
|
||||
NOTIFY_QR_CAMERA_IN_USE,
|
||||
@@ -319,6 +320,15 @@ export default class ContactQRScanShow extends Vue {
|
||||
|
||||
async handleBack(): Promise<void> {
|
||||
await this.cleanupScanner();
|
||||
|
||||
// Show seed phrase backup reminder if needed
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
||||
} catch (error) {
|
||||
logger.error("Error checking seed backup status:", error);
|
||||
}
|
||||
|
||||
this.$router.back();
|
||||
}
|
||||
|
||||
|
||||
@@ -280,6 +280,7 @@ import { logger } from "../utils/logger";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
import {
|
||||
NOTIFY_GIFTED_DETAILS_RETRIEVAL_ERROR,
|
||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
|
||||
@@ -770,6 +771,15 @@ export default class GiftedDetails extends Vue {
|
||||
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED.message,
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
|
||||
// Show seed phrase backup reminder if needed
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
||||
} catch (error) {
|
||||
logger.error("Error checking seed backup status:", error);
|
||||
}
|
||||
|
||||
localStorage.removeItem("imageUrl");
|
||||
if (this.destinationPathAfter) {
|
||||
(this.$router as Router).push({ path: this.destinationPathAfter });
|
||||
|
||||
Reference in New Issue
Block a user