forked from trent_larson/crowd-funder-for-time-pwa
shrink the contents of the QR code so people can scan it
This commit is contained in:
@@ -83,10 +83,7 @@
|
|||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import {
|
import { createAndSubmitOffer } from "../libs/endorserServer";
|
||||||
createAndSubmitOffer,
|
|
||||||
serverMessageForUser,
|
|
||||||
} from "../libs/endorserServer";
|
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
|
|||||||
@@ -44,8 +44,7 @@ export default class TopMessage extends Vue {
|
|||||||
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
||||||
) {
|
) {
|
||||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||||
this.message =
|
this.message = "You are using prod, user " + didPrefix;
|
||||||
"You are using prod, user " + didPrefix;
|
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const deepLinkSchemas = {
|
|||||||
"user-profile": z.object({
|
"user-profile": z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
"project": z.object({
|
project: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
"onboard-meeting-setup": z.object({
|
"onboard-meeting-setup": z.object({
|
||||||
|
|||||||
@@ -882,6 +882,71 @@ export const contactToCsvLine = (contact: Contact): string => {
|
|||||||
return fields.join(",");
|
return fields.join(",");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a CSV line into a Contact object. See contactToCsvLine for the format.
|
||||||
|
* @param lineRaw - The CSV line to parse
|
||||||
|
* @returns A Contact object
|
||||||
|
*/
|
||||||
|
export const csvLineToContact = (lineRaw: string): Contact => {
|
||||||
|
// Note that Endorser Mobile puts name first, then did, etc.
|
||||||
|
let line = lineRaw.trim();
|
||||||
|
let did, publicKeyInput, seesMe, registered;
|
||||||
|
let name;
|
||||||
|
let commaPos1 = -1;
|
||||||
|
if (line.startsWith('"')) {
|
||||||
|
let doubleDoubleQuotePos = line.lastIndexOf('""') + 2;
|
||||||
|
if (doubleDoubleQuotePos === -1) {
|
||||||
|
doubleDoubleQuotePos = 1;
|
||||||
|
}
|
||||||
|
const quote2Pos = line.indexOf('"', doubleDoubleQuotePos);
|
||||||
|
if (quote2Pos > -1) {
|
||||||
|
commaPos1 = line.indexOf(",", quote2Pos);
|
||||||
|
name = line.substring(1, quote2Pos).trim();
|
||||||
|
name = name.replace(/""/g, '"');
|
||||||
|
} else {
|
||||||
|
// something is weird with one " to start, so ignore it and start after "
|
||||||
|
line = line.substring(1);
|
||||||
|
commaPos1 = line.indexOf(",");
|
||||||
|
name = line.substring(0, commaPos1).trim();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commaPos1 = line.indexOf(",");
|
||||||
|
name = line.substring(0, commaPos1).trim();
|
||||||
|
}
|
||||||
|
if (commaPos1 > -1) {
|
||||||
|
did = line.substring(commaPos1 + 1).trim();
|
||||||
|
const commaPos2 = line.indexOf(",", commaPos1 + 1);
|
||||||
|
if (commaPos2 > -1) {
|
||||||
|
did = line.substring(commaPos1 + 1, commaPos2).trim();
|
||||||
|
publicKeyInput = line.substring(commaPos2 + 1).trim();
|
||||||
|
const commaPos3 = line.indexOf(",", commaPos2 + 1);
|
||||||
|
if (commaPos3 > -1) {
|
||||||
|
publicKeyInput = line.substring(commaPos2 + 1, commaPos3).trim();
|
||||||
|
seesMe = line.substring(commaPos3 + 1).trim() == "true";
|
||||||
|
const commaPos4 = line.indexOf(",", commaPos3 + 1);
|
||||||
|
if (commaPos4 > -1) {
|
||||||
|
seesMe = line.substring(commaPos3 + 1, commaPos4).trim() == "true";
|
||||||
|
registered = line.substring(commaPos4 + 1).trim() == "true";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// help with potential mistakes while this sharing requires copy-and-paste
|
||||||
|
let publicKeyBase64 = publicKeyInput;
|
||||||
|
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
||||||
|
// it must be all hex (compressed public key), so convert
|
||||||
|
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
||||||
|
}
|
||||||
|
const newContact: Contact = {
|
||||||
|
did: did || "",
|
||||||
|
name,
|
||||||
|
publicKeyBase64,
|
||||||
|
seesMe,
|
||||||
|
registered,
|
||||||
|
};
|
||||||
|
return newContact;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for the JSON export format of database tables
|
* Interface for the JSON export format of database tables
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -81,15 +81,15 @@ export class DeepLinkHandler {
|
|||||||
string,
|
string,
|
||||||
{ name: string; paramKey?: string }
|
{ name: string; paramKey?: string }
|
||||||
> = {
|
> = {
|
||||||
"claim": { name: "claim" },
|
claim: { name: "claim" },
|
||||||
"claim-add-raw": { name: "claim-add-raw" },
|
"claim-add-raw": { name: "claim-add-raw" },
|
||||||
"claim-cert": { name: "claim-cert" },
|
"claim-cert": { name: "claim-cert" },
|
||||||
"confirm-gift": { name: "confirm-gift" },
|
"confirm-gift": { name: "confirm-gift" },
|
||||||
"did": { name: "did", paramKey: "did" },
|
did: { name: "did", paramKey: "did" },
|
||||||
"invite-one-accept": { name: "invite-one-accept" },
|
"invite-one-accept": { name: "invite-one-accept" },
|
||||||
"onboard-meeting-members": { name: "onboard-meeting-members" },
|
"onboard-meeting-members": { name: "onboard-meeting-members" },
|
||||||
"onboard-meeting-setup": { name: "onboard-meeting-setup" },
|
"onboard-meeting-setup": { name: "onboard-meeting-setup" },
|
||||||
"project": { name: "project" },
|
project: { name: "project" },
|
||||||
"user-profile": { name: "user-profile" },
|
"user-profile": { name: "user-profile" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
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";
|
||||||
@@ -117,11 +118,15 @@ import { db } from "../db/index";
|
|||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
||||||
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
||||||
|
import * as libsUtil from "../libs/util";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { setVisibilityUtil } from "../libs/endorserServer";
|
import {
|
||||||
|
CONTACT_CSV_HEADER,
|
||||||
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
|
setVisibilityUtil,
|
||||||
|
} from "../libs/endorserServer";
|
||||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||||
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
|
|
||||||
import { retrieveAccountMetadata } from "../libs/util";
|
import { retrieveAccountMetadata } from "../libs/util";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
import { parseJsonField } from "../db/databaseUtil";
|
import { parseJsonField } from "../db/databaseUtil";
|
||||||
@@ -142,7 +147,7 @@ interface IUserNameDialog {
|
|||||||
UserNameDialog,
|
UserNameDialog,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ContactQRScan extends Vue {
|
export default class ContactQRScanFull extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
$router!: Router;
|
$router!: Router;
|
||||||
|
|
||||||
@@ -151,6 +156,7 @@ export default class ContactQRScan extends Vue {
|
|||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
givenName = "";
|
givenName = "";
|
||||||
|
isRegistered = false;
|
||||||
qrValue = "";
|
qrValue = "";
|
||||||
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
||||||
|
|
||||||
@@ -172,19 +178,21 @@ export default class ContactQRScan extends Vue {
|
|||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.givenName = settings.firstName || "";
|
this.givenName = settings.firstName || "";
|
||||||
|
this.isRegistered = !!settings.isRegistered;
|
||||||
|
|
||||||
const account = await retrieveAccountMetadata(this.activeDid);
|
const account = await retrieveAccountMetadata(this.activeDid);
|
||||||
if (account) {
|
if (account) {
|
||||||
const name =
|
const name =
|
||||||
(settings.firstName || "") +
|
(settings.firstName || "") +
|
||||||
(settings.lastName ? ` ${settings.lastName}` : "");
|
(settings.lastName ? ` ${settings.lastName}` : "");
|
||||||
this.qrValue = await generateEndorserJwtUrlForAccount(
|
const publicKeyBase64 = Buffer.from(
|
||||||
account,
|
account.publicKeyHex,
|
||||||
!!settings.isRegistered,
|
"hex",
|
||||||
name,
|
).toString("base64");
|
||||||
settings.profileImageUrl || "",
|
this.qrValue =
|
||||||
false,
|
CONTACT_CSV_HEADER +
|
||||||
);
|
"\n" +
|
||||||
|
`"${name}",${account.did},${publicKeyBase64},false,${this.isRegistered}`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error initializing component:", {
|
logger.error("Error initializing component:", {
|
||||||
@@ -336,57 +344,69 @@ export default class ContactQRScan extends Vue {
|
|||||||
|
|
||||||
logger.info("Processing QR code scan result:", rawValue);
|
logger.info("Processing QR code scan result:", rawValue);
|
||||||
|
|
||||||
// Extract JWT
|
let contact: Contact;
|
||||||
const jwt = getContactJwtFromJwtUrl(rawValue);
|
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
|
||||||
if (!jwt) {
|
// Extract JWT
|
||||||
logger.warn("Invalid QR code format - no JWT found in URL");
|
const jwt = getContactJwtFromJwtUrl(rawValue);
|
||||||
|
if (!jwt) {
|
||||||
|
logger.warn("Invalid QR code format - no JWT found in URL");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid QR Code",
|
||||||
|
text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process JWT and contact info
|
||||||
|
logger.info("Decoding JWT payload from QR code");
|
||||||
|
const decodedJwt = await decodeEndorserJwt(jwt);
|
||||||
|
if (!decodedJwt?.payload?.own) {
|
||||||
|
logger.warn("Invalid JWT payload - missing 'own' field");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid Contact Info",
|
||||||
|
text: "The contact information is incomplete or invalid.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactInfo = decodedJwt.payload.own;
|
||||||
|
const did = contactInfo.did || decodedJwt.payload.iss;
|
||||||
|
if (!did) {
|
||||||
|
logger.warn("Invalid contact info - missing DID");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid Contact",
|
||||||
|
text: "The contact DID is missing.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create contact object
|
||||||
|
contact = {
|
||||||
|
did: did,
|
||||||
|
name: contactInfo.name || "",
|
||||||
|
publicKeyBase64: contactInfo.publicKeyBase64 || "",
|
||||||
|
seesMe: contactInfo.seesMe || false,
|
||||||
|
registered: contactInfo.registered || false,
|
||||||
|
};
|
||||||
|
} else if (rawValue.startsWith(CONTACT_CSV_HEADER)) {
|
||||||
|
const lines = rawValue.split(/\n/);
|
||||||
|
contact = libsUtil.csvLineToContact(lines[1]);
|
||||||
|
} else {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Invalid QR Code",
|
title: "Error",
|
||||||
text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.",
|
text: "Could not determine the type of contact info. Please try again.",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process JWT and contact info
|
|
||||||
logger.info("Decoding JWT payload from QR code");
|
|
||||||
const decodedJwt = await decodeEndorserJwt(jwt);
|
|
||||||
if (!decodedJwt?.payload?.own) {
|
|
||||||
logger.warn("Invalid JWT payload - missing 'own' field");
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Contact Info",
|
|
||||||
text: "The contact information is incomplete or invalid.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contactInfo = decodedJwt.payload.own;
|
|
||||||
const did = contactInfo.did || decodedJwt.payload.iss;
|
|
||||||
if (!did) {
|
|
||||||
logger.warn("Invalid contact info - missing DID");
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Contact",
|
|
||||||
text: "The contact DID is missing.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create contact object
|
|
||||||
const contact = {
|
|
||||||
did: did,
|
|
||||||
name: contactInfo.name || "",
|
|
||||||
email: contactInfo.email || "",
|
|
||||||
phone: contactInfo.phone || "",
|
|
||||||
company: contactInfo.company || "",
|
|
||||||
title: contactInfo.title || "",
|
|
||||||
notes: contactInfo.notes || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add contact but keep scanning
|
// Add contact but keep scanning
|
||||||
logger.info("Adding new contact to database:", {
|
logger.info("Adding new contact to database:", {
|
||||||
did: contact.did,
|
did: contact.did,
|
||||||
@@ -468,7 +488,7 @@ export default class ContactQRScan extends Vue {
|
|||||||
title: "Contact Exists",
|
title: "Contact Exists",
|
||||||
text: "This contact has already been added to your list.",
|
text: "This contact has already been added to your list.",
|
||||||
},
|
},
|
||||||
3000,
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
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 { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
@@ -174,12 +175,13 @@ import * as databaseUtil from "../db/databaseUtil";
|
|||||||
import { parseJsonField } from "../db/databaseUtil";
|
import { parseJsonField } from "../db/databaseUtil";
|
||||||
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
||||||
import {
|
import {
|
||||||
generateEndorserJwtUrlForAccount,
|
CONTACT_CSV_HEADER,
|
||||||
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
register,
|
register,
|
||||||
setVisibilityUtil,
|
setVisibilityUtil,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
||||||
import { retrieveAccountMetadata } from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
||||||
@@ -252,18 +254,19 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
!!settings.hideRegisterPromptOnNewContact;
|
!!settings.hideRegisterPromptOnNewContact;
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
|
|
||||||
const account = await retrieveAccountMetadata(this.activeDid);
|
const account = await libsUtil.retrieveAccountMetadata(this.activeDid);
|
||||||
if (account) {
|
if (account) {
|
||||||
const name =
|
const name =
|
||||||
(settings.firstName || "") +
|
(settings.firstName || "") +
|
||||||
(settings.lastName ? ` ${settings.lastName}` : "");
|
(settings.lastName ? ` ${settings.lastName}` : "");
|
||||||
this.qrValue = await generateEndorserJwtUrlForAccount(
|
const publicKeyBase64 = Buffer.from(
|
||||||
account,
|
account.publicKeyHex,
|
||||||
!!settings.isRegistered,
|
"hex",
|
||||||
name,
|
).toString("base64");
|
||||||
settings.profileImageUrl || "",
|
this.qrValue =
|
||||||
false,
|
CONTACT_CSV_HEADER +
|
||||||
);
|
"\n" +
|
||||||
|
`"${name}",${account.did},${publicKeyBase64},false,${this.isRegistered}`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error initializing component:", {
|
logger.error("Error initializing component:", {
|
||||||
@@ -274,7 +277,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Initialization Error",
|
title: "Initialization Error",
|
||||||
text: "Failed to initialize QR scanner. Please try again.",
|
text: "Failed to initialize QR renderer or scanner. Please try again.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,53 +464,68 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
|
|
||||||
logger.info("Processing QR code scan result:", rawValue);
|
logger.info("Processing QR code scan result:", rawValue);
|
||||||
|
|
||||||
// Extract JWT
|
let contact: Contact;
|
||||||
const jwt = getContactJwtFromJwtUrl(rawValue);
|
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
|
||||||
if (!jwt) {
|
const jwt = getContactJwtFromJwtUrl(rawValue);
|
||||||
logger.warn("Invalid QR code format - no JWT found in URL");
|
if (!jwt) {
|
||||||
|
logger.warn("Invalid QR code format - no JWT found in URL");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid QR Code",
|
||||||
|
text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info("Decoding JWT payload from QR code");
|
||||||
|
const decodedJwt = await decodeEndorserJwt(jwt);
|
||||||
|
|
||||||
|
// Process JWT and contact info
|
||||||
|
if (!decodedJwt?.payload?.own) {
|
||||||
|
logger.warn("Invalid JWT payload - missing 'own' field");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid Contact Info",
|
||||||
|
text: "The contact information is incomplete or invalid.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactInfo = decodedJwt.payload.own;
|
||||||
|
const did = contactInfo.did || decodedJwt.payload.iss;
|
||||||
|
if (!did) {
|
||||||
|
logger.warn("Invalid contact info - missing DID");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid Contact",
|
||||||
|
text: "The contact DID is missing.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create contact object
|
||||||
|
contact = {
|
||||||
|
did: did,
|
||||||
|
name: contactInfo.name || "",
|
||||||
|
publicKeyBase64: contactInfo.publicKeyBase64 || "",
|
||||||
|
seesMe: contactInfo.seesMe || false,
|
||||||
|
registered: contactInfo.registered || false,
|
||||||
|
};
|
||||||
|
} else if (rawValue.startsWith(CONTACT_CSV_HEADER)) {
|
||||||
|
const lines = rawValue.split(/\n/);
|
||||||
|
contact = libsUtil.csvLineToContact(lines[1]);
|
||||||
|
} else {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Invalid QR Code",
|
title: "Error",
|
||||||
text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.",
|
text: "Could not determine the type of contact info. Please try again.",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process JWT and contact info
|
|
||||||
logger.info("Decoding JWT payload from QR code");
|
|
||||||
const decodedJwt = await decodeEndorserJwt(jwt);
|
|
||||||
if (!decodedJwt?.payload?.own) {
|
|
||||||
logger.warn("Invalid JWT payload - missing 'own' field");
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Contact Info",
|
|
||||||
text: "The contact information is incomplete or invalid.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contactInfo = decodedJwt.payload.own;
|
|
||||||
const did = contactInfo.did || decodedJwt.payload.iss;
|
|
||||||
if (!did) {
|
|
||||||
logger.warn("Invalid contact info - missing DID");
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Contact",
|
|
||||||
text: "The contact DID is missing.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create contact object
|
|
||||||
const contact = {
|
|
||||||
did: did,
|
|
||||||
name: contactInfo.name || "",
|
|
||||||
notes: contactInfo.notes || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add contact but keep scanning
|
// Add contact but keep scanning
|
||||||
logger.info("Adding new contact to database:", {
|
logger.info("Adding new contact to database:", {
|
||||||
did: contact.did,
|
did: contact.did,
|
||||||
@@ -654,7 +672,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(this.qrValue)
|
.copy(this.qrValue)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// console.log("Contact URL:", this.qrValue);
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -772,7 +789,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
title: "Contact Exists",
|
title: "Contact Exists",
|
||||||
text: "This contact has already been added to your list.",
|
text: "This contact has already been added to your list.",
|
||||||
},
|
},
|
||||||
3000,
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -935,45 +935,9 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async addContactFromEndorserMobileLine(
|
private async addContactFromEndorserMobileLine(
|
||||||
line: string,
|
lineRaw: string,
|
||||||
): Promise<IndexableType> {
|
): Promise<IndexableType> {
|
||||||
// Note that Endorser Mobile puts name first, then did, etc.
|
const newContact = libsUtil.csvLineToContact(lineRaw);
|
||||||
let name = line;
|
|
||||||
let did = "";
|
|
||||||
let publicKeyInput, seesMe, registered;
|
|
||||||
const commaPos1 = line.indexOf(",");
|
|
||||||
if (commaPos1 > -1) {
|
|
||||||
name = line.substring(0, commaPos1).trim();
|
|
||||||
did = line.substring(commaPos1 + 1).trim();
|
|
||||||
const commaPos2 = line.indexOf(",", commaPos1 + 1);
|
|
||||||
if (commaPos2 > -1) {
|
|
||||||
did = line.substring(commaPos1 + 1, commaPos2).trim();
|
|
||||||
publicKeyInput = line.substring(commaPos2 + 1).trim();
|
|
||||||
const commaPos3 = line.indexOf(",", commaPos2 + 1);
|
|
||||||
if (commaPos3 > -1) {
|
|
||||||
publicKeyInput = line.substring(commaPos2 + 1, commaPos3).trim();
|
|
||||||
seesMe = line.substring(commaPos3 + 1).trim() == "true";
|
|
||||||
const commaPos4 = line.indexOf(",", commaPos3 + 1);
|
|
||||||
if (commaPos4 > -1) {
|
|
||||||
seesMe = line.substring(commaPos3 + 1, commaPos4).trim() == "true";
|
|
||||||
registered = line.substring(commaPos4 + 1).trim() == "true";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// help with potential mistakes while this sharing requires copy-and-paste
|
|
||||||
let publicKeyBase64 = publicKeyInput;
|
|
||||||
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
|
||||||
// it must be all hex (compressed public key), so convert
|
|
||||||
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
|
||||||
}
|
|
||||||
const newContact = {
|
|
||||||
did,
|
|
||||||
name,
|
|
||||||
publicKeyBase64,
|
|
||||||
seesMe,
|
|
||||||
registered,
|
|
||||||
};
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const { sql, params } = databaseUtil.generateInsertStatement(
|
const { sql, params } = databaseUtil.generateInsertStatement(
|
||||||
newContact as unknown as Record<string, unknown>,
|
newContact as unknown as Record<string, unknown>,
|
||||||
@@ -1215,7 +1179,6 @@ export default class ContactsView extends Vue {
|
|||||||
);
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
//contact.seesMe = visibility; // why doesn't it affect the UI from here?
|
//contact.seesMe = visibility; // why doesn't it affect the UI from here?
|
||||||
//console.log("Set result & seesMe", result, contact.seesMe, contact.did);
|
|
||||||
if (showSuccessAlert) {
|
if (showSuccessAlert) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -1431,10 +1394,6 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
return contact;
|
return contact;
|
||||||
});
|
});
|
||||||
// console.log(
|
|
||||||
// "Array of selected contacts:",
|
|
||||||
// JSON.stringify(selectedContacts),
|
|
||||||
// );
|
|
||||||
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
||||||
contacts: selectedContacts,
|
contacts: selectedContacts,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -298,24 +298,25 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// in parallel, make a confirmation for each selected claim and send them all to the server
|
// in parallel, make a confirmation for each selected claim and send them all to the server
|
||||||
const confirmResults: PromiseSettledResult<CreateAndSubmitClaimResult>[] = await Promise.allSettled(
|
const confirmResults: PromiseSettledResult<CreateAndSubmitClaimResult>[] =
|
||||||
this.claimsToConfirmSelected.map(async (jwtId) => {
|
await Promise.allSettled(
|
||||||
const record = this.claimsToConfirm.find(
|
this.claimsToConfirmSelected.map(async (jwtId) => {
|
||||||
(claim) => claim.id === jwtId,
|
const record = this.claimsToConfirm.find(
|
||||||
);
|
(claim) => claim.id === jwtId,
|
||||||
if (!record) {
|
);
|
||||||
return { success: false, error: "Record not found." };
|
if (!record) {
|
||||||
}
|
return { success: false, error: "Record not found." };
|
||||||
return createAndSubmitConfirmation(
|
}
|
||||||
this.activeDid,
|
return createAndSubmitConfirmation(
|
||||||
record.claim as GenericVerifiableCredential,
|
this.activeDid,
|
||||||
record.id,
|
record.claim as GenericVerifiableCredential,
|
||||||
record.handleId,
|
record.id,
|
||||||
this.apiServer,
|
record.handleId,
|
||||||
axios,
|
this.apiServer,
|
||||||
);
|
axios,
|
||||||
}),
|
);
|
||||||
);
|
}),
|
||||||
|
);
|
||||||
// check for any rejected confirmations
|
// check for any rejected confirmations
|
||||||
const confirmsSucceeded = confirmResults.filter(
|
const confirmsSucceeded = confirmResults.filter(
|
||||||
// 'fulfilled' is the status in a successful PromiseFulfilledResult
|
// 'fulfilled' is the status in a successful PromiseFulfilledResult
|
||||||
|
|||||||
Reference in New Issue
Block a user