From 0a314934b83f005366dbc87584662b75016c98d5 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 13 Dec 2024 13:27:22 -0700 Subject: [PATCH] add invite-one-accept screen dedicated to accepting invitations --- .env.development | 2 + README.md | 7 +- src/App.vue | 23 +---- src/libs/endorserServer.ts | 2 + src/main.ts | 9 +- src/router/index.ts | 5 ++ src/views/AccountViewView.vue | 45 ++++++---- src/views/ContactsView.vue | 21 +++-- src/views/HomeView.vue | 3 +- src/views/InviteOneAcceptView.vue | 138 ++++++++++++++++++++++++++++++ src/views/InviteOneView.vue | 29 ++++--- 11 files changed, 220 insertions(+), 64 deletions(-) create mode 100644 src/views/InviteOneAcceptView.vue diff --git a/.env.development b/.env.development index fc025fa..6da6399 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,5 @@ # I tried and failed to set things here with vue-cli-service but # things may be more reliable with vite so let's try again. + +VITE_APP_SERVER=http://localhost:8080 diff --git a/README.md b/README.md index 6a6e144..5d5c7f7 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,12 @@ npm run test-all * Run the correct build: * Staging + + (Let's replace this with a .env.development or .env.staging file.) + + The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there. + ``` -# (Let's replace this with a .env.development or .env.staging file.) -# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there. TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build ``` diff --git a/src/App.vue b/src/App.vue index 50249b2..4778077 100644 --- a/src/App.vue +++ b/src/App.vue @@ -309,28 +309,6 @@ - -
-
-
-

- Something has gone very wrong. We'd appreciate if you'd - contact us and let us know how you got here. Thank you! -

- -
-
-
@@ -398,6 +376,7 @@ export default class App extends Vue { return true; } + // clone in order to get only the properties and allow stringify to work const serverSubscription = { ...subscription, }; diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index ed44c39..e937d5a 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -1070,6 +1070,7 @@ export async function generateEndorserJwtForAccount( contactInfo.own.profileImageUrl = profileImageUrl; } + // Add the next key -- but note that it makes the QR more detailed if (includeNextKeyIfDerived && account?.mnemonic && account?.derivationPath) { const newDerivPath = nextDerivationPath(account.derivationPath as string); const nextPublicHex = deriveAddress( @@ -1082,6 +1083,7 @@ export async function generateEndorserJwtForAccount( Buffer.from(nextPublicEncKeyHash).toString("base64"); contactInfo.own.nextPublicEncKeyHash = nextPublicEncKeyHashBase64; } + const vcJwt = await createEndorserJwtForDid(account.did, contactInfo); const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION; diff --git a/src/main.ts b/src/main.ts index cd386a3..f4e7b15 100644 --- a/src/main.ts +++ b/src/main.ts @@ -172,11 +172,14 @@ function setupGlobalErrorHandler(app: VueApp) { info: string, ) => { console.error( - "Ouch! Global Error Handler. Info:", - info, + "Ouch! Global Error Handler.", "Error:", err, - "Instance:", + "- Error toString:", + err.toString(), + "- Info:", + info, + "- Instance:", instance, ); // Want to show a nice notiwind notification but can't figure out how. diff --git a/src/router/index.ts b/src/router/index.ts index bffa5b4..d01be86 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -144,6 +144,11 @@ const routes: Array = [ name: "invite-one", component: () => import("../views/InviteOneView.vue"), }, + { + path: "/invite-one-accept/:jwt?", + name: "InviteOneAcceptView", + component: () => import("@/views/InviteOneAcceptView.vue"), + }, { path: "/new-activity", name: "new-activity", diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 1298f97..1a6ffd1 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -243,7 +243,7 @@ {{ notifyingNewActivityTime.replace(" ", " ") }} - Troubleshoot your notification setup. + Troubleshoot your notifications. @@ -918,9 +918,29 @@ export default class AccountViewView extends Vue { // Initialize component state with values from the database or defaults await this.initializeState(); await this.processIdentity(); + } catch (error) { + // this can happen when running automated tests in dev mode because notifications don't work + console.error( + "Telling user to clear cache at page create because:", + error, + ); + // this sometimes gives different information on the error + console.error( + "To repeat with concatenated error: telling user to clear cache at page create because: " + + error, + ); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error Loading Profile", + text: "See the Help page about errors with your personal data.", + }, + -1, + ); + } - this.passkeyExpirationDescription = tokenExpiryTimeDescription(); - + try { /** * Beware! I've seen where this "ready" never resolves. */ @@ -937,26 +957,17 @@ export default class AccountViewView extends Vue { * Beware! I've seen where we never get to this point because "ready" never resolves. */ } catch (error) { - // this can happen when running automated tests in dev mode because notifications don't work - console.error( - "Telling user to clear cache at page create because:", - error, - ); - // this sometimes gives different information on the error - console.error( - "To repeat with concatenated error: telling user to clear cache at page create because: " + - error, - ); this.$notify( { group: "alert", - type: "danger", - title: "Error Loading Profile", - text: "See the Help page about errors with your personal data.", + type: "warning", + title: "Cannot Set Notifications", + text: "This browser does not support notifications. Try Chrome or Safari, or other suggestions on the 'Troubleshoot your notifications' page.", }, - -1, + 3000, ); } + this.passkeyExpirationDescription = tokenExpiryTimeDescription(); } beforeUnmount() { diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 4655007..d841e47 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -440,7 +440,7 @@ export default class ContactsView extends Vue { this.$notify( { group: "alert", - type: "warning", + type: "danger", title: "Blank Invite", text: "The invite was not included, which can happen when your iOS device cuts off the link. Try pasting the full link into a browser.", }, @@ -474,29 +474,36 @@ export default class ContactsView extends Vue { 3000, ); + // wait for a second before continuing so they see the registration message + await new Promise((resolve) => setTimeout(resolve, 1000)); + // now add the inviter as a contact + // (similar code is in InviteOneAcceptView.vue) const payload: JWTPayload = decodeEndorserJwt(importedInviteJwt).payload; const registration = payload as VerifiableCredential; (this.$refs.contactNameDialog as ContactNameDialog).open( "Who Invited You?", "", - (name) => { - // not doing await on purpose, so that they always see the onboarding - this.addContact({ + async (name) => { + await this.addContact({ did: registration.vc.credentialSubject.agent.identifier, name: name, registered: true, }); + // wait for a second before continuing so they see the user-added message + await new Promise((resolve) => setTimeout(resolve, 1000)); this.showOnboardingInfo(); }, - () => { - // not doing await on purpose, so that they always see the onboarding - this.addContact({ + async () => { + // on cancel, will still add the contact + await this.addContact({ did: registration.vc.credentialSubject.agent.identifier, name: "(person who invited you)", registered: true, }); + // wait for a second before continuing so they see the user-added message + await new Promise((resolve) => setTimeout(resolve, 1000)); this.showOnboardingInfo(); }, ); diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 8e4de44..9318a20 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -73,7 +73,8 @@

- Loading… + + Loading…

diff --git a/src/views/InviteOneAcceptView.vue b/src/views/InviteOneAcceptView.vue new file mode 100644 index 0000000..f8ea042 --- /dev/null +++ b/src/views/InviteOneAcceptView.vue @@ -0,0 +1,138 @@ + + + diff --git a/src/views/InviteOneView.vue b/src/views/InviteOneView.vue index ca3c479..82543b0 100644 --- a/src/views/InviteOneView.vue +++ b/src/views/InviteOneView.vue @@ -99,7 +99,7 @@ {{ invite.notes }} - {{ invite.expiresAt.substring(0, 10) }} + {{ invite.redeemedAt ? "" : invite.expiresAt.substring(0, 10) }} {{ invite.redeemedAt?.substring(0, 10) }} @@ -137,8 +137,9 @@ import ContactNameDialog from "@/components/ContactNameDialog.vue"; import QuickNav from "@/components/QuickNav.vue"; import TopMessage from "@/components/TopMessage.vue"; import InviteDialog from "@/components/InviteDialog.vue"; -import { APP_SERVER, NotificationIface } from "@/constants/app"; -import { db, retrieveSettingsForActiveAccount } from "@/db"; +import { APP_SERVER, AppString, NotificationIface } from "@/constants/app"; +import { db, retrieveSettingsForActiveAccount } from "@/db/index"; +import { Contact } from "@/db/tables/contacts"; import { createInviteJwt, getHeaders } from "@/libs/endorserServer"; interface Invite { @@ -159,7 +160,7 @@ export default class InviteOneView extends Vue { invites: Invite[] = []; activeDid: string = ""; apiServer: string = ""; - contactsRedeemed = {}; + contactsRedeemed: { [key: string]: Contact } = {}; isRegistered: boolean = false; showAppleWarning = false; @@ -178,12 +179,12 @@ export default class InviteOneView extends Vue { ); this.invites = response.data.data; - const baseContacts = await db.contacts.toArray(); + const baseContacts: Contact[] = await db.contacts.toArray(); for (const invite of this.invites) { const contact = baseContacts.find( (contact) => contact.did === invite.redeemedBy, ); - if (contact) { + if (contact && invite.redeemedBy) { this.contactsRedeemed[invite.redeemedBy] = contact; } } @@ -209,14 +210,16 @@ export default class InviteOneView extends Vue { getTruncatedRedeemedBy(redeemedBy: string | null): string { if (!redeemedBy) return ""; if (this.contactsRedeemed[redeemedBy]) { - return this.contactsRedeemed[redeemedBy].name; + return ( + this.contactsRedeemed[redeemedBy].name || AppString.NO_CONTACT_NAME + ); } if (redeemedBy.length <= 19) return redeemedBy; return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`; } inviteLink(jwt: string): string { - return APP_SERVER + "/contacts?inviteJwt=" + jwt; + return APP_SERVER + "/invite-one-accept/" + jwt; } copyInviteAndNotify(inviteId: string, jwt: string) { @@ -251,7 +254,8 @@ export default class InviteOneView extends Vue { ); } - lookForErrorAndNotify(error, title: string, defaultMessage: string) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lookForErrorAndNotify(error: any, title: string, defaultMessage: string) { console.error(title, "-", error); let message = defaultMessage; if (error.response && error.response.data && error.response.data.error) { @@ -301,14 +305,15 @@ export default class InviteOneView extends Vue { { inviteJwt: inviteJwt, notes: notes }, { headers }, ); - this.invites.push({ + const newInvite = { inviteIdentifier: inviteIdentifier, expiresAt: expiresAt, jwt: inviteJwt, notes: notes, redeemedAt: null, redeemedBy: null, - }); + }; + this.invites = [newInvite, ...this.invites]; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this.lookForErrorAndNotify( @@ -321,7 +326,7 @@ export default class InviteOneView extends Vue { ); } - addNewContact(did) { + addNewContact(did: string) { (this.$refs.contactNameDialog as ContactNameDialog).open( "To Whom Did You Send The Invite?", "Their name will be added to your contact list.",