Compare commits
10 Commits
claimview-
...
ios-qr-cod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c218c4786 | ||
|
|
5fc5b958af | ||
| 8815f36596 | |||
| 631aa468e6 | |||
| ee29b517ce | |||
| f34c567ab4 | |||
| bd072d95eb | |||
|
|
ca1190aa47 | ||
|
|
f38ec1daff | ||
|
|
ec2cab768b |
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' }"
|
:to="{ name: 'seed-backup' }"
|
||||||
:class="backupButtonClasses"
|
: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
|
Backup Identifier Seed
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
@@ -98,6 +104,12 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
isExporting = false;
|
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
|
* Notification helper for consistent notification patterns
|
||||||
* Created as a getter to ensure $notify is available when called
|
* 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)
|
* CSS classes for the backup button (router link)
|
||||||
*/
|
*/
|
||||||
get backupButtonClasses(): string {
|
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() {
|
created() {
|
||||||
this.notify = createNotifyHelpers(this.$notify);
|
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>
|
</script>
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ import GiftDetailsStep from "../components/GiftDetailsStep.vue";
|
|||||||
import { PlanData } from "../interfaces/records";
|
import { PlanData } from "../interfaces/records";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
|
||||||
|
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||||
import {
|
import {
|
||||||
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
|
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
|
||||||
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
|
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
|
||||||
@@ -411,6 +412,15 @@ export default class GiftedDialog extends Vue {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.safeNotify.success("That gift was recorded.", TIMEOUTS.VERY_LONG);
|
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) {
|
if (this.callbackOnSuccess) {
|
||||||
this.callbackOnSuccess(amount);
|
this.callbackOnSuccess(amount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', deepLinkUrl)"
|
@click="copyTextToClipboard('A link to this page', deepLinkUrl)"
|
||||||
>click here to copy this page, paste it into a message, and ask if
|
>click here to copy this page, paste it into a message, and ask if
|
||||||
they'll tell you more about the {{ roleName }}.</a
|
they'll tell you more about the {{ roleName }}.</a
|
||||||
>
|
>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
* @since 2024-12-19
|
* @since 2024-12-19
|
||||||
*/
|
*/
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from "../libs/endorserServer";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
@@ -197,19 +197,24 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
copyToClipboard(name: string, text: string) {
|
async copyTextToClipboard(name: string, text: string) {
|
||||||
useClipboard()
|
try {
|
||||||
.copy(text)
|
await copyToClipboard(text);
|
||||||
.then(() => {
|
|
||||||
this.notify.success(
|
this.notify.success(
|
||||||
NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"),
|
NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"),
|
||||||
TIMEOUTS.SHORT,
|
TIMEOUTS.SHORT,
|
||||||
);
|
);
|
||||||
});
|
} catch (error) {
|
||||||
|
this.$logAndConsole(
|
||||||
|
`Error copying ${name || "content"} to clipboard: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.notify.error(`Failed to copy ${name || "content"} to clipboard.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
this.copyToClipboard("A link to this page", this.deepLinkUrl);
|
this.copyTextToClipboard("A link to this page", this.deepLinkUrl);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import * as libsUtil from "../libs/util";
|
|||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||||
import {
|
import {
|
||||||
NOTIFY_OFFER_SETTINGS_ERROR,
|
NOTIFY_OFFER_SETTINGS_ERROR,
|
||||||
NOTIFY_OFFER_RECORDING,
|
NOTIFY_OFFER_RECORDING,
|
||||||
@@ -299,6 +300,14 @@ export default class OfferDialog extends Vue {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.notify.success(NOTIFY_OFFER_SUCCESS.message, TIMEOUTS.VERY_LONG);
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -124,6 +124,12 @@ const MIGRATIONS = [
|
|||||||
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
|
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;
|
||||||
|
`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export type Settings = {
|
|||||||
finishedOnboarding?: boolean; // the user has completed the onboarding process
|
finishedOnboarding?: boolean; // the user has completed the onboarding process
|
||||||
|
|
||||||
firstName?: string; // user's full name, may be null if unwanted for a particular account
|
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;
|
hideRegisterPromptOnNewContact?: boolean;
|
||||||
isRegistered?: boolean;
|
isRegistered?: boolean;
|
||||||
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
|
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
|
|
||||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
||||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||||
@@ -232,11 +232,19 @@ export const nameForContact = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
|
export const doCopyTwoSecRedo = async (
|
||||||
|
text: string,
|
||||||
|
fn: () => void,
|
||||||
|
): Promise<void> => {
|
||||||
fn();
|
fn();
|
||||||
useClipboard()
|
try {
|
||||||
.copy(text)
|
await copyToClipboard(text);
|
||||||
.then(() => setTimeout(fn, 2000));
|
setTimeout(fn, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
// Note: This utility function doesn't have access to notification system
|
||||||
|
// The calling component should handle error notifications
|
||||||
|
// Error is silently caught to avoid breaking the 2-second redo pattern
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ConfirmerData {
|
export interface ConfirmerData {
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -764,7 +764,7 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
|
||||||
@@ -811,6 +811,7 @@ import { logger } from "../utils/logger";
|
|||||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||||
|
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||||
import {
|
import {
|
||||||
AccountSettings,
|
AccountSettings,
|
||||||
isApiError,
|
isApiError,
|
||||||
@@ -1083,11 +1084,15 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
||||||
doCopyTwoSecRedo(text: string, fn: () => void): void {
|
async doCopyTwoSecRedo(text: string, fn: () => void): Promise<void> {
|
||||||
fn();
|
fn();
|
||||||
useClipboard()
|
try {
|
||||||
.copy(text)
|
await copyToClipboard(text);
|
||||||
.then(() => setTimeout(fn, 2000));
|
setTimeout(fn, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
|
||||||
|
this.notify.error("Failed to copy to clipboard.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleShowContactAmounts(): Promise<void> {
|
async toggleShowContactAmounts(): Promise<void> {
|
||||||
@@ -1695,6 +1700,14 @@ export default class AccountViewView extends Vue {
|
|||||||
);
|
);
|
||||||
if (success) {
|
if (success) {
|
||||||
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_SAVED);
|
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 {
|
} else {
|
||||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
|
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 { logger } from "../utils/logger";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||||
|
|
||||||
// Type guard for API responses
|
// Type guard for API responses
|
||||||
function isApiResponse(response: unknown): response is AxiosResponse {
|
function isApiResponse(response: unknown): response is AxiosResponse {
|
||||||
@@ -223,6 +224,14 @@ export default class ClaimAddRawView extends Vue {
|
|||||||
);
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.notify.success("Claim submitted.", TIMEOUTS.LONG);
|
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 {
|
} else {
|
||||||
logger.error("Got error submitting the claim:", result);
|
logger.error("Got error submitting the claim:", result);
|
||||||
this.notify.error(
|
this.notify.error(
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
title="Copy Printable Certificate Link"
|
title="Copy Printable Certificate Link"
|
||||||
aria-label="Copy printable certificate link"
|
aria-label="Copy printable certificate link"
|
||||||
@click="
|
@click="
|
||||||
copyToClipboard(
|
copyTextToClipboard(
|
||||||
'A link to the certificate page',
|
'A link to the certificate page',
|
||||||
`${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`,
|
`${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`,
|
||||||
)
|
)
|
||||||
@@ -72,7 +72,9 @@
|
|||||||
<button
|
<button
|
||||||
title="Copy Link"
|
title="Copy Link"
|
||||||
aria-label="Copy page link"
|
aria-label="Copy page link"
|
||||||
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
@click="
|
||||||
|
copyTextToClipboard('A link to this page', windowDeepLink)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<font-awesome icon="link" class="text-slate-500" />
|
<font-awesome icon="link" class="text-slate-500" />
|
||||||
</button>
|
</button>
|
||||||
@@ -399,7 +401,7 @@
|
|||||||
contacts can see more details:
|
contacts can see more details:
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
@click="copyTextToClipboard('A link to this page', windowDeepLink)"
|
||||||
>click to copy this page info</a
|
>click to copy this page info</a
|
||||||
>
|
>
|
||||||
and see if they can make an introduction. Someone is connected to
|
and see if they can make an introduction. Someone is connected to
|
||||||
@@ -422,7 +424,7 @@
|
|||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
@click="copyTextToClipboard('A link to this page', windowDeepLink)"
|
||||||
>share this page with them and ask if they'll tell you more about
|
>share this page with them and ask if they'll tell you more about
|
||||||
about the participants.</a
|
about the participants.</a
|
||||||
>
|
>
|
||||||
@@ -532,7 +534,7 @@ import * as yaml from "js-yaml";
|
|||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
import { GenericVerifiableCredential } from "../interfaces";
|
import { GenericVerifiableCredential } from "../interfaces";
|
||||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
@@ -1129,16 +1131,21 @@ export default class ClaimView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
copyToClipboard(name: string, text: string) {
|
async copyTextToClipboard(name: string, text: string) {
|
||||||
useClipboard()
|
try {
|
||||||
.copy(text)
|
await copyToClipboard(text);
|
||||||
.then(() => {
|
|
||||||
this.notify.copied(name || "That");
|
this.notify.copied(name || "That");
|
||||||
});
|
} catch (error) {
|
||||||
|
this.$logAndConsole(
|
||||||
|
`Error copying ${name || "content"} to clipboard: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.notify.error(`Failed to copy ${name || "content"} to clipboard.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
this.copyToClipboard("A link to this page", this.windowDeepLink);
|
this.copyTextToClipboard("A link to this page", this.windowDeepLink);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
|
|||||||
@@ -192,7 +192,7 @@
|
|||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
copyToClipboard(
|
copyTextToClipboard(
|
||||||
'The DID of ' + confirmerId,
|
'The DID of ' + confirmerId,
|
||||||
confirmerId,
|
confirmerId,
|
||||||
)
|
)
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
copyToClipboard(
|
copyTextToClipboard(
|
||||||
'The DID of ' + confsVisibleTo,
|
'The DID of ' + confsVisibleTo,
|
||||||
confsVisibleTo,
|
confsVisibleTo,
|
||||||
)
|
)
|
||||||
@@ -309,7 +309,9 @@
|
|||||||
contacts can see more details:
|
contacts can see more details:
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
@click="
|
||||||
|
copyTextToClipboard('A link to this page', windowLocation)
|
||||||
|
"
|
||||||
>click to copy this page info</a
|
>click to copy this page info</a
|
||||||
>
|
>
|
||||||
and see if they can make an introduction. Someone is connected to
|
and see if they can make an introduction. Someone is connected to
|
||||||
@@ -332,7 +334,9 @@
|
|||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
@click="
|
||||||
|
copyTextToClipboard('A link to this page', windowLocation)
|
||||||
|
"
|
||||||
>share this page with them and ask if they'll tell you more about
|
>share this page with them and ask if they'll tell you more about
|
||||||
about the participants.</a
|
about the participants.</a
|
||||||
>
|
>
|
||||||
@@ -360,7 +364,7 @@
|
|||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
copyToClipboard('The DID of ' + visDid, visDid)
|
copyTextToClipboard('The DID of ' + visDid, visDid)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
@@ -433,7 +437,7 @@
|
|||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
@@ -779,16 +783,21 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
* @param description - Description of copied content
|
* @param description - Description of copied content
|
||||||
* @param text - Text to copy
|
* @param text - Text to copy
|
||||||
*/
|
*/
|
||||||
copyToClipboard(description: string, text: string): void {
|
async copyTextToClipboard(description: string, text: string): Promise<void> {
|
||||||
useClipboard()
|
try {
|
||||||
.copy(text)
|
await copyToClipboard(text);
|
||||||
.then(() => {
|
|
||||||
this.notify.toast(
|
this.notify.toast(
|
||||||
NOTIFY_COPIED_TO_CLIPBOARD.title,
|
NOTIFY_COPIED_TO_CLIPBOARD.title,
|
||||||
NOTIFY_COPIED_TO_CLIPBOARD.message(description),
|
NOTIFY_COPIED_TO_CLIPBOARD.message(description),
|
||||||
TIMEOUTS.SHORT,
|
TIMEOUTS.SHORT,
|
||||||
);
|
);
|
||||||
});
|
} catch (error) {
|
||||||
|
this.$logAndConsole(
|
||||||
|
`Error copying ${description} to clipboard: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.notify.error(`Failed to copy ${description} to clipboard.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -870,7 +879,7 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
* Handles share functionality based on platform capabilities
|
* Handles share functionality based on platform capabilities
|
||||||
*/
|
*/
|
||||||
async onClickShareClaim(): Promise<void> {
|
async onClickShareClaim(): Promise<void> {
|
||||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
this.copyTextToClipboard("A link to this page", this.windowLocation);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the full details of this claim. Can you help me?",
|
text: "I'm trying to find the full details of this claim. Can you help me?",
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ import { Buffer } from "buffer/";
|
|||||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
|
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
|
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
|
||||||
@@ -144,6 +144,7 @@ import {
|
|||||||
QR_TIMEOUT_LONG,
|
QR_TIMEOUT_LONG,
|
||||||
} from "@/constants/notifications";
|
} from "@/constants/notifications";
|
||||||
import { createNotifyHelpers, NotifyFunction } from "../utils/notify";
|
import { createNotifyHelpers, NotifyFunction } from "../utils/notify";
|
||||||
|
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||||
|
|
||||||
interface QRScanResult {
|
interface QRScanResult {
|
||||||
rawValue?: string;
|
rawValue?: string;
|
||||||
@@ -195,7 +196,7 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
$router!: Router;
|
$router!: Router;
|
||||||
|
|
||||||
// Notification helper system
|
// Notification helper system
|
||||||
private notify = createNotifyHelpers(this.$notify);
|
private notify!: ReturnType<typeof createNotifyHelpers>;
|
||||||
|
|
||||||
isScanning = false;
|
isScanning = false;
|
||||||
error: string | null = null;
|
error: string | null = null;
|
||||||
@@ -263,6 +264,9 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
* Loads user settings and generates QR code for contact sharing
|
* Loads user settings and generates QR code for contact sharing
|
||||||
*/
|
*/
|
||||||
async created() {
|
async created() {
|
||||||
|
// Initialize notification helper system
|
||||||
|
this.notify = createNotifyHelpers(this.$notify);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = await this.$accountSettings();
|
const settings = await this.$accountSettings();
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
@@ -622,6 +626,15 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
*/
|
*/
|
||||||
async handleBack() {
|
async handleBack() {
|
||||||
await this.cleanupScanner();
|
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();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,6 +649,7 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
* Copies contact URL to clipboard for sharing
|
* Copies contact URL to clipboard for sharing
|
||||||
*/
|
*/
|
||||||
async onCopyUrlToClipboard() {
|
async onCopyUrlToClipboard() {
|
||||||
|
try {
|
||||||
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
||||||
this.activeDid,
|
this.activeDid,
|
||||||
)) as Account;
|
)) as Account;
|
||||||
@@ -646,26 +660,40 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
this.profileImageUrl,
|
this.profileImageUrl,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
useClipboard()
|
|
||||||
.copy(jwtUrl)
|
// Use the platform-specific ClipboardService for reliable iOS support
|
||||||
.then(() => {
|
await copyToClipboard(jwtUrl);
|
||||||
|
|
||||||
this.notify.toast(
|
this.notify.toast(
|
||||||
NOTIFY_QR_URL_COPIED.title,
|
NOTIFY_QR_URL_COPIED.title,
|
||||||
NOTIFY_QR_URL_COPIED.message,
|
NOTIFY_QR_URL_COPIED.message,
|
||||||
QR_TIMEOUT_MEDIUM,
|
QR_TIMEOUT_MEDIUM,
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error copying URL to clipboard:", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
|
this.notify.error("Failed to copy URL to clipboard.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies DID to clipboard for manual sharing
|
* Copies DID to clipboard for manual sharing
|
||||||
*/
|
*/
|
||||||
onCopyDidToClipboard() {
|
async onCopyDidToClipboard() {
|
||||||
useClipboard()
|
try {
|
||||||
.copy(this.activeDid)
|
// Use the platform-specific ClipboardService for reliable iOS support
|
||||||
.then(() => {
|
await copyToClipboard(this.activeDid);
|
||||||
|
|
||||||
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
|
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error copying DID to clipboard:", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
|
this.notify.error("Failed to copy DID to clipboard.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ import { AxiosError } from "axios";
|
|||||||
import { Buffer } from "buffer/";
|
import { Buffer } from "buffer/";
|
||||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
|
|
||||||
import { QrcodeStream } from "vue-qrcode-reader";
|
import { QrcodeStream } from "vue-qrcode-reader";
|
||||||
|
|
||||||
@@ -163,6 +164,7 @@ import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
|||||||
import { CameraState } from "@/services/QRScanner/types";
|
import { CameraState } from "@/services/QRScanner/types";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers } from "@/utils/notify";
|
import { createNotifyHelpers } from "@/utils/notify";
|
||||||
|
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||||
import {
|
import {
|
||||||
NOTIFY_QR_INITIALIZATION_ERROR,
|
NOTIFY_QR_INITIALIZATION_ERROR,
|
||||||
NOTIFY_QR_CAMERA_IN_USE,
|
NOTIFY_QR_CAMERA_IN_USE,
|
||||||
@@ -319,6 +321,15 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
|
|
||||||
async handleBack(): Promise<void> {
|
async handleBack(): Promise<void> {
|
||||||
await this.cleanupScanner();
|
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();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,7 +629,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Copy the URL to clipboard
|
// Copy the URL to clipboard
|
||||||
const { copyToClipboard } = await import("../services/ClipboardService");
|
|
||||||
await copyToClipboard(jwtUrl);
|
await copyToClipboard(jwtUrl);
|
||||||
this.notify.toast(
|
this.notify.toast(
|
||||||
NOTIFY_QR_URL_COPIED.title,
|
NOTIFY_QR_URL_COPIED.title,
|
||||||
@@ -637,7 +647,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
async onCopyDidToClipboard() {
|
async onCopyDidToClipboard() {
|
||||||
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
|
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
|
||||||
try {
|
try {
|
||||||
const { copyToClipboard } = await import("../services/ClipboardService");
|
|
||||||
await copyToClipboard(this.activeDid);
|
await copyToClipboard(this.activeDid);
|
||||||
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
|
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -738,24 +747,17 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
!contact.registered
|
!contact.registered
|
||||||
) {
|
) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.notify.confirm(
|
this.$notify(
|
||||||
"Do you want to register them?",
|
|
||||||
{
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "confirm",
|
||||||
|
title: "Register",
|
||||||
|
text: "Do you want to register them?",
|
||||||
onCancel: async (stopAsking?: boolean) => {
|
onCancel: async (stopAsking?: boolean) => {
|
||||||
if (stopAsking) {
|
await this.handleRegistrationPromptResponse(stopAsking);
|
||||||
await this.$updateSettings({
|
|
||||||
hideRegisterPromptOnNewContact: stopAsking,
|
|
||||||
});
|
|
||||||
this.hideRegisterPromptOnNewContact = stopAsking;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onNo: async (stopAsking?: boolean) => {
|
onNo: async (stopAsking?: boolean) => {
|
||||||
if (stopAsking) {
|
await this.handleRegistrationPromptResponse(stopAsking);
|
||||||
await this.$updateSettings({
|
|
||||||
hideRegisterPromptOnNewContact: stopAsking,
|
|
||||||
});
|
|
||||||
this.hideRegisterPromptOnNewContact = stopAsking;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onYes: async () => {
|
onYes: async () => {
|
||||||
await this.register(contact);
|
await this.register(contact);
|
||||||
@@ -885,6 +887,17 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
videoElement.style.transform = shouldMirror ? "scaleX(-1)" : "none";
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1003,7 +1003,7 @@
|
|||||||
<h2>Exported Data</h2>
|
<h2>Exported Data</h2>
|
||||||
<span
|
<span
|
||||||
class="text-blue-500 cursor-pointer hover:text-blue-700"
|
class="text-blue-500 cursor-pointer hover:text-blue-700"
|
||||||
@click="copyToClipboard"
|
@click="copyExportedDataToClipboard"
|
||||||
>
|
>
|
||||||
Copy to Clipboard
|
Copy to Clipboard
|
||||||
</span>
|
</span>
|
||||||
@@ -1014,7 +1014,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -1072,8 +1072,6 @@ export default class DatabaseMigration extends Vue {
|
|||||||
private exportedData: Record<string, any> | null = null;
|
private exportedData: Record<string, any> | null = null;
|
||||||
private successMessage = "";
|
private successMessage = "";
|
||||||
|
|
||||||
useClipboard = useClipboard;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed property to get the display name for a setting
|
* Computed property to get the display name for a setting
|
||||||
* Handles both live comparison data and exported JSON format
|
* Handles both live comparison data and exported JSON format
|
||||||
@@ -1133,13 +1131,11 @@ export default class DatabaseMigration extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Copies exported data to clipboard and shows success message
|
* Copies exported data to clipboard and shows success message
|
||||||
*/
|
*/
|
||||||
async copyToClipboard(): Promise<void> {
|
async copyExportedDataToClipboard(): Promise<void> {
|
||||||
if (!this.exportedData) return;
|
if (!this.exportedData) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.useClipboard().copy(
|
await copyToClipboard(JSON.stringify(this.exportedData, null, 2));
|
||||||
JSON.stringify(this.exportedData, null, 2),
|
|
||||||
);
|
|
||||||
// Use global window object properly
|
// Use global window object properly
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.alert("Copied to clipboard!");
|
window.alert("Copied to clipboard!");
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ import { logger } from "../utils/logger";
|
|||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||||
import {
|
import {
|
||||||
NOTIFY_GIFTED_DETAILS_RETRIEVAL_ERROR,
|
NOTIFY_GIFTED_DETAILS_RETRIEVAL_ERROR,
|
||||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
|
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
|
||||||
@@ -770,6 +771,15 @@ export default class GiftedDetails extends Vue {
|
|||||||
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED.message,
|
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED.message,
|
||||||
TIMEOUTS.SHORT,
|
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");
|
localStorage.removeItem("imageUrl");
|
||||||
if (this.destinationPathAfter) {
|
if (this.destinationPathAfter) {
|
||||||
(this.$router as Router).push({ path: this.destinationPathAfter });
|
(this.$router as Router).push({ path: this.destinationPathAfter });
|
||||||
|
|||||||
@@ -584,15 +584,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
// Capacitor import removed - using QRNavigationService instead
|
// Capacitor import removed - using QRNavigationService instead
|
||||||
|
|
||||||
import * as Package from "../../package.json";
|
import * as Package from "../../package.json";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { APP_SERVER } from "../constants/app";
|
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { QRNavigationService } from "@/services/QRNavigationService";
|
import { QRNavigationService } from "@/services/QRNavigationService";
|
||||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||||
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HelpView.vue - Comprehensive Help System Component
|
* HelpView.vue - Comprehensive Help System Component
|
||||||
@@ -626,8 +627,10 @@ import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
|||||||
})
|
})
|
||||||
export default class HelpView extends Vue {
|
export default class HelpView extends Vue {
|
||||||
$router!: Router;
|
$router!: Router;
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
package = Package;
|
package = Package;
|
||||||
|
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||||
commitHash = import.meta.env.VITE_GIT_HASH;
|
commitHash = import.meta.env.VITE_GIT_HASH;
|
||||||
showAlpha = false;
|
showAlpha = false;
|
||||||
showBasics = false;
|
showBasics = false;
|
||||||
@@ -640,6 +643,13 @@ export default class HelpView extends Vue {
|
|||||||
APP_SERVER = APP_SERVER;
|
APP_SERVER = APP_SERVER;
|
||||||
// Capacitor reference removed - using QRNavigationService instead
|
// Capacitor reference removed - using QRNavigationService instead
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize notification helpers
|
||||||
|
*/
|
||||||
|
created() {
|
||||||
|
this.notify = createNotifyHelpers(this.$notify);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the unnamed entity name constant
|
* Get the unnamed entity name constant
|
||||||
*/
|
*/
|
||||||
@@ -660,11 +670,15 @@ export default class HelpView extends Vue {
|
|||||||
* @param {string} text - The text to copy to clipboard
|
* @param {string} text - The text to copy to clipboard
|
||||||
* @param {Function} fn - Callback function to execute before and after copying
|
* @param {Function} fn - Callback function to execute before and after copying
|
||||||
*/
|
*/
|
||||||
doCopyTwoSecRedo(text: string, fn: () => void): void {
|
async doCopyTwoSecRedo(text: string, fn: () => void): Promise<void> {
|
||||||
fn();
|
fn();
|
||||||
useClipboard()
|
try {
|
||||||
.copy(text)
|
await copyToClipboard(text);
|
||||||
.then(() => setTimeout(fn, 2000));
|
setTimeout(fn, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
|
||||||
|
this.notify.error("Failed to copy to clipboard.", TIMEOUTS.SHORT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import ContactNameDialog from "../components/ContactNameDialog.vue";
|
import ContactNameDialog from "../components/ContactNameDialog.vue";
|
||||||
@@ -333,17 +333,27 @@ export default class InviteOneView extends Vue {
|
|||||||
return `${APP_SERVER}/deep-link/invite-one-accept/${jwt}`;
|
return `${APP_SERVER}/deep-link/invite-one-accept/${jwt}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
copyInviteAndNotify(inviteId: string, jwt: string) {
|
async copyInviteAndNotify(inviteId: string, jwt: string) {
|
||||||
useClipboard().copy(this.inviteLink(jwt));
|
try {
|
||||||
|
await copyToClipboard(this.inviteLink(jwt));
|
||||||
this.notify.success(createInviteLinkCopyMessage(inviteId), TIMEOUTS.LONG);
|
this.notify.success(createInviteLinkCopyMessage(inviteId), TIMEOUTS.LONG);
|
||||||
|
} catch (error) {
|
||||||
|
this.$logAndConsole(`Error copying invite link: ${error}`, true);
|
||||||
|
this.notify.error("Failed to copy invite link.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showInvite(inviteId: string, redeemed: boolean, expired: boolean) {
|
async showInvite(inviteId: string, redeemed: boolean, expired: boolean) {
|
||||||
useClipboard().copy(inviteId);
|
try {
|
||||||
|
await copyToClipboard(inviteId);
|
||||||
this.notify.success(
|
this.notify.success(
|
||||||
createInviteIdCopyMessage(inviteId, redeemed, expired),
|
createInviteIdCopyMessage(inviteId, redeemed, expired),
|
||||||
TIMEOUTS.LONG,
|
TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.$logAndConsole(`Error copying invite ID: ${error}`, true);
|
||||||
|
this.notify.error("Failed to copy invite ID.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -270,7 +270,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
@@ -676,12 +676,17 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
this.notify.error(message, TIMEOUTS.LONG);
|
this.notify.error(message, TIMEOUTS.LONG);
|
||||||
}
|
}
|
||||||
|
|
||||||
copyMembersLinkToClipboard() {
|
async copyMembersLinkToClipboard() {
|
||||||
useClipboard()
|
try {
|
||||||
.copy(this.onboardMeetingMembersLink())
|
await copyToClipboard(this.onboardMeetingMembersLink());
|
||||||
.then(() => {
|
|
||||||
this.notify.info(NOTIFY_MEETING_LINK_COPIED.message, TIMEOUTS.LONG);
|
this.notify.info(NOTIFY_MEETING_LINK_COPIED.message, TIMEOUTS.LONG);
|
||||||
});
|
} catch (error) {
|
||||||
|
this.$logAndConsole(
|
||||||
|
`Error copying meeting link to clipboard: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.notify.error("Failed to copy meeting link to clipboard.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -616,7 +616,7 @@ import * as serverUtil from "../libs/endorserServer";
|
|||||||
import { retrieveAccountDids } from "../libs/util";
|
import { retrieveAccountDids } from "../libs/util";
|
||||||
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
|
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
|
||||||
@@ -817,7 +817,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onCopyLinkClick() {
|
async onCopyLinkClick() {
|
||||||
const shortestProjectId = this.projectId.startsWith(
|
const shortestProjectId = this.projectId.startsWith(
|
||||||
serverUtil.ENDORSER_CH_HANDLE_PREFIX,
|
serverUtil.ENDORSER_CH_HANDLE_PREFIX,
|
||||||
)
|
)
|
||||||
@@ -825,11 +825,13 @@ export default class ProjectViewView extends Vue {
|
|||||||
: this.projectId;
|
: this.projectId;
|
||||||
// Use production URL for sharing to avoid localhost issues in development
|
// Use production URL for sharing to avoid localhost issues in development
|
||||||
const deepLink = `${APP_SERVER}/deep-link/project/${shortestProjectId}`;
|
const deepLink = `${APP_SERVER}/deep-link/project/${shortestProjectId}`;
|
||||||
useClipboard()
|
try {
|
||||||
.copy(deepLink)
|
await copyToClipboard(deepLink);
|
||||||
.then(() => {
|
|
||||||
this.notify.copied("link to this project", TIMEOUTS.SHORT);
|
this.notify.copied("link to this project", TIMEOUTS.SHORT);
|
||||||
});
|
} catch (error) {
|
||||||
|
this.$logAndConsole(`Error copying project link: ${error}`, true);
|
||||||
|
this.notify.error("Failed to copy project link.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Isn't there a better way to make this available to the template?
|
// Isn't there a better way to make this available to the template?
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ import { DateTime } from "luxon";
|
|||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
@@ -305,23 +305,21 @@ export default class QuickActionBvcEndView extends Vue {
|
|||||||
(this.$router as Router).push(route);
|
(this.$router as Router).push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
copyContactsLinkToClipboard() {
|
async copyContactsLinkToClipboard() {
|
||||||
const deepLinkUrl = `${APP_SERVER}/deep-link/did/${this.activeDid}`;
|
const deepLinkUrl = `${APP_SERVER}/deep-link/did/${this.activeDid}`;
|
||||||
useClipboard()
|
try {
|
||||||
.copy(deepLinkUrl)
|
await copyToClipboard(deepLinkUrl);
|
||||||
.then(() => {
|
|
||||||
this.notify.success(
|
this.notify.success(
|
||||||
NOTIFY_COPIED_TO_CLIPBOARD.message("Your info link"),
|
NOTIFY_COPIED_TO_CLIPBOARD.message("Your info link"),
|
||||||
TIMEOUTS.SHORT,
|
TIMEOUTS.SHORT,
|
||||||
);
|
);
|
||||||
})
|
} catch (error) {
|
||||||
.catch((error) => {
|
|
||||||
logger.error("Failed to copy to clipboard:", error);
|
logger.error("Failed to copy to clipboard:", error);
|
||||||
this.notify.error(
|
this.notify.error(
|
||||||
"Failed to copy link to clipboard. Please try again.",
|
"Failed to copy link to clipboard. Please try again.",
|
||||||
TIMEOUTS.SHORT,
|
TIMEOUTS.SHORT,
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async record() {
|
async record() {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
@@ -231,9 +231,24 @@ export default class SeedBackupView extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Reveals the seed phrase to the user
|
* Reveals the seed phrase to the user
|
||||||
* Sets showSeed to true to display the sensitive seed phrase data
|
* 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;
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -264,11 +279,15 @@ export default class SeedBackupView extends Vue {
|
|||||||
* @param text - The text to copy to clipboard
|
* @param text - The text to copy to clipboard
|
||||||
* @param fn - Callback function to execute for feedback (called twice - immediately and after 2 seconds)
|
* @param fn - Callback function to execute for feedback (called twice - immediately and after 2 seconds)
|
||||||
*/
|
*/
|
||||||
doCopyTwoSecRedo(text: string, fn: () => void) {
|
async doCopyTwoSecRedo(text: string, fn: () => void) {
|
||||||
fn();
|
fn();
|
||||||
useClipboard()
|
try {
|
||||||
.copy(text)
|
await copyToClipboard(text);
|
||||||
.then(() => setTimeout(fn, 2000));
|
setTimeout(fn, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
|
||||||
|
this.notify.error("Failed to copy to clipboard.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
|
|||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { Settings } from "@/db/tables/settings";
|
import { Settings } from "@/db/tables/settings";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
|
||||||
// Constants for magic numbers
|
// Constants for magic numbers
|
||||||
@@ -99,7 +100,7 @@ export default class ShareMyContactInfoView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const message = await this.generateContactMessage(settings, account);
|
const message = await this.generateContactMessage(settings, account);
|
||||||
await this.copyToClipboard(message);
|
await copyToClipboard(message);
|
||||||
await this.showSuccessNotifications();
|
await this.showSuccessNotifications();
|
||||||
this.navigateToContacts();
|
this.navigateToContacts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -140,14 +141,6 @@ export default class ShareMyContactInfoView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy the contact message to clipboard
|
|
||||||
*/
|
|
||||||
private async copyToClipboard(message: string): Promise<void> {
|
|
||||||
const { copyToClipboard } = await import("../services/ClipboardService");
|
|
||||||
await copyToClipboard(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show success notifications after copying
|
* Show success notifications after copying
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ import { didInfo, getHeaders } from "../libs/endorserServer";
|
|||||||
import { UserProfile } from "../libs/partnerServer";
|
import { UserProfile } from "../libs/partnerServer";
|
||||||
import { retrieveAccountDids } from "../libs/util";
|
import { retrieveAccountDids } from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { NOTIFY_PROFILE_LOAD_ERROR } from "@/constants/notifications";
|
import { NOTIFY_PROFILE_LOAD_ERROR } from "@/constants/notifications";
|
||||||
@@ -240,14 +240,16 @@ export default class UserProfileView extends Vue {
|
|||||||
* Creates a deep link to the profile and copies it to the clipboard
|
* Creates a deep link to the profile and copies it to the clipboard
|
||||||
* Shows success notification when completed
|
* Shows success notification when completed
|
||||||
*/
|
*/
|
||||||
onCopyLinkClick() {
|
async onCopyLinkClick() {
|
||||||
// Use production URL for sharing to avoid localhost issues in development
|
// Use production URL for sharing to avoid localhost issues in development
|
||||||
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
||||||
useClipboard()
|
try {
|
||||||
.copy(deepLink)
|
await copyToClipboard(deepLink);
|
||||||
.then(() => {
|
|
||||||
this.notify.copied("profile link", TIMEOUTS.STANDARD);
|
this.notify.copied("profile link", TIMEOUTS.STANDARD);
|
||||||
});
|
} catch (error) {
|
||||||
|
this.$logAndConsole(`Error copying profile link: ${error}`, true);
|
||||||
|
this.notify.error("Failed to copy profile link.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user