From f38ec1daffaec292ba4808d4c18d200ba71d7d64 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Wed, 3 Sep 2025 19:50:29 +0800 Subject: [PATCH] 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 --- doc/seed-phrase-reminder-implementation.md | 181 +++++++++++++++++++++ src/components/GiftedDialog.vue | 10 ++ src/components/OfferDialog.vue | 9 + src/utils/seedPhraseReminder.ts | 90 ++++++++++ src/views/AccountViewView.vue | 9 + src/views/ClaimAddRawView.vue | 9 + src/views/ContactQRScanFullView.vue | 10 ++ src/views/ContactQRScanShowView.vue | 10 ++ src/views/GiftedDetailsView.vue | 10 ++ 9 files changed, 338 insertions(+) create mode 100644 doc/seed-phrase-reminder-implementation.md create mode 100644 src/utils/seedPhraseReminder.ts diff --git a/doc/seed-phrase-reminder-implementation.md b/doc/seed-phrase-reminder-implementation.md new file mode 100644 index 00000000..400535f8 --- /dev/null +++ b/doc/seed-phrase-reminder-implementation.md @@ -0,0 +1,181 @@ +# Seed Phrase Backup Reminder Implementation + +## Overview + +This implementation adds a modal dialog that reminds users to back up their seed phrase if they haven't done so yet. The reminder appears after specific user actions and includes a 24-hour cooldown to avoid being too intrusive. + +## Features + +- **Modal Dialog**: Uses the existing notification group modal system from `App.vue` +- **Smart Timing**: Only shows when `hasBackedUpSeed = false` +- **24-Hour Cooldown**: Uses localStorage to prevent showing more than once per day +- **Action-Based Triggers**: Shows after specific user actions +- **User Choice**: "Backup Identifier Seed" or "Remind me Later" options + +## Implementation Details + +### Core Utility (`src/utils/seedPhraseReminder.ts`) + +The main utility provides: + +- `shouldShowSeedReminder(hasBackedUpSeed)`: Checks if reminder should be shown +- `markSeedReminderShown()`: Updates localStorage timestamp +- `createSeedReminderNotification()`: Creates the modal configuration +- `showSeedPhraseReminder(hasBackedUpSeed, notifyFunction)`: Main function to show reminder + +### Trigger Points + +The reminder is shown after these user actions: + +**Note**: The reminder is triggered by **claim creation** actions, not claim confirmations. This focuses on when users are actively creating new content rather than just confirming existing claims. + +1. **Profile Saving** (`AccountViewView.vue`) + - After clicking "Save Profile" button + - Only when profile save is successful + +2. **Claim Creation** (Multiple views) + - `ClaimAddRawView.vue`: After submitting raw claims + - `GiftedDialog.vue`: After creating gifts/claims + - `GiftedDetailsView.vue`: After recording gifts/claims + - `OfferDialog.vue`: After creating offers + +3. **QR Code Views Exit** + - `ContactQRScanFullView.vue`: When exiting via back button + - `ContactQRScanShowView.vue`: When exiting via back button + +### Modal Configuration + +```typescript +{ + 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: () => navigate to /seed-backup, + onNo: () => mark as shown for 24 hours, + onCancel: () => mark as shown for 24 hours +} +``` + +**Important**: The modal is configured with `timeout: -1` to ensure it stays open until the user explicitly interacts with one of the buttons. This prevents the dialog from closing automatically. + +### Cooldown Mechanism + +- **Storage Key**: `seedPhraseReminderLastShown` +- **Cooldown Period**: 24 hours (24 * 60 * 60 * 1000 milliseconds) +- **Implementation**: localStorage with timestamp comparison +- **Fallback**: Shows reminder if timestamp is invalid or missing + +## User Experience + +### When Reminder Appears + +- User has not backed up their seed phrase (`hasBackedUpSeed = false`) +- At least 24 hours have passed since last reminder +- User performs one of the trigger actions +- **1-second delay** after the success message to allow users to see the confirmation + +### User Options + +1. **"Backup Identifier Seed"**: Navigates to `/seed-backup` page +2. **"Remind me Later"**: Dismisses and won't show again for 24 hours +3. **Cancel/Close**: Same behavior as "Remind me Later" + +### Frequency Control + +- **First Time**: Always shows if user hasn't backed up +- **Subsequent**: Only shows after 24-hour cooldown +- **Automatic Reset**: When user completes seed backup (`hasBackedUpSeed = true`) + +## Technical Implementation + +### Error Handling + +- Graceful fallback if localStorage operations fail +- Logging of errors for debugging +- Non-blocking implementation (doesn't affect main functionality) + +### Integration Points + +- **Platform Service**: Uses `$accountSettings()` to check backup status +- **Notification System**: Integrates with existing `$notify` system +- **Router**: Uses `window.location.href` for navigation + +### Performance Considerations + +- Minimal localStorage operations +- No blocking operations +- Efficient timestamp comparisons +- **Timing Behavior**: 1-second delay before showing reminder to improve user experience flow + +## Testing + +### Manual Testing Scenarios + +1. **First Time User** + - Create new account + - Perform trigger action (save profile, create claim, exit QR view) + - Verify reminder appears + +2. **Repeat User (Within 24h)** + - Perform trigger action + - Verify reminder does NOT appear + +3. **Repeat User (After 24h)** + - Wait 24+ hours + - Perform trigger action + - Verify reminder appears again + +4. **User Who Has Backed Up** + - Complete seed backup + - Perform trigger action + - Verify reminder does NOT appear + +5. **QR Code View Exit** + - Navigate to QR code view (full or show) + - Exit via back button + - Verify reminder appears (if conditions are met) + +### Browser Testing + +- Test localStorage functionality +- Verify timestamp handling +- Check navigation to seed backup page + +## Future Enhancements + +### Potential Improvements + +1. **Customizable Cooldown**: Allow users to set reminder frequency +2. **Progressive Urgency**: Increase reminder frequency over time +3. **Analytics**: Track reminder effectiveness and user response +4. **A/B Testing**: Test different reminder messages and timing + +### Configuration Options + +- Reminder frequency settings +- Custom reminder messages +- Different trigger conditions +- Integration with other notification systems + +## Maintenance + +### Monitoring + +- Check localStorage usage in browser dev tools +- Monitor user feedback about reminder frequency +- Track navigation success to seed backup page + +### Updates + +- Modify reminder text in `createSeedReminderNotification()` +- Adjust cooldown period in `REMINDER_COOLDOWN_MS` constant +- Add new trigger points as needed + +## Conclusion + +This implementation provides a non-intrusive way to remind users about seed phrase backup while respecting their preferences and avoiding notification fatigue. The 24-hour cooldown ensures users aren't overwhelmed while maintaining the importance of the security reminder. + +The feature is fully integrated with the existing codebase architecture and follows established patterns for notifications, error handling, and user interaction. diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index d91e68b3..abdeb4c4 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -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); } diff --git a/src/components/OfferDialog.vue b/src/components/OfferDialog.vue index a958bd06..eeedce82 100644 --- a/src/components/OfferDialog.vue +++ b/src/components/OfferDialog.vue @@ -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) { diff --git a/src/utils/seedPhraseReminder.ts b/src/utils/seedPhraseReminder.ts new file mode 100644 index 00000000..9c0348fc --- /dev/null +++ b/src/utils/seedPhraseReminder.ts @@ -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; +} diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index e68efca2..19872da6 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -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); } diff --git a/src/views/ClaimAddRawView.vue b/src/views/ClaimAddRawView.vue index 8784c7ef..ed96a79c 100644 --- a/src/views/ClaimAddRawView.vue +++ b/src/views/ClaimAddRawView.vue @@ -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( diff --git a/src/views/ContactQRScanFullView.vue b/src/views/ContactQRScanFullView.vue index bb52bea4..42358b65 100644 --- a/src/views/ContactQRScanFullView.vue +++ b/src/views/ContactQRScanFullView.vue @@ -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(); } diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index bdf86a84..ddc3c140 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -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 { 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(); } diff --git a/src/views/GiftedDetailsView.vue b/src/views/GiftedDetailsView.vue index 983ee0b8..812c0b02 100644 --- a/src/views/GiftedDetailsView.vue +++ b/src/views/GiftedDetailsView.vue @@ -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 });