Browse Source

feat & doc: automatically set visibility & alert about registration, alert to help onboard (and refine docs & tasks)

pull/82/head
Trent Larson 12 months ago
parent
commit
c391385500
  1. 161
      README.md
  2. 5
      project.task.yaml
  3. 15
      src/views/ContactQRScanShowView.vue
  4. 60
      src/views/ContactsView.vue
  5. 103
      src/views/HelpView.vue
  6. 2
      src/views/HomeView.vue

161
README.md

@ -26,31 +26,13 @@ npm run build
npm run lint npm run lint
``` ```
## Tests
### Web-push
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
### Test key contents ## Tests
See [this page](openssl_signing_console.rst)
### Register new user on test server ### Register new user on test server
New users require registration. This can be done with a claim payload like this
by an existing user:
```
const vcClaim = {
"@context": "https://schema.org",
"@type": "RegisterAction",
agent: { identifier: identity0.did },
object: SERVICE_ID,
participant: { identifier: newIdentity.did },
};
```
On the test server, User #0 has rights to register others, so you can start On the test server, User #0 has rights to register others, so you can start
playing one of two ways: playing one of two ways:
@ -66,14 +48,37 @@ playing one of two ways:
### Create multiple identifiers ### Create multiple identifiers
Go to /start and create or import a new one. Then switch identifiers on the bottom of the Your Identity page. Under the "Your Identity" screen, click "Advanced", click "Switch Identity / No Identity", then "Add Another Identity...".
### Create keys with alternate tools ### Create keys with alternate tools
See [this page](openssl_signing_console.rst) [This page](openssl_signing_console.rst) is a tool to create a JWT from a locally-generated keypair.
### Web-push
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
### Manual walk-through
- Clear the browser cache for localhost to be a new user.
- On each page, verify the messaging.
- On the home page, see message prompting to generate an ID.
- As User #0 in another browser, add a give record. (See User #0 details above.)
- With the new user on the home page, see the feed that shows users without names.
- As the new user on the contacts page, add a contact.
- On the discovery page, check that they can see projects.
- Choose a search area and see items show nearby.
- Generate an ID.
- On the home page, check that it now prompts them to get registered.
- On the account page, check that they see messages on limits.
- Register the ID from User #0.
- As the new user on the home page, check that they can now record a gift.
Also that they see a hint on the user name in the feed (since user #0 is visible to them).
- On the contacts page, check that they cannot register someone else yet.
- As the new user, add User #0 to contacts.
- On the home page, see the name of user #0 info in the feed.
- Walk through the functions on each page.
### Customize Vue configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
## Scenarios ## Scenarios
@ -90,8 +95,8 @@ See [Configuration Reference](https://cli.vuejs.org/config/).
### Clear data & restart ### Clear data & restart
Clear cache for localhost, then go to http://localhost:8080/start Clear the browser cache for localhost.
(because it'll generate a new one automatically if you start on the `/account` page).
@ -102,110 +107,10 @@ Clear cache for localhost, then go to http://localhost:8080/start
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`. * Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue. They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
``` * [Customize Vue configuration](https://cli.vuejs.org/config/).
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
// Import an existing ID
export const importAndStoreIdentifier = async (mnemonic: string, mnemonicPassword: string, toLowercase: boolean, previousIdentifiers: Array<IIdentifier>) => {
// just to get rid of variability that might cause an error
mnemonic = mnemonic.trim().toLowerCase()
/**
// an approach I pieced together
// requires: yarn add elliptic
// ... plus:
// const EC = require('elliptic').ec
// const secp256k1 = new EC('secp256k1')
//
const keyHex: string = bip39.mnemonicToEntropy(mnemonic)
// returns a KeyPair from the elliptic.ec library
const keyPair = secp256k1.keyFromPrivate(keyHex, 'hex')
// this code is from did-provider-eth createIdentifier
const privateHex = keyPair.getPrivate('hex')
const publicHex = keyPair.getPublic('hex')
const address = didJwt.toEthereumAddress(publicHex)
**/
/**
// from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234
// ... which almost works but the didJwt.toEthereumAddress is wrong
// requires: yarn add bip32
// ... plus: import * as bip32 from 'bip32'
//
const seed: Buffer = await bip39.mnemonicToSeed(mnemonic)
const root = bip32.fromSeed(seed)
const node = root.derivePath(UPORT_ROOT_DERIVATION_PATH)
const privateHex = node.privateKey.toString("hex")
const publicHex = node.publicKey.toString("hex")
const address = didJwt.toEthereumAddress('0x' + publicHex)
**/
/**
// from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234
// requires: yarn add @ethersproject/hdnode
// ... plus: import { HDNode } from '@ethersproject/hdnode'
**/
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic)
const rootNode: HDNode = hdnode.derivePath(UPORT_ROOT_DERIVATION_PATH)
const privateHex = rootNode.privateKey.substring(2) // original starts with '0x'
const publicHex = rootNode.publicKey.substring(2) // original starts with '0x'
let address = rootNode.address
const prevIds = previousIdentifiers || [];
if (toLowercase) {
const foundEqual = R.find(
(id) => utility.rawAddressOfDid(id.did) === address,
prevIds
)
if (foundEqual) {
// They're trying to create a lowercase version of one that exists in normal case.
// (We really should notify the user.)
appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a normal-case version of the DID since a regular version exists."}))
} else {
address = address.toLowerCase()
}
} else {
// They're not trying to convert to lowercase.
const foundLower = R.find((id) =>
utility.rawAddressOfDid(id.did) === address.toLowerCase(),
prevIds
)
if (foundLower) {
// They're trying to create a normal case version of one that exists in lowercase.
// (We really should notify the user.)
appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a lowercase version of the DID since a lowercase version exists."}))
address = address.toLowerCase()
}
}
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... derived keys and address..."}))
const newId = newIdentifier(address, publicHex, privateHex, UPORT_ROOT_DERIVATION_PATH)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... created new ID..."}))
// awaiting because otherwise the UI may not see that a mnemonic was created
const savedId = await storeIdentifier(newId, mnemonic, mnemonicPassword)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... stored new ID..."}))
return savedId
}
// Create a totally new ID
export const createAndStoreIdentifier = async (mnemonicPassword) => {
// This doesn't give us the entropy/seed.
//const id = await agent.didManagerCreate()
const entropy = crypto.randomBytes(32)
const mnemonic = bip39.entropyToMnemonic(entropy)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... generated mnemonic..."}))
return importAndStoreIdentifier(mnemonic, mnemonicPassword, false, [])
}
```
## Kudos ### Kudos
Gifts make the world go 'round! Gifts make the world go 'round!

5
project.task.yaml

@ -5,7 +5,12 @@ tasks:
- 40 notifications : - 40 notifications :
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew - push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
- .5 if there is no area search selected, make "anywhere" search the default
- .5 allow to manage their notifications even without an identity
- .1 put a "back" button at the top of the start page
- .5 bug - on the discover page, enter a search term and search and see a duplicate project show at the end of the list
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s) - 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
- 01 Make a new contact able to see me by default.
- Home Feed & Quick Give screen : - Home Feed & Quick Give screen :
- 01 save the feed-viewed status in settings storage ("afterQuery") - 01 save the feed-viewed status in settings storage ("afterQuery")

15
src/views/ContactQRScanShowView.vue

@ -2,10 +2,23 @@
<QuickNav selected="Profile"></QuickNav> <QuickNav selected="Profile"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- 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()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
Your Contact Info Your Contact Info
</h1> </h1>
</div>
<!-- <!--
Play with display options: https://qr-code-styling.com/ Play with display options: https://qr-code-styling.com/
@ -137,7 +150,7 @@ export default class ContactQRScanShow extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanDetect(content: any) { onScanDetect(content: any) {
if (content[0]?.rawValue) { if (content[0]?.rawValue) {
console.log("onDetect", content[0].rawValue); //console.log("onDetect", content[0].rawValue);
localStorage.setItem("contactEndorserUrl", content[0].rawValue); localStorage.setItem("contactEndorserUrl", content[0].rawValue);
this.$router.push({ name: "contacts" }); this.$router.push({ name: "contacts" });
} else { } else {

60
src/views/ContactsView.vue

@ -113,7 +113,7 @@
<button <button
v-if="contact.seesMe" v-if="contact.seesMe"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md" class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="setVisibility(contact, false)" @click="setVisibility(contact, false, true)"
title="They can see you" title="They can see you"
> >
<fa icon="eye" class="fa-fw" /> <fa icon="eye" class="fa-fw" />
@ -121,7 +121,7 @@
<button <button
v-else v-else
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md" class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="setVisibility(contact, true)" @click="setVisibility(contact, true, true)"
title="They cannot see you" title="They cannot see you"
> >
<fa icon="eye-slash" class="fa-fw" /> <fa icon="eye-slash" class="fa-fw" />
@ -137,7 +137,7 @@
<button <button
@click="register(contact)" @click="register(contact)"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md" class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
> >
<fa <fa
v-if="contact.registered" v-if="contact.registered"
@ -155,7 +155,7 @@
<button <button
@click="deleteContact(contact)" @click="deleteContact(contact)"
class="text-sm uppercase bg-red-600 text-white px-2 py-1.5 rounded-md" class="text-sm uppercase bg-red-600 text-white ml-24 px-2 py-1.5 rounded-md"
title="Delete" title="Delete"
> >
<fa icon="trash-can" class="fa-fw" /> <fa icon="trash-can" class="fa-fw" />
@ -531,6 +531,7 @@ export default class ContactsView extends Vue {
); );
return; return;
} }
newContact.seesMe = true; // since we will immediately set that on the server
return db.contacts return db.contacts
.add(newContact) .add(newContact)
.then(() => { .then(() => {
@ -539,12 +540,28 @@ export default class ContactsView extends Vue {
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""), (a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts, allContacts,
); );
this.setVisibility(newContact, true, false);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Contact added", title: "Contact Added",
text: newContact.name + " was added.", text:
newContact.name +
" was added, and your activity is visible to them.",
},
-1,
);
// putting this last so that it shows on the top
this.$notify(
{
group: "alert",
type: "info",
title: "New User?",
text:
"If " +
newContact.name +
" is a new user, be sure to register them.",
}, },
-1, -1,
); );
@ -556,7 +573,7 @@ export default class ContactsView extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Contact Not Added", title: "Contact Not Added",
text: "An error prevented importing.", text: "An error prevented this import.",
}, },
-1, -1,
); );
@ -566,11 +583,13 @@ export default class ContactsView extends Vue {
async deleteContact(contact: Contact) { async deleteContact(contact: Contact) {
if ( if (
confirm( confirm(
"Are you sure you want to delete " + "You should first make sure that your activity is no longer visible to them." +
" Note that this only deletes them from your contacts on this device." +
" \n\nAre you sure you want to remove " +
this.nameForDid(this.contacts, contact.did) + this.nameForDid(this.contacts, contact.did) +
" with DID " + " with DID " +
contact.did + contact.did +
" ?", " from your contact list?",
) )
) { ) {
await db.open(); await db.open();
@ -692,7 +711,11 @@ export default class ContactsView extends Vue {
} }
} }
async setVisibility(contact: Contact, visibility: boolean) { async setVisibility(
contact: Contact,
visibility: boolean,
showSuccessAlert: boolean,
) {
const url = const url =
this.apiServer + this.apiServer +
"/api/report/" + "/api/report/" +
@ -704,6 +727,21 @@ export default class ContactsView extends Vue {
try { try {
const resp = await this.axios.post(url, payload, { headers }); const resp = await this.axios.post(url, payload, { headers });
if (resp.status === 200) { if (resp.status === 200) {
if (showSuccessAlert) {
this.$notify(
{
group: "alert",
type: "success",
title: "Visibility Set",
text:
this.nameForDid(this.contacts, contact.did) +
" can " +
(visibility ? "" : "not ") +
"see your activity.",
},
-1,
);
}
contact.seesMe = visibility; contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility }); db.contacts.update(contact.did, { seesMe: visibility });
} else { } else {
@ -756,7 +794,7 @@ export default class ContactsView extends Vue {
{ {
group: "alert", group: "alert",
type: "info", type: "info",
title: "Refreshed", title: "Visibility Refreshed",
text: text:
this.nameForContact(contact, true) + this.nameForContact(contact, true) +
" can " + " can " +

103
src/views/HelpView.vue

@ -2,10 +2,23 @@
<QuickNav selected="Profile"></QuickNav> <QuickNav selected="Profile"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- 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()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Help Help
</h1> </h1>
</div>
<div> <div>
<p> <p>
@ -15,7 +28,7 @@
<h2 class="text-xl font-semibold">What is the philosophy here?</h2> <h2 class="text-xl font-semibold">What is the philosophy here?</h2>
<p> <p>
We are building networks of people who want to grow a gifting society. We are building networks of people who want to grow a giving society.
First of all, you can record ways you've seen people give, and that First of all, you can record ways you've seen people give, and that
leaves a permanent record -- one that came from you, and the recipient leaves a permanent record -- one that came from you, and the recipient
can prove it was for them. This is personally gratifying, but it extends can prove it was for them. This is personally gratifying, but it extends
@ -36,22 +49,39 @@
the control; this app gives you the control. the control; this app gives you the control.
</p> </p>
<h2 class="text-xl font-semibold">How do I take my first action?</h2> <h2 class="text-xl font-semibold">How do I get started?</h2>
<p> <p>
You need someone to register you -- usually the person who told you You need someone to register you -- usually the person who told you
about this app, on the Contacts about this app, on the Contacts
<fa icon="circle-user" class="fa-fw" /> page. After they register you, <fa icon="users" class="fa-fw" /> page. After they register you, you can
you can select any contact on the home page (or "anonymous") and record select any contact on the home page (or "anonymous") and record your
your appreciation for... whatever. The main goal is to record what appreciation for... whatever. The main goal is to record what people
people have given you, to grow gifting economies. Each claim is recorded have given you, to grow giving economies. Each claim is recorded on a
on a custom ledger. The day after being registered, you'll be able to custom ledger. The day after being registered, you'll be able to able to
able to register others; later, you can create projects, too. register others; later, you can create projects, too.
</p> </p>
<p> <p>
Note that there are limits to how many others each person can register, Note that there are limits to how many others each person can register,
so you may have to wait. so you may have to wait.
</p> </p>
<h2 class="text-xl font-semibold">How do I add someone else?</h2>
<p>
<button class="text-blue-500" @click="showOnboardInfo">
Click here to show an alert with the steps.
</button>
To start scanning, go
<router-link class="text-blue-500" to="/contact-qr">here.</router-link>
</p>
<p>
If they are not nearby to scan QR codes, tell them to copy their ID from
their Identity <fa icon="circle-user" class="fa-fw" /> page, which
typically starts with "did:ethr:...", and send it to you. Go to the
Contacts <fa icon="users" class="fa-fw" /> page and enter that into the
top form. To add a name, put a comma and then their name; to add their
public key, put another comma followed by the key.
</p>
<h2 class="text-xl font-semibold">How do I backup all my data?</h2> <h2 class="text-xl font-semibold">How do I backup all my data?</h2>
<p> <p>
There are two sets of data to backup: the identifier secrets and the There are two sets of data to backup: the identifier secrets and the
@ -113,27 +143,20 @@
How do I restore my other (non-identifier-secret) data? How do I restore my other (non-identifier-secret) data?
</h2> </h2>
<ul class="list-disc list-inside"> <ul class="list-disc list-inside">
<li>Make sure you have your backup file (above), then contact us.</li> <li>
Make sure you have your backup file (above), then contact us with
your interest. This is functionality that has to be written, and
your interest will help us prioritize it, but there are also manual
ways to restore your data.
</li>
</ul> </ul>
</div> </div>
<h2 class="text-xl font-semibold">
How do I add someone to my contacts?
</h2>
<p>
Tell them to copy their ID, which typically starts with "did:ethr:...",
and send it to you. Go to the Contacts
<fa icon="circle-user" class="fa-fw" /> page and enter that into the top
form. You may add a name by adding a comma followed by their name; you
may also add their public key by adding another comma followed by the
key.
</p>
<h2 class="text-xl font-semibold">How do I create another identity?</h2> <h2 class="text-xl font-semibold">How do I create another identity?</h2>
<p> <p>
Before doing this, note that it is an advanced feature that affects Before doing this, note that it is an advanced feature that affects
functionality (eg. the words "Alt ID" next to results, backup features) functionality (eg. the words "Alt ID" next to results, backup features)
so beware if you think that may cause confusion. You can so beware. You can
<router-link to="start" class="text-blue-500"> <router-link to="start" class="text-blue-500">
create another identity here. create another identity here.
</router-link> </router-link>
@ -151,10 +174,10 @@
<fa icon="eye-slash" class="fa-fw" />. <fa icon="eye-slash" class="fa-fw" />.
</p> </p>
<p> <p>
Sometimes the reason you don't see something is because the search time Sometimes the reason you don't see something is because the search
is limited. Go to the bottom and make sure to load all the data on a results are limited. Go to the bottom and make sure to load all the data
list. If you still don't see it, try a search or view on a different on a list. If you still don't see it, try a search or view on a
page. different page.
</p> </p>
<h2 class="text-xl font-semibold">What is your privacy policy?</h2> <h2 class="text-xl font-semibold">What is your privacy policy?</h2>
@ -171,11 +194,11 @@
</p> </p>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
For any other questions, including remove your data: For any other questions, including removing your data:
</h2> </h2>
<p> <p>
Contact us through Contact us at
<a href="https://communitycred.org">CommunityCred.org</a>. <a mailto="info@TimeSafari.app">info@TimeSafari.app</a>
</p> </p>
</div> </div>
</section> </section>
@ -186,8 +209,30 @@ import * as Package from "../../package.json";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class Help extends Vue { export default class Help extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
package = Package; package = Package;
showOnboardInfo() {
this.$notify(
{
group: "alert",
type: "info",
title: "Onboard Someone",
// If you edit this, check that the numbers still line up on the side in the alert (on mobile, preferably).
text: "1) Check that they've entered their name. 2) Go to the scanning page via the Identity page and then the through the QR icon at the top, and then scan and register them. 3) Have them go to that page and scan you.",
},
-1,
);
}
} }
</script> </script>

2
src/views/HomeView.vue

@ -29,7 +29,7 @@
<div v-else> <div v-else>
<!-- activeDid && isRegistered --> <!-- activeDid && isRegistered -->
<h2 class="text-xl font-bold">Record a Gift</h2> <h2 class="text-xl font-bold">Record Something Given</h2>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5"> <ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openDialog()"> <li @click="openDialog()">

Loading…
Cancel
Save