docs: merge notification system docs into single Native-First guide
- Consolidate 5 notification-system-* files into doc/notification-system.md - Add web-push cleanup guide and Start-on-Login glossary entry - Configure markdownlint for consistent formatting - Remove web-push references, focus on native OS scheduling Reduces maintenance overhead while preserving all essential information in a single, well-formatted reference document.
This commit is contained in:
181
doc/seed-phrase-reminder-implementation.md
Normal file
181
doc/seed-phrase-reminder-implementation.md
Normal file
@@ -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.
|
||||
@@ -16,6 +16,12 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
||||
:to="{ name: 'seed-backup' }"
|
||||
:class="backupButtonClasses"
|
||||
>
|
||||
<!-- Notification dot - show while the user has not yet backed up their seed phrase -->
|
||||
<font-awesome
|
||||
v-if="!hasBackedUpSeed"
|
||||
icon="circle"
|
||||
class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full"
|
||||
></font-awesome>
|
||||
Backup Identifier Seed
|
||||
</router-link>
|
||||
|
||||
@@ -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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
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,
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
<div class="flex columns-3">
|
||||
<h2 class="text-md font-bold w-full">
|
||||
{{
|
||||
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "")
|
||||
serverUtil.capitalizeAndInsertSpacesBeforeCaps(
|
||||
veriClaim.claimType || "",
|
||||
)
|
||||
}}
|
||||
<button
|
||||
v-if="canEditClaim"
|
||||
@@ -106,77 +108,91 @@
|
||||
</div>
|
||||
|
||||
<!-- Fullfills Links -->
|
||||
|
||||
<!-- fullfills links for a give -->
|
||||
<div v-if="detailsForGive?.fulfillsPlanHandleId" class="mt-4">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
|
||||
"
|
||||
class="text-blue-500 mt-2"
|
||||
>
|
||||
Fulfills a bigger plan...
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- if there's another, it's probably fulfilling an offer, too -->
|
||||
<div
|
||||
v-if="
|
||||
detailsForGive?.fulfillsType &&
|
||||
detailsForGive?.fulfillsType !== 'PlanAction' &&
|
||||
detailsForGive?.fulfillsHandleId
|
||||
"
|
||||
>
|
||||
<!-- router-link to /claim/ only changes URL path -->
|
||||
<a
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
@click="
|
||||
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
|
||||
"
|
||||
>
|
||||
Fulfills
|
||||
{{
|
||||
capitalizeAndInsertSpacesBeforeCaps(
|
||||
detailsForGive.fulfillsType,
|
||||
)
|
||||
}}...
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- fullfills links for an offer -->
|
||||
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
|
||||
"
|
||||
class="text-blue-500 mt-4"
|
||||
>
|
||||
Offered to a bigger plan...
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Providers -->
|
||||
<div v-if="providersForGive?.length > 0" class="mt-4">
|
||||
<span>Other assistance provided by:</span>
|
||||
<ul class="ml-4">
|
||||
<li
|
||||
v-for="provider of providersForGive"
|
||||
:key="provider.identifier"
|
||||
class="list-disc ml-4"
|
||||
<div class="mt-4 empty:hidden">
|
||||
<!-- fullfills links for a give -->
|
||||
<div v-if="detailsForGive?.fulfillsPlanHandleId">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
|
||||
"
|
||||
class="text-blue-500 mt-2"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="grow overflow-hidden">
|
||||
<a
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
@click="handleProviderClick(provider)"
|
||||
>
|
||||
an activity...
|
||||
</a>
|
||||
This fulfills a bigger plan
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Show offer fulfillment if this give fulfills an offer -->
|
||||
<div v-if="detailsForGiveOfferFulfillment?.offerHandleId">
|
||||
<!-- router-link to /claim/ only changes URL path -->
|
||||
<a
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
@click="
|
||||
showDifferentClaimPage(
|
||||
detailsForGiveOfferFulfillment.offerHandleId,
|
||||
)
|
||||
"
|
||||
>
|
||||
This fulfills
|
||||
{{
|
||||
serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
||||
detailsForGiveOfferFulfillment.offerType || "Offer",
|
||||
)
|
||||
}}
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- fullfills links for an offer -->
|
||||
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
|
||||
"
|
||||
class="text-blue-500 mt-4"
|
||||
>
|
||||
Offered to a bigger plan
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Providers -->
|
||||
<div v-if="providersForGive?.length > 0">
|
||||
<span>Other assistance provided by:</span>
|
||||
<ul class="ml-4">
|
||||
<li
|
||||
v-for="provider of providersForGive"
|
||||
:key="provider.identifier"
|
||||
class="list-disc ml-4"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="grow overflow-hidden">
|
||||
<a
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
@click="handleProviderClick(provider)"
|
||||
>
|
||||
an activity
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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),
|
||||
|
||||
@@ -96,50 +96,50 @@
|
||||
</div>
|
||||
|
||||
<!-- Fullfills Links -->
|
||||
<div class="mt-4">
|
||||
<!-- fullfills links for a give -->
|
||||
<div v-if="giveDetails?.fulfillsPlanHandleId">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(
|
||||
giveDetails?.fulfillsPlanHandleId || '',
|
||||
)
|
||||
"
|
||||
class="text-blue-500 mt-2 cursor-pointer"
|
||||
>
|
||||
This fulfills a bigger plan
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- fullfills links for a give -->
|
||||
<div v-if="giveDetails?.fulfillsPlanHandleId" class="mt-2">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(giveDetails?.fulfillsPlanHandleId || '')
|
||||
"
|
||||
class="text-blue-500 mt-2 cursor-pointer"
|
||||
>
|
||||
This fulfills a bigger plan
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- if there's another, it's probably fulfilling an offer, too -->
|
||||
<div
|
||||
v-if="
|
||||
giveDetails?.fulfillsType &&
|
||||
giveDetails?.fulfillsType !== 'PlanAction' &&
|
||||
giveDetails?.fulfillsHandleId
|
||||
"
|
||||
>
|
||||
<!-- router-link to /claim/ only changes URL path -->
|
||||
<router-link
|
||||
:to="
|
||||
'/claim/' +
|
||||
encodeURIComponent(giveDetails?.fulfillsHandleId || '')
|
||||
"
|
||||
class="text-blue-500 mt-2 cursor-pointer"
|
||||
>
|
||||
This fulfills
|
||||
{{
|
||||
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
||||
giveDetails?.fulfillsType || "",
|
||||
)
|
||||
}}
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
<!-- Show offer fulfillment if this give fulfills an offer -->
|
||||
<div v-if="giveDetailsOfferFulfillment?.offerHandleId">
|
||||
<!-- router-link to /claim/ only changes URL path -->
|
||||
<router-link
|
||||
:to="
|
||||
'/claim/' +
|
||||
encodeURIComponent(
|
||||
giveDetailsOfferFulfillment.offerHandleId || '',
|
||||
)
|
||||
"
|
||||
class="text-blue-500 mt-2 cursor-pointer"
|
||||
>
|
||||
This fulfills
|
||||
{{
|
||||
serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
||||
giveDetailsOfferFulfillment.offerType || "Offer",
|
||||
)
|
||||
}}
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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();
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
if (stopAsking) {
|
||||
await this.$saveSettings({
|
||||
hideRegisterPromptOnNewContact: stopAsking,
|
||||
});
|
||||
this.hideRegisterPromptOnNewContact = stopAsking;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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<void> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user