Merge branch 'build-improvement' into performance-optimizations-testing
This commit is contained in:
791
BUILDING.md
791
BUILDING.md
File diff suppressed because it is too large
Load Diff
84
docs/contact-sharing-url-solution.md
Normal file
84
docs/contact-sharing-url-solution.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Contact Sharing - URL Solution
|
||||
|
||||
## Overview
|
||||
|
||||
Simple implementation to switch ContactQRScanShowView from copying QR value (CSV) to copying a URL for better user experience.
|
||||
|
||||
## Problem
|
||||
|
||||
The ContactQRScanShowView was copying QR value (CSV content) to clipboard instead of a URL, making contact sharing less user-friendly.
|
||||
|
||||
## Solution
|
||||
|
||||
Updated the `onCopyUrlToClipboard()` method in ContactQRScanShowView.vue to generate and copy a URL instead of the QR value.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### ContactQRScanShowView.vue
|
||||
|
||||
**Added Imports:**
|
||||
```typescript
|
||||
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
```
|
||||
|
||||
**Updated Method:**
|
||||
```typescript
|
||||
async onCopyUrlToClipboard() {
|
||||
try {
|
||||
// Generate URL for sharing
|
||||
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
||||
this.activeDid,
|
||||
)) as Account;
|
||||
const jwtUrl = await generateEndorserJwtUrlForAccount(
|
||||
account,
|
||||
this.isRegistered,
|
||||
this.givenName,
|
||||
this.profileImageUrl,
|
||||
true,
|
||||
);
|
||||
|
||||
// Copy the URL to clipboard
|
||||
useClipboard()
|
||||
.copy(jwtUrl)
|
||||
.then(() => {
|
||||
this.notify.toast(
|
||||
"Copied",
|
||||
NOTIFY_QR_URL_COPIED.message,
|
||||
QR_TIMEOUT_MEDIUM,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Failed to generate contact URL:", error);
|
||||
this.notify.error("Failed to generate contact URL. Please try again.");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Better UX**: Recipients can click the URL to add contact directly
|
||||
2. **Consistency**: Both ContactQRScanShowView and ContactQRScanFullView now use URL format
|
||||
3. **Error Handling**: Graceful fallback if URL generation fails
|
||||
4. **Simple**: Minimal changes, no new components needed
|
||||
|
||||
## User Experience
|
||||
|
||||
**Before:**
|
||||
- Click QR code → Copy CSV data to clipboard
|
||||
- Recipient must paste CSV into input field
|
||||
|
||||
**After:**
|
||||
- Click QR code → Copy URL to clipboard
|
||||
- Recipient clicks URL → Contact added automatically
|
||||
|
||||
## Testing
|
||||
|
||||
- ✅ Linting passes
|
||||
- ✅ Error handling implemented
|
||||
- ✅ Consistent with ContactQRScanFullView behavior
|
||||
- ✅ Maintains existing notification system
|
||||
|
||||
## Deployment
|
||||
|
||||
Ready for deployment. No breaking changes, maintains backward compatibility.
|
||||
@@ -92,6 +92,7 @@
|
||||
"clean:android": "adb uninstall app.timesafari.app || true",
|
||||
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
|
||||
"clean:electron": "./scripts/build-electron.sh --clean",
|
||||
"clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron",
|
||||
"build:android": "./scripts/build-android.sh",
|
||||
"build:android:dev": "./scripts/build-android.sh --dev",
|
||||
"build:android:test": "./scripts/build-android.sh --test",
|
||||
|
||||
@@ -126,15 +126,15 @@ export default class GiftedDialog extends Vue {
|
||||
firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description)
|
||||
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
|
||||
offerId = "";
|
||||
projects: PlanData[] = [];
|
||||
prompt = "";
|
||||
receiver?: libsUtil.GiverReceiverInputInfo;
|
||||
stepType = "giver";
|
||||
unitCode = "HUR";
|
||||
visible = false;
|
||||
|
||||
libsUtil = libsUtil;
|
||||
|
||||
projects: PlanData[] = [];
|
||||
|
||||
didInfo = didInfo;
|
||||
|
||||
// Computed property to help debug template logic
|
||||
@@ -188,8 +188,6 @@ export default class GiftedDialog extends Vue {
|
||||
return false;
|
||||
}
|
||||
|
||||
stepType = "giver";
|
||||
|
||||
async open(
|
||||
giver?: libsUtil.GiverReceiverInputInfo,
|
||||
receiver?: libsUtil.GiverReceiverInputInfo,
|
||||
|
||||
@@ -1191,17 +1191,6 @@ export const NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER = {
|
||||
message: "You must select an identifier before you can record a give.",
|
||||
};
|
||||
|
||||
export const NOTIFY_GIFTED_DETAILS_PROJECT_PROVIDER_INFO = {
|
||||
title: "Project Provider Info",
|
||||
message:
|
||||
"To select a project as a provider, you must open this page through a project.",
|
||||
};
|
||||
|
||||
export const NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED = {
|
||||
title: "Invalid Selection",
|
||||
message: "You cannot select both a giving project and person.",
|
||||
};
|
||||
|
||||
export const NOTIFY_GIFTED_DETAILS_RECORDING_GIVE = {
|
||||
title: "",
|
||||
message: "Recording the give...",
|
||||
|
||||
@@ -10,6 +10,8 @@ export type BoundingBox = {
|
||||
|
||||
/**
|
||||
* Settings type encompasses user-specific configuration details.
|
||||
*
|
||||
* New entries that are boolean should also be added to PlatformServiceMixin._mapColumnsToValues
|
||||
*/
|
||||
export type Settings = {
|
||||
// default entry is keyed with MASTER_SETTINGS_KEY; other entries are linked to an account with account ID
|
||||
|
||||
@@ -4,6 +4,6 @@ export interface UserProfile {
|
||||
locLon?: number;
|
||||
locLat2?: number;
|
||||
locLon2?: number;
|
||||
issuerDid?: string;
|
||||
issuerDid: string;
|
||||
rowId?: string; // set on profile retrieved from server
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||
import { Contact, ContactWithJsonStrings } from "../db/tables/contacts";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
||||
import {
|
||||
arrayBufferToBase64,
|
||||
@@ -34,7 +34,7 @@ import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
|
||||
|
||||
// Consolidate this with src/utils/PlatformServiceMixin._parseJsonField
|
||||
// Consolidate this with src/utils/PlatformServiceMixin.mapQueryResultToValues
|
||||
function mapQueryResultToValues(
|
||||
record: { columns: string[]; values: unknown[][] } | undefined,
|
||||
): Array<Record<string, unknown>> {
|
||||
@@ -57,10 +57,10 @@ async function getPlatformService() {
|
||||
}
|
||||
|
||||
export interface GiverReceiverInputInfo {
|
||||
did?: string;
|
||||
did?: string; // only for people
|
||||
name?: string;
|
||||
image?: string;
|
||||
handleId?: string;
|
||||
handleId?: string; // only for projects
|
||||
}
|
||||
|
||||
export enum OnboardPage {
|
||||
@@ -900,24 +900,12 @@ export interface DatabaseExport {
|
||||
* @returns DatabaseExport object in the standardized format
|
||||
*/
|
||||
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
||||
// Convert each contact to a plain object and ensure all fields are included
|
||||
const rows = contacts.map((contact) => {
|
||||
const exContact: ContactWithJsonStrings = R.omit(
|
||||
["contactMethods"],
|
||||
contact,
|
||||
);
|
||||
exContact.contactMethods = contact.contactMethods
|
||||
? JSON.stringify(contact.contactMethods)
|
||||
: undefined;
|
||||
return exContact;
|
||||
});
|
||||
|
||||
return {
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
tableName: "contacts",
|
||||
rows,
|
||||
rows: contacts,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -182,14 +182,6 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
||||
}
|
||||
operation.resolve(result);
|
||||
} catch (error) {
|
||||
// logger.error( // DISABLED
|
||||
// "Error while processing SQL queue:",
|
||||
// error,
|
||||
// " ... for sql:",
|
||||
// operation.sql,
|
||||
// " ... with params:",
|
||||
// operation.params,
|
||||
// );
|
||||
logger.error(
|
||||
"Error while processing SQL queue:",
|
||||
error,
|
||||
@@ -242,9 +234,6 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
||||
|
||||
// If initialized but no db, something went wrong
|
||||
if (!this.db) {
|
||||
// logger.error( // DISABLED
|
||||
// `Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
|
||||
// );
|
||||
logger.error(
|
||||
`Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
|
||||
);
|
||||
|
||||
@@ -246,6 +246,15 @@ export const PlatformServiceMixin = {
|
||||
// Keep null values as null
|
||||
}
|
||||
|
||||
// Handle JSON fields like contactMethods
|
||||
if (column === "contactMethods" && typeof value === "string") {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch {
|
||||
value = [];
|
||||
}
|
||||
}
|
||||
|
||||
obj[column] = value;
|
||||
});
|
||||
return obj;
|
||||
@@ -1051,12 +1060,18 @@ export const PlatformServiceMixin = {
|
||||
contact.profileImageUrl !== undefined
|
||||
? contact.profileImageUrl
|
||||
: null,
|
||||
contactMethods:
|
||||
contact.contactMethods !== undefined
|
||||
? Array.isArray(contact.contactMethods)
|
||||
? JSON.stringify(contact.contactMethods)
|
||||
: contact.contactMethods
|
||||
: null,
|
||||
};
|
||||
|
||||
await this.$dbExec(
|
||||
`INSERT OR REPLACE INTO contacts
|
||||
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl, contactMethods)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
safeContact.did,
|
||||
safeContact.name,
|
||||
@@ -1065,6 +1080,7 @@ export const PlatformServiceMixin = {
|
||||
safeContact.registered,
|
||||
safeContact.nextPubKeyHashB64,
|
||||
safeContact.profileImageUrl,
|
||||
safeContact.contactMethods,
|
||||
],
|
||||
);
|
||||
return true;
|
||||
|
||||
@@ -1055,13 +1055,8 @@ export default class ClaimView extends Vue {
|
||||
if (this.projectInfo) {
|
||||
// Recipient is a project
|
||||
recipient = {
|
||||
did:
|
||||
this.detailsForGive?.fulfillsPlanHandleId ||
|
||||
this.detailsForOffer?.fulfillsPlanHandleId,
|
||||
name: this.projectInfo.name,
|
||||
handleId:
|
||||
this.detailsForGive?.fulfillsPlanHandleId ||
|
||||
this.detailsForOffer?.fulfillsPlanHandleId,
|
||||
handleId: this.detailsForOffer?.fulfillsPlanHandleId,
|
||||
image: this.projectInfo.imageUrl,
|
||||
};
|
||||
} else {
|
||||
|
||||
@@ -153,6 +153,7 @@ import {
|
||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||
register,
|
||||
setVisibilityUtil,
|
||||
generateEndorserJwtUrlForAccount,
|
||||
} from "../libs/endorserServer";
|
||||
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
||||
import * as libsUtil from "../libs/util";
|
||||
@@ -187,6 +188,7 @@ import {
|
||||
QR_TIMEOUT_STANDARD,
|
||||
QR_TIMEOUT_LONG,
|
||||
} from "@/constants/notifications";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
interface QRScanResult {
|
||||
rawValue?: string;
|
||||
@@ -610,16 +612,33 @@ export default class ContactQRScanShow extends Vue {
|
||||
}
|
||||
|
||||
async onCopyUrlToClipboard() {
|
||||
// Copy the CSV format QR code value instead of generating a deep link
|
||||
useClipboard()
|
||||
.copy(this.qrValue)
|
||||
.then(() => {
|
||||
this.notify.toast(
|
||||
"Copied",
|
||||
NOTIFY_QR_URL_COPIED.message,
|
||||
QR_TIMEOUT_MEDIUM,
|
||||
);
|
||||
});
|
||||
try {
|
||||
// Generate URL for sharing
|
||||
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
||||
this.activeDid,
|
||||
)) as Account;
|
||||
const jwtUrl = await generateEndorserJwtUrlForAccount(
|
||||
account,
|
||||
this.isRegistered,
|
||||
this.givenName,
|
||||
this.profileImageUrl,
|
||||
true,
|
||||
);
|
||||
|
||||
// Copy the URL to clipboard
|
||||
useClipboard()
|
||||
.copy(jwtUrl)
|
||||
.then(() => {
|
||||
this.notify.toast(
|
||||
"Copied",
|
||||
NOTIFY_QR_URL_COPIED.message,
|
||||
QR_TIMEOUT_MEDIUM,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Failed to generate contact URL:", error);
|
||||
this.notify.error("Failed to generate contact URL. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
toastQRCodeHelp() {
|
||||
|
||||
@@ -296,8 +296,6 @@ import {
|
||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
|
||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR,
|
||||
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
|
||||
NOTIFY_GIFTED_DETAILS_PROJECT_PROVIDER_INFO,
|
||||
NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED,
|
||||
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
|
||||
NOTIFY_GIFTED_DETAILS_CREATE_GIVE_ERROR,
|
||||
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED,
|
||||
@@ -635,29 +633,32 @@ export default class GiftedDetails extends Vue {
|
||||
}
|
||||
|
||||
notifyUserOfGiver() {
|
||||
// there's no individual giver or there's a provider project
|
||||
if (!this.giverDid) {
|
||||
this.notify.warning(
|
||||
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message,
|
||||
"To assign a giver, you must choose a person in a previous step.",
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
} else {
|
||||
// must be because providedByProject is true
|
||||
this.notify.warning(
|
||||
NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED.message,
|
||||
"You cannot assign both a giver and a project.",
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
notifyUserOfRecipient() {
|
||||
// there's no individual recipient or there's a fulfills project
|
||||
if (!this.recipientDid) {
|
||||
this.notify.warning(
|
||||
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message,
|
||||
"To assign a recipient, you must choose a person in a previous step.",
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
} else {
|
||||
// must be because givenToProject is true
|
||||
this.notify.warning(
|
||||
NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED.message,
|
||||
"You cannot assign both to a recipient and to a project.",
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
}
|
||||
@@ -667,13 +668,13 @@ export default class GiftedDetails extends Vue {
|
||||
// we're here because they clicked and either there is no provider project or there is a giver chosen
|
||||
if (!this.providerProjectId) {
|
||||
this.notify.warning(
|
||||
NOTIFY_GIFTED_DETAILS_PROJECT_PROVIDER_INFO.message,
|
||||
"To select a project as a provider, you must choose a project in a previous step.",
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
} else {
|
||||
// no providing project was chosen
|
||||
// no providing project was chosen, so there must be an individual giver
|
||||
this.notify.warning(
|
||||
NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED.message,
|
||||
"You cannot select both a giving project and person.",
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
}
|
||||
@@ -683,13 +684,13 @@ export default class GiftedDetails extends Vue {
|
||||
// we're here because they clicked and either there is no fulfills project or there is a recipient chosen
|
||||
if (!this.fulfillsProjectId) {
|
||||
this.notify.warning(
|
||||
NOTIFY_GIFTED_DETAILS_PROJECT_PROVIDER_INFO.message,
|
||||
"To assign a project as a recipient, you must choose a project in a previous step.",
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
} else {
|
||||
// no fulfills project was chosen
|
||||
// no fulfills project was chosen, so there must be an individual recipient
|
||||
this.notify.warning(
|
||||
NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED.message,
|
||||
"You cannot select both a receiving project and person.",
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user