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/DataExportSection.vue b/src/components/DataExportSection.vue
index 61bf4753..17524e79 100644
--- a/src/components/DataExportSection.vue
+++ b/src/components/DataExportSection.vue
@@ -16,6 +16,12 @@ messages * - Conditional UI based on platform capabilities * * @component *
:to="{ name: 'seed-backup' }"
:class="backupButtonClasses"
>
+
+
Backup Identifier Seed
@@ -98,6 +104,12 @@ export default class DataExportSection extends Vue {
*/
isExporting = false;
+ /**
+ * Flag indicating if the user has backed up their seed phrase
+ * Used to control the visibility of the notification dot
+ */
+ hasBackedUpSeed = false;
+
/**
* Notification helper for consistent notification patterns
* Created as a getter to ensure $notify is available when called
@@ -129,7 +141,7 @@ export default class DataExportSection extends Vue {
* CSS classes for the backup button (router link)
*/
get backupButtonClasses(): string {
- 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-1.5 py-2 rounded-md mb-2 mt-2";
+ return "block relative 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-1.5 py-2 rounded-md mb-2 mt-2";
}
/**
@@ -218,6 +230,22 @@ export default class DataExportSection extends Vue {
created() {
this.notify = createNotifyHelpers(this.$notify);
+ this.loadSeedBackupStatus();
+ }
+
+ /**
+ * Loads the seed backup status from account settings
+ * Updates the hasBackedUpSeed flag to control notification dot visibility
+ */
+ private async loadSeedBackupStatus(): Promise {
+ try {
+ const settings = await this.$accountSettings();
+ this.hasBackedUpSeed = !!settings.hasBackedUpSeed;
+ } catch (err: unknown) {
+ logger.error("Failed to load seed backup status:", err);
+ // Default to false (show notification dot) if we can't load the setting
+ this.hasBackedUpSeed = false;
+ }
}
}
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/db-sql/migration.ts b/src/db-sql/migration.ts
index 67944b75..0881dd02 100644
--- a/src/db-sql/migration.ts
+++ b/src/db-sql/migration.ts
@@ -124,6 +124,12 @@ const MIGRATIONS = [
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
`,
},
+ {
+ name: "003_add_hasBackedUpSeed_to_settings",
+ sql: `
+ ALTER TABLE settings ADD COLUMN hasBackedUpSeed BOOLEAN DEFAULT FALSE;
+ `,
+ },
];
/**
diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts
index 0b86e355..ff43e0f8 100644
--- a/src/db/tables/settings.ts
+++ b/src/db/tables/settings.ts
@@ -29,6 +29,7 @@ export type Settings = {
finishedOnboarding?: boolean; // the user has completed the onboarding process
firstName?: string; // user's full name, may be null if unwanted for a particular account
+ hasBackedUpSeed?: boolean; // tracks whether the user has backed up their seed phrase
hideRegisterPromptOnNewContact?: boolean;
isRegistered?: boolean;
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
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 });
diff --git a/src/views/SeedBackupView.vue b/src/views/SeedBackupView.vue
index c19caf13..fb9b7605 100644
--- a/src/views/SeedBackupView.vue
+++ b/src/views/SeedBackupView.vue
@@ -231,9 +231,24 @@ export default class SeedBackupView extends Vue {
/**
* Reveals the seed phrase to the user
* Sets showSeed to true to display the sensitive seed phrase data
+ * Updates the hasBackedUpSeed setting to true to track that user has backed up
*/
- revealSeed(): void {
+ async revealSeed(): Promise {
this.showSeed = true;
+
+ // Update the account setting to track that user has backed up their seed
+ try {
+ const settings = await this.$accountSettings();
+ if (settings.activeDid) {
+ await this.$saveUserSettings(settings.activeDid, {
+ hasBackedUpSeed: true,
+ });
+ }
+ } catch (err: unknown) {
+ logger.error("Failed to update hasBackedUpSeed setting:", err);
+ // Don't show error to user as this is not critical to the main functionality
+ // The seed phrase is still revealed, just the tracking won't work
+ }
}
/**