You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							439 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							439 lines
						
					
					
						
							13 KiB
						
					
					
				| <template> | |
|   <QuickNav selected="Profile" /> | |
|   <!-- CONTENT --> | |
|   <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> | |
|     <!-- Breadcrumb --> | |
|     <div class="mb-8"> | |
|       <!-- Back --> | |
|       <div class="text-lg text-center font-light relative px-7"> | |
|         <h1 | |
|           class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" | |
|           @click="$router.back()" | |
|         > | |
|           <font-awesome icon="chevron-left" class="fa-fw" /> | |
|         </h1> | |
|       </div> | |
| 
 | |
|       <!-- Heading --> | |
|       <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4"> | |
|         Your Contact Info | |
|       </h1> | |
|       <p | |
|         v-if="!givenName" | |
|         class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4" | |
|       > | |
|         <span class="text-red">Beware!</span> | |
|         You aren't sharing your name, so quickly | |
|         <br /> | |
|         <span | |
|           class="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-1 rounded-md" | |
|           @click="() => $refs.userNameDialog.open((name) => (givenName = name))" | |
|         > | |
|           click here to set it for them. | |
|         </span> | |
|       </p> | |
|     </div> | |
|     <UserNameDialog ref="userNameDialog" /> | |
|  | |
|     <div | |
|       v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)" | |
|       class="text-center" | |
|       @click="onCopyUrlToClipboard()" | |
|     > | |
|       <!-- | |
|         Play with display options: https://qr-code-styling.com/ | |
|         See docs: https://www.npmjs.com/package/qr-code-generator-vue3 | |
|       --> | |
|       <QRCodeVue3 | |
|         :value="qrValue" | |
|         :corners-square-options="{ type: 'extra-rounded' }" | |
|         :dots-options="{ type: 'square' }" | |
|         class="flex justify-center" | |
|       /> | |
|       <span> | |
|         Click the QR code to copy your contact info to your clipboard. | |
|       </span> | |
|     </div> | |
|     <div v-else-if="activeDid" class="text-center"> | |
|       <!-- Not an ETHR DID so force them to paste it. (Passkey Peer DIDs are too big.) --> | |
|       <span class="text-blue-500" @click="onCopyDidToClipboard()"> | |
|         Click here to copy your DID to your clipboard. | |
|       </span> | |
|       <span> | |
|         Then give it to them so they can paste it in their list of People. | |
|       </span> | |
|     </div> | |
|     <div v-else class="text-center"> | |
|       You have no identitifiers yet, so | |
|       <router-link | |
|         :to="{ name: 'start' }" | |
|         class="bg-blue-500 text-white px-1.5 py-1 rounded-md" | |
|       > | |
|         create your identifier. | |
|       </router-link> | |
|       <br /> | |
|       If you don't that first, these contacts won't see your activity. | |
|     </div> | |
|  | |
|     <div class="text-center"> | |
|       <h1 class="text-4xl text-center font-light pt-6">Scan Contact Info</h1> | |
|       <qrcode-stream @detect="onScanDetect" @error="onScanError" /> | |
|       <span> | |
|         If you do not see a scanning camera window here, check your camera | |
|         permissions. | |
|       </span> | |
|     </div> | |
|   </section> | |
| </template> | |
|  | |
| <script lang="ts"> | |
| import { AxiosError } from "axios"; | |
| import QRCodeVue3 from "qr-code-generator-vue3"; | |
| import { Component, Vue } from "vue-facing-decorator"; | |
| import { QrcodeStream } from "vue-qrcode-reader"; | |
| import { useClipboard } from "@vueuse/core"; | |
| 
 | |
| import QuickNav from "../components/QuickNav.vue"; | |
| import UserNameDialog from "../components/UserNameDialog.vue"; | |
| import { NotificationIface } from "../constants/app"; | |
| import { db, retrieveSettingsForActiveAccount } from "../db/index"; | |
| import { Contact } from "../db/tables/contacts"; | |
| import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; | |
| import { getContactJwtFromJwtUrl } from "../libs/crypto"; | |
| import { | |
|   generateEndorserJwtUrlForAccount, | |
|   isDid, | |
|   register, | |
|   setVisibilityUtil, | |
| } from "../libs/endorserServer"; | |
| import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc"; | |
| import { retrieveAccountMetadata } from "../libs/util"; | |
| import { Router } from "vue-router"; | |
| 
 | |
| @Component({ | |
|   components: { | |
|     QrcodeStream, | |
|     QRCodeVue3, | |
|     QuickNav, | |
|     UserNameDialog, | |
|   }, | |
| }) | |
| export default class ContactQRScanShow extends Vue { | |
|   $notify!: (notification: NotificationIface, timeout?: number) => void; | |
|   $router!: Router; | |
| 
 | |
|   activeDid = ""; | |
|   apiServer = ""; | |
|   givenName = ""; | |
|   hideRegisterPromptOnNewContact = false; | |
|   isRegistered = false; | |
|   qrValue = ""; | |
| 
 | |
|   ETHR_DID_PREFIX = ETHR_DID_PREFIX; | |
| 
 | |
|   async created() { | |
|     const settings = await retrieveSettingsForActiveAccount(); | |
|     this.activeDid = settings.activeDid || ""; | |
|     this.apiServer = settings.apiServer || ""; | |
|     this.givenName = settings.firstName || ""; | |
|     this.hideRegisterPromptOnNewContact = | |
|       !!settings.hideRegisterPromptOnNewContact; | |
|     this.isRegistered = !!settings.isRegistered; | |
| 
 | |
|     const account = await retrieveAccountMetadata(this.activeDid); | |
|     if (account) { | |
|       const name = | |
|         (settings.firstName || "") + | |
|         (settings.lastName ? ` ${settings.lastName}` : ""); // lastName is deprecated, pre v 0.1.3 | |
|  | |
|       this.qrValue = await generateEndorserJwtUrlForAccount( | |
|         account, | |
|         !!settings.isRegistered, | |
|         name, | |
|         settings.profileImageUrl, | |
|         false, | |
|       ); | |
|     } | |
|   } | |
| 
 | |
|   danger(message: string, title: string = "Error", timeout = 5000) { | |
|     this.$notify( | |
|       { | |
|         group: "alert", | |
|         type: "danger", | |
|         title: title, | |
|         text: message, | |
|       }, | |
|       timeout, | |
|     ); | |
|   } | |
| 
 | |
|   /** | |
|    * | |
|    * @param content is the result of a QR scan, an array with one item with a rawValue property | |
|    */ | |
|   // Unfortunately, there are not typescript definitions for the qrcode-stream component yet. | |
|   // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
|   async onScanDetect(content: any) { | |
|     const url = content[0]?.rawValue; | |
|     if (url) { | |
|       let newContact: Contact; | |
|       try { | |
|         const jwt = getContactJwtFromJwtUrl(url); | |
|         if (!jwt) { | |
|           this.$notify( | |
|             { | |
|               group: "alert", | |
|               type: "danger", | |
|               title: "No Contact Info", | |
|               text: "The contact info could not be parsed.", | |
|             }, | |
|             3000, | |
|           ); | |
|           return; | |
|         } | |
|         const { payload } = decodeEndorserJwt(jwt); | |
|         newContact = { | |
|           did: payload.own.did || payload.iss, // ".own.did" is reliable as of v 0.3.49 | |
|           name: payload.own.name, | |
|           nextPubKeyHashB64: payload.own.nextPublicEncKeyHash, | |
|           profileImageUrl: payload.own.profileImageUrl, | |
|           publicKeyBase64: payload.own.publicEncKey, | |
|           registered: payload.own.registered, | |
|         }; | |
|         if (!newContact.did) { | |
|           this.danger("There is no DID.", "Incomplete Contact"); | |
|           return; | |
|         } | |
|         if (!isDid(newContact.did)) { | |
|           this.danger("The DID must begin with 'did:'", "Invalid DID"); | |
|           return; | |
|         } | |
|       } catch (e) { | |
|         console.error("Error parsing QR info:", e); | |
|         this.danger("Could not parse the QR info.", "Read Error"); | |
|         return; | |
|       } | |
| 
 | |
|       try { | |
|         await db.open(); | |
|         await db.contacts.add(newContact); | |
| 
 | |
|         let addedMessage; | |
|         if (this.activeDid) { | |
|           await this.setVisibility(newContact, true); | |
|           newContact.seesMe = true; // didn't work inside setVisibility | |
|           addedMessage = | |
|             "They were added, and your activity is visible to them."; | |
|         } else { | |
|           addedMessage = "They were added."; | |
|         } | |
|         this.$notify( | |
|           { | |
|             group: "alert", | |
|             type: "success", | |
|             title: "Contact Added", | |
|             text: addedMessage, | |
|           }, | |
|           3000, | |
|         ); | |
| 
 | |
|         if (this.isRegistered) { | |
|           if (!this.hideRegisterPromptOnNewContact && !newContact.registered) { | |
|             setTimeout(() => { | |
|               this.$notify( | |
|                 { | |
|                   group: "modal", | |
|                   type: "confirm", | |
|                   title: "Register", | |
|                   text: "Do you want to register them?", | |
|                   onCancel: async (stopAsking: boolean) => { | |
|                     if (stopAsking) { | |
|                       await db.settings.update(MASTER_SETTINGS_KEY, { | |
|                         hideRegisterPromptOnNewContact: stopAsking, | |
|                       }); | |
|                       this.hideRegisterPromptOnNewContact = stopAsking; | |
|                     } | |
|                   }, | |
|                   onNo: async (stopAsking: boolean) => { | |
|                     if (stopAsking) { | |
|                       await db.settings.update(MASTER_SETTINGS_KEY, { | |
|                         hideRegisterPromptOnNewContact: stopAsking, | |
|                       }); | |
|                       this.hideRegisterPromptOnNewContact = stopAsking; | |
|                     } | |
|                   }, | |
|                   onYes: async () => { | |
|                     await this.register(newContact); | |
|                   }, | |
|                   promptToStopAsking: true, | |
|                 }, | |
|                 -1, | |
|               ); | |
|             }, 500); | |
|           } | |
|         } | |
|       } catch (e) { | |
|         console.error("Error saving contact info:", e); | |
|         this.$notify( | |
|           { | |
|             group: "alert", | |
|             type: "danger", | |
|             title: "Contact Error", | |
|             text: "Could not save contact info. Check if it already exists.", | |
|           }, | |
|           5000, | |
|         ); | |
|       } | |
|     } else { | |
|       this.$notify( | |
|         { | |
|           group: "alert", | |
|           type: "danger", | |
|           title: "Invalid Contact QR Code", | |
|           text: "No QR code detected with contact information.", | |
|         }, | |
|         5000, | |
|       ); | |
|     } | |
|   } | |
| 
 | |
|   async setVisibility(contact: Contact, visibility: boolean) { | |
|     const result = await setVisibilityUtil( | |
|       this.activeDid, | |
|       this.apiServer, | |
|       this.axios, | |
|       db, | |
|       contact, | |
|       visibility, | |
|     ); | |
|     if (result.error) { | |
|       this.danger(result.error as string, "Error Setting Visibility"); | |
|     } else if (!result.success) { | |
|       console.error("Got strange result from setting visibility:", result); | |
|     } | |
|   } | |
| 
 | |
|   async register(contact: Contact) { | |
|     this.$notify( | |
|       { | |
|         group: "alert", | |
|         type: "toast", | |
|         text: "", | |
|         title: "Registration submitted...", | |
|       }, | |
|       1000, | |
|     ); | |
| 
 | |
|     try { | |
|       const regResult = await register( | |
|         this.activeDid, | |
|         this.apiServer, | |
|         this.axios, | |
|         contact, | |
|       ); | |
|       if (regResult.success) { | |
|         contact.registered = true; | |
|         db.contacts.update(contact.did, { registered: true }); | |
| 
 | |
|         this.$notify( | |
|           { | |
|             group: "alert", | |
|             type: "success", | |
|             title: "Registration Success", | |
|             text: | |
|               (contact.name || "That unnamed person") + " has been registered.", | |
|           }, | |
|           5000, | |
|         ); | |
|       } else { | |
|         this.$notify( | |
|           { | |
|             group: "alert", | |
|             type: "danger", | |
|             title: "Registration Error", | |
|             text: | |
|               (regResult.error as string) || | |
|               "Something went wrong during registration.", | |
|           }, | |
|           5000, | |
|         ); | |
|       } | |
|     } catch (error) { | |
|       console.error("Error when registering:", error); | |
|       let userMessage = "There was an error."; | |
|       const serverError = error as AxiosError; | |
|       if (serverError) { | |
|         if (serverError.response?.data?.error?.message) { | |
|           userMessage = serverError.response.data.error.message; | |
|         } else if (serverError.message) { | |
|           userMessage = serverError.message; // Info for the user | |
|         } else { | |
|           userMessage = JSON.stringify(serverError.toJSON()); | |
|         } | |
|       } else { | |
|         userMessage = error as string; | |
|       } | |
|       // Now set that error for the user to see. | |
|       this.$notify( | |
|         { | |
|           group: "alert", | |
|           type: "danger", | |
|           title: "Registration Error", | |
|           text: userMessage, | |
|         }, | |
|         5000, | |
|       ); | |
|     } | |
|   } | |
| 
 | |
|   // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
|   onScanError(error: any) { | |
|     console.error("Scan was invalid:", error); | |
|     this.$notify( | |
|       { | |
|         group: "alert", | |
|         type: "danger", | |
|         title: "Invalid Scan", | |
|         text: "The scan was invalid.", | |
|       }, | |
|       5000, | |
|     ); | |
|   } | |
| 
 | |
|   onCopyUrlToClipboard() { | |
|     //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing | |
|     useClipboard() | |
|       .copy(this.qrValue) | |
|       .then(() => { | |
|         // console.log("Contact URL:", this.qrValue); | |
|         this.$notify( | |
|           { | |
|             group: "alert", | |
|             type: "toast", | |
|             title: "Copied", | |
|             text: "Contact URL was copied to clipboard.", | |
|           }, | |
|           2000, | |
|         ); | |
|       }); | |
|   } | |
| 
 | |
|   onCopyDidToClipboard() { | |
|     //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing | |
|     useClipboard() | |
|       .copy(this.activeDid) | |
|       .then(() => { | |
|         this.$notify( | |
|           { | |
|             group: "alert", | |
|             type: "info", | |
|             title: "Copied", | |
|             text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.", | |
|           }, | |
|           5000, | |
|         ); | |
|       }); | |
|   } | |
| } | |
| </script>
 | |
| 
 |