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 9561fbc6..482f685e 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, @@ -420,6 +421,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 be486913..943a27fc 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, @@ -303,6 +304,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 8d653897..52cdd589 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -144,6 +144,12 @@ const MIGRATIONS = [ INSERT INTO active_identity (id, activeDid, lastUpdated) VALUES (1, NULL, datetime('now')); `, }, + { + 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/libs/endorserServer.ts b/src/libs/endorserServer.ts index 667083bf..ca8a9e97 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -1313,6 +1313,28 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => { : text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1"); }; +/** + * Formats type string for display by adding spaces before capitals + * and optionally adds an appropriate article prefix (a/an) + * + * @param text - Text to format + * @returns Formatted string with article prefix + */ +export const capitalizeAndInsertSpacesBeforeCapsWithAPrefix = ( + text: string, +): string => { + const word = capitalizeAndInsertSpacesBeforeCaps(text); + if (word) { + // if the word starts with a vowel, use "an" instead of "a" + const firstLetter = word[0].toLowerCase(); + const vowels = ["a", "e", "i", "o", "u"]; + const particle = vowels.includes(firstLetter) ? "an" : "a"; + return particle + " " + word; + } else { + return ""; + } +}; + /** return readable summary of claim, or something generic diff --git a/src/libs/util.ts b/src/libs/util.ts index 51cf021c..4116d0fc 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -160,6 +160,41 @@ export const isGiveAction = ( return isGiveClaimType(veriClaim.claimType); }; +export interface OfferFulfillment { + offerHandleId: string; + offerType: string; +} + +/** + * Extract offer fulfillment information from the fulfills field + * Handles both array and single object cases + */ +export const extractOfferFulfillment = (fulfills: any): OfferFulfillment | null => { + if (!fulfills) { + return null; + } + + // Handle both array and single object cases + let offerFulfill = null; + + if (Array.isArray(fulfills)) { + // Find the Offer in the fulfills array + offerFulfill = fulfills.find((item) => item["@type"] === "Offer"); + } else if (fulfills["@type"] === "Offer") { + // fulfills is a single Offer object + offerFulfill = fulfills; + } + + if (offerFulfill) { + return { + offerHandleId: offerFulfill.identifier, + offerType: offerFulfill["@type"], + }; + } + + return null; +}; + export const shortDid = (did: string) => { if (did.startsWith("did:peer:")) { return ( 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 42fd7c5a..ae2931c9 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, @@ -1699,6 +1700,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 a9d2988f..2b4410f3 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 { @@ -228,6 +229,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/ClaimView.vue b/src/views/ClaimView.vue index c3ca1ffe..c72a0b24 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -24,7 +24,9 @@

{{ - capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "") + serverUtil.capitalizeAndInsertSpacesBeforeCaps( + veriClaim.claimType || "", + ) }}

@@ -556,6 +572,17 @@ export default class ClaimView extends Vue { fulfillsPlanHandleId?: string; fulfillsType?: string; fulfillsHandleId?: string; + fullClaim?: { + fulfills?: Array<{ + "@type": string; + identifier?: string; + }>; + }; + } | null = null; + // Additional offer information extracted from the fulfills array + detailsForGiveOfferFulfillment: { + offerHandleId?: string; + offerType?: string; } | null = null; detailsForOffer: { fulfillsPlanHandleId?: string } | null = null; // Project information for fulfillsPlanHandleId @@ -689,6 +716,7 @@ export default class ClaimView extends Vue { this.confsVisibleToIdList = []; this.detailsForGive = null; this.detailsForOffer = null; + this.detailsForGiveOfferFulfillment = null; this.projectInfo = null; this.fullClaim = null; this.fullClaimDump = ""; @@ -701,6 +729,15 @@ export default class ClaimView extends Vue { this.veriClaimDidsVisible = {}; } + /** + * Extract offer fulfillment information from the fulfills array + */ + extractOfferFulfillment() { + this.detailsForGiveOfferFulfillment = libsUtil.extractOfferFulfillment( + this.detailsForGive?.fullClaim?.fulfills + ); + } + // ================================================= // UTILITY METHODS // ================================================= @@ -762,13 +799,6 @@ export default class ClaimView extends Vue { this.canShare = !!navigator.share; } - // insert a space before any capital letters except the initial letter - // (and capitalize initial letter, just in case) - capitalizeAndInsertSpacesBeforeCaps(text: string): string { - if (!text) return ""; - return text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1"); - } - totalConfirmers() { return ( this.numConfsNotVisible + @@ -825,6 +855,8 @@ export default class ClaimView extends Vue { }); if (giveResp.status === 200 && giveResp.data.data?.length > 0) { this.detailsForGive = giveResp.data.data[0]; + // Extract offer information from the fulfills array + this.extractOfferFulfillment(); } else { await this.$logError( "Error getting detailed give info: " + JSON.stringify(giveResp), diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index 188fce37..5c4f11fc 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -96,50 +96,50 @@ +
+ +
+ + This fulfills a bigger plan + + +
- -
- - This fulfills a bigger plan - - -
- -
- - - This fulfills - {{ - capitalizeAndInsertSpacesBeforeCapsWithAPrefix( - giveDetails?.fulfillsType || "", - ) - }} - - + +
+ + + This fulfills + {{ + serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix( + giveDetailsOfferFulfillment.offerType || "Offer", + ) + }} + + +
@@ -493,6 +493,11 @@ export default class ConfirmGiftView extends Vue { confsVisibleErrorMessage = ""; confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer giveDetails?: GiveSummaryRecord; + // Additional offer information extracted from the fulfills array + giveDetailsOfferFulfillment: { + offerHandleId?: string; + offerType?: string; + } | null = null; giverName = ""; issuerName = ""; isLoading = false; @@ -653,6 +658,8 @@ export default class ConfirmGiftView extends Vue { if (resp.status === 200) { this.giveDetails = resp.data.data[0]; + // Extract offer information from the fulfills array + this.extractOfferFulfillment(); } else { throw new Error("Error getting detailed give info: " + resp.status); } @@ -712,6 +719,15 @@ export default class ConfirmGiftView extends Vue { } } + /** + * Extract offer fulfillment information from the fulfills array + */ + private extractOfferFulfillment() { + this.giveDetailsOfferFulfillment = libsUtil.extractOfferFulfillment( + this.giveDetails?.fullClaim?.fulfills + ); + } + /** * Fetches confirmer information for the claim */ @@ -854,27 +870,6 @@ export default class ConfirmGiftView extends Vue { ); } - /** - * Formats type string for display by adding spaces before capitals - * Optionally adds a prefix - * - * @param text - Text to format - * @param prefix - Optional prefix to add - * @returns Formatted string - */ - capitalizeAndInsertSpacesBeforeCapsWithAPrefix(text: string): string { - const word = this.capitalizeAndInsertSpacesBeforeCaps(text); - if (word) { - // if the word starts with a vowel, use "an" instead of "a" - const firstLetter = word[0].toLowerCase(); - const vowels = ["a", "e", "i", "o", "u"]; - const particle = vowels.includes(firstLetter) ? "an" : "a"; - return particle + " " + word; - } else { - return ""; - } - } - /** * Initiates sharing of claim information * Handles share functionality based on platform capabilities @@ -899,11 +894,5 @@ export default class ConfirmGiftView extends Vue { this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; this.veriClaimDump = ""; } - - capitalizeAndInsertSpacesBeforeCaps(text: string) { - return !text - ? "" - : text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1"); - } } diff --git a/src/views/ContactQRScanFullView.vue b/src/views/ContactQRScanFullView.vue index 32814499..ddddc174 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; @@ -627,6 +628,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 bbfe1af4..d62dd966 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, @@ -324,6 +325,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(); } @@ -743,24 +753,17 @@ export default class ContactQRScanShow extends Vue { !contact.registered ) { setTimeout(() => { - this.notify.confirm( - "Do you want to register them?", + this.$notify( { + group: "modal", + type: "confirm", + title: "Register", + text: "Do you want to register them?", onCancel: async (stopAsking?: boolean) => { - if (stopAsking) { - await this.$updateSettings({ - hideRegisterPromptOnNewContact: stopAsking, - }); - this.hideRegisterPromptOnNewContact = stopAsking; - } + await this.handleRegistrationPromptResponse(stopAsking); }, onNo: async (stopAsking?: boolean) => { - if (stopAsking) { - await this.$updateSettings({ - hideRegisterPromptOnNewContact: stopAsking, - }); - this.hideRegisterPromptOnNewContact = stopAsking; - } + await this.handleRegistrationPromptResponse(stopAsking); }, onYes: async () => { await this.register(contact); @@ -890,6 +893,17 @@ export default class ContactQRScanShow extends Vue { videoElement.style.transform = shouldMirror ? "scaleX(-1)" : "none"; } } + + private async handleRegistrationPromptResponse( + stopAsking?: boolean, + ): Promise { + if (stopAsking) { + await this.$saveSettings({ + hideRegisterPromptOnNewContact: stopAsking, + }); + this.hideRegisterPromptOnNewContact = stopAsking; + } + } } diff --git a/src/views/GiftedDetailsView.vue b/src/views/GiftedDetailsView.vue index e7a2c711..990a8b20 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, @@ -774,6 +775,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 + } } /**