Compare commits

...

28 Commits

Author SHA1 Message Date
a12d7fcc1b refactor task list 2023-12-04 20:02:09 -07:00
69c60e5426 change verbiage from "project" to "idea" 2023-12-04 19:55:57 -07:00
4806acc30e increase max characters for project description 2023-12-04 19:51:29 -07:00
1127d7079b remove outdated check, refactor tasks 2023-12-04 19:42:04 -07:00
0bbadfec6d add contact import by URL, add error notification, refine tasks 2023-12-04 19:21:03 -07:00
276d8b2f19 refine tasks & an error message 2023-12-04 17:27:36 -07:00
a7fbbbd4cd fix more paths where there may be no ID 2023-12-04 15:54:03 -07:00
a8d362c14d don't show note about registering if this user isn't registered 2023-12-04 13:36:51 -07:00
ce5933f645 remove visibility to contact operations where there is no activeDid 2023-12-04 13:29:16 -07:00
5cbf917ada don't show non-message to user; fix API server setting; misc doc & task stuff 2023-12-04 09:34:27 -07:00
7335412145 revert type complaint, which is opposite from previous suggestion, which 8-S 2023-12-04 09:31:24 -07:00
feea1a1d3b Merge pull request 'allow to customize the push-server for testing' (#80) from set-push-server into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#80
2023-12-04 10:59:22 -05:00
7f4d31a79c Merge branch 'master' into set-push-server 2023-12-04 08:55:39 -07:00
4041a7d08e more commentary, including for blank values for the user 2023-12-02 23:15:50 -07:00
681d949098 update web push servers to the domains we're using 2023-12-02 15:35:44 -07:00
3bf8fd0c22 rename "push" to "webPush" for future-proofing 2023-12-02 15:28:32 -07:00
fa41fb3415 enhance documentation 2023-12-02 15:13:56 -07:00
6dbfc5f77d Merge pull request 'A cleaner attempt to merge' (#87) from service-worker-final into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#87
2023-12-01 22:36:35 -05:00
1b9ae96006 fix linting that caused failures 2023-12-01 11:06:50 -07:00
Matthew Raymer
4dd5664462 Fix exit from loops 2023-12-01 07:12:13 -05:00
Matthew Raymer
7d6a45061d A few missing configurations 2023-12-01 06:50:17 -05:00
Matthew Raymer
3b32c2b156 Some updates after a quick test run 2023-12-01 05:02:17 -05:00
Matthew Aaron Raymer
1ee6203f4c Small package update 2023-12-01 17:14:17 +08:00
Matthew Aaron Raymer
d93299c352 Update worker dependencies 2023-12-01 17:04:14 +08:00
Matthew Aaron Raymer
9aea7a576d Merging the workflow 2023-12-01 17:03:19 +08:00
714bb169fa Merge pull request 'fix keyword search to work for both local and everywhere searches' (#86) from searching into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#86
2023-12-01 01:28:30 -05:00
ee6a344daf doc: add a guess for the states of the notifications 2023-11-12 19:03:39 -07:00
65a5edf26b allow to customize the push-server for testing 2023-11-12 11:35:36 -07:00
24 changed files with 10361 additions and 5369 deletions

View File

@@ -8,8 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- Web push notifications
## [0.1.3] - 2023.11 ## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde
### Added ### Added
- Contact name editing - Contact name editing
### Changed ### Changed

View File

@@ -13,6 +13,11 @@ npm install
npm run serve npm run serve
``` ```
### Lints and fixes files
```
npm run lint
```
### Compiles and minifies for production ### Compiles and minifies for production
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",` If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
@@ -21,11 +26,7 @@ If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js,
npm run build npm run build
``` ```
### Lints and fixes files ... then copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js.
```
npm run lint
```
@@ -84,16 +85,18 @@ For your own web-push tests, change the 'vapid' URL in App.vue, and install apps
- Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard. - Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard.
- Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this this seed phrase: - Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this this seed phrase:
`seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control` `rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).) (Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
- Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them. - Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them.
- Click on the "Registration Unknown" button and register that person to be able to make claims as them. - Click on the "Registration Unknown" button and register that person to be able to make claims as them.
### Clear data & restart ### Clear/Reset data & restart
Clear the browser cache for localhost. * Clear cache for localhost.
* Unregister service worker (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers` or `about:debugging`).
* Clear notification permission (in Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search).

View File

@@ -72,13 +72,13 @@
"@vue/cli-service": "~5.0.8", "@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
"eslint": "^8.48.0", "eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.17.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"postcss": "^8.4.29", "postcss": "^8.4.29",
"prettier": "^3.0.3", "prettier": "^3.1.0",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "~5.2.2" "typescript": "~5.2.2"
} }

View File

@@ -1,47 +1,40 @@
tasks: tasks:
- don't show "Give" & "Offer" on project screen if they don't have an identifier
- allow some gives even if they aren't registered
- in endorser-push-server - mount folder for persistent sqlite DB outside of container
- 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
- extract private_key_hex in py-push-server webpush.py
- lock down regenerate_vapid endpoint (so only we admins can do it on demand)
- remove sleep in py-push-server app.py
- .5 allow to manage their notifications even without an identity
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
- .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui - .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
- .5 Add infinite scroll to gifts on the home page - .5 Add infinite scroll to gifts on the home page
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
- .1 when creating a plan, select location and then make sure you can deselect on Android
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164 assignee:trent
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
- fix cert generation (since it didn't happen automatically for Nov 30)
- Discuss whether the remaining tasks are worthwhile before MVP release. - Discuss whether the remaining tasks are worthwhile before MVP release.
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
- .5 make a VC details page, or link to endorser.ch (including confirmations)
- 01 allow download of each VC (& confirmations, to show that they actually own their data)
- .3 check that Android shows "back" buttons on screens without bottom tray
- .1 Make give description text box into something that expands as they type? - .1 Make give description text box into something that expands as they type?
- 04 allow user to download claims, mine + ones I can see about me from others - 04 allow user to download claims, mine + ones I can see about me from others
- .5 customize favicon assignee-group:ui - .5 customize favicon assignee-group:ui
- .2 Show a warning if both giver and recipient are the same (but still allow?) - .2 Show a warning if both giver and recipient are the same (but still allow?)
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui - 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
- .5 Display a more appealing confirmation on the map when erasing the marker - .5 Display a more appealing confirmation on the map when erasing the marker
- .5 make a VC details page, or link to endorser.ch
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
- .5 include the hash of the latest commit on help page next to version (maybe Trent's git-hash branch) - .5 include the hash of the latest commit on help page next to version (maybe Trent's git-hash branch)
- .5 remove references to localStorage for projectId (now that it's pulling from the path) - .5 remove references to localStorage for projectId (now that it's pulling from the path)
- bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent - bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too) - make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
- allow download of each VC (to show that they can actually own their data) - allow some gives even if they aren't registered
- switch some checks for activeDid to check for isRegistered
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
- .5 fix cert generation on server (since it didn't happen automatically for Nov 30)
- contacts v+ : - contacts v+ :
- 01 Import all the non-sensitive data (ie. contacts & settings). - 01 Import all the non-sensitive data (ie. contacts & settings).
- .2 show error to user when adding a duplicate contact - .2 show error to user when adding a duplicate contact
- 01 parse input more robustly (with CSV lib and not commas) - 01 parse input more robustly (with CSV lib and not commas)
- stats v1 : - stats v1 :
- 01 show numeric stats - 01 show numeric stats
- 04 show different graphic for projects vs people (gnome?) on world - 04 show different graphic for projects vs people (gnome?) on world
@@ -51,6 +44,7 @@ tasks:
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie) - 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- Release Minimum Viable Product : - Release Minimum Viable Product :
- generate new webpush.db entries, data/webpush.db private_key_hex & subscription_info & vapid_claims email
- .5 deploy endorser.ch server above Dec 1 (to get plan searches by names as well as descriptions) - .5 deploy endorser.ch server above Dec 1 (to get plan searches by names as well as descriptions)
- 08 thorough testing for errors & edge cases - 08 thorough testing for errors & edge cases
- 01 ensure ability to recover server remotely, and add redundant access - 01 ensure ability to recover server remotely, and add redundant access
@@ -64,6 +58,7 @@ tasks:
- .5 show seed phrase in a QR code for transfer to another device - .5 show seed phrase in a QR code for transfer to another device
- .5 on DiscoverView, switch to a filter UI (eg. just from friend - .5 on DiscoverView, switch to a filter UI (eg. just from friend
- .5 don't show "Offer" on project screen if they aren't registered
- 24 Move to Vite - 24 Move to Vite
- 32 accept images for projects - 32 accept images for projects

View File

@@ -262,52 +262,158 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component } from "vue-facing-decorator";
import axios from "axios"; import axios from "axios";
interface ServiceWorkerMessage {
type: string;
data: string;
}
interface ServiceWorkerResponse {
// Define the properties and their types
success: boolean;
message?: string;
}
// Example interface for error
interface ErrorResponse {
message: string;
// Other properties as needed
}
interface VapidResponse {
data: {
vapidKey: string;
};
}
import { AppString } from "@/constants/app";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component @Component
export default class App extends Vue { export default class App extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
b64 = ""; b64 = "";
mounted() {
axios async mounted() {
.get("https://timesafari-pwa.anomalistlabs.com/web-push/vapid") try {
.then((response) => { await db.open();
this.b64 = response.data.vapidKey; const settings = await db.settings.get(MASTER_SETTINGS_KEY);
console.log(this.b64); let pushUrl: string = AppString.DEFAULT_PUSH_SERVER;
}) if (settings?.webPushServer) {
.catch((error) => { pushUrl = settings.webPushServer;
console.error("API error", error); }
});
await axios
.get(pushUrl + "/web-push/vapid")
.then((response: VapidResponse) => {
this.b64 = response.data?.vapidKey || "";
console.log("Got vapid key:", this.b64);
navigator.serviceWorker.addEventListener("controllerchange", () => {
console.log("New service worker is now controlling the page");
});
});
if (!this.b64) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notifications",
text: "Could not set notifications.",
},
-1,
);
}
} catch (error) {
console.error("Got an error initializing notifications:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notifications",
text: "Got an error setting notifications.",
},
-1,
);
}
}
private sendMessageToServiceWorker(
message: ServiceWorkerMessage,
): Promise<unknown> {
return new Promise((resolve, reject) => {
if (navigator.serviceWorker.controller) {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event: MessageEvent) => {
if (event.data.error) {
reject(event.data.error as ErrorResponse);
} else {
resolve(event.data as ServiceWorkerResponse);
}
};
navigator.serviceWorker.controller.postMessage(message, [
messageChannel.port2,
]);
} else {
reject("Service worker controller not available");
}
});
} }
private askPermission(): Promise<NotificationPermission> { private askPermission(): Promise<NotificationPermission> {
// Check if Notifications are supported if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
return Promise.reject("Service worker not available.");
}
const secret = localStorage.getItem("secret");
if (!secret) {
return Promise.reject("No secret found.");
}
return this.sendSecretToServiceWorker(secret)
.then(() => this.checkNotificationSupport())
.then(() => this.requestNotificationPermission())
.catch((error) => Promise.reject(error));
}
private sendSecretToServiceWorker(secret: string): Promise<void> {
const message: ServiceWorkerMessage = {
type: "SEND_LOCAL_DATA",
data: secret,
};
return this.sendMessageToServiceWorker(message).then((response) => {
console.log("Response from service worker:", response);
});
}
private checkNotificationSupport(): Promise<void> {
if (!("Notification" in window)) { if (!("Notification" in window)) {
alert("This browser does not support notifications."); alert("This browser does not support notifications.");
return Promise.reject("This browser does not support notifications."); return Promise.reject("This browser does not support notifications.");
} }
// Check existing permissions
if (Notification.permission === "granted") { if (Notification.permission === "granted") {
return Promise.resolve("granted"); return Promise.resolve();
} }
return Promise.resolve();
}
// Request permission private requestNotificationPermission(): Promise<NotificationPermission> {
return new Promise((resolve, reject) => { return Notification.requestPermission().then((permission) => {
const permissionResult = Notification.requestPermission((result) => { if (permission !== "granted") {
resolve(result);
});
if (permissionResult) {
permissionResult.then(resolve, reject);
}
}).then((permissionResult) => {
console.log("Permission result:", permissionResult);
if (permissionResult !== "granted") {
alert("We need notification permission to provide certain features."); alert("We need notification permission to provide certain features.");
return Promise.reject("We weren't granted permission."); throw new Error("We weren't granted permission.");
} }
return permission;
return permissionResult;
}); });
} }
@@ -366,34 +472,49 @@ export default class App extends Vue {
return outputArray; return outputArray;
} }
// The subscribeToPush method
private subscribeToPush(): Promise<void> { private subscribeToPush(): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if ("serviceWorker" in navigator && "PushManager" in window) { if (!("serviceWorker" in navigator && "PushManager" in window)) {
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
const options: PushSubscriptionOptions = {
userVisibleOnly: true,
applicationServerKey: applicationServerKey,
};
console.log(options);
navigator.serviceWorker.ready
.then((registration) => {
return registration.pushManager.subscribe(options);
})
.then((subscription) => {
console.log("Push subscription successful:", subscription);
resolve();
})
.catch((error) => {
console.error("Push subscription failed:", error, options);
reject(error);
});
} else {
const errorMsg = "Push messaging is not supported"; const errorMsg = "Push messaging is not supported";
console.warn(errorMsg); console.warn(errorMsg);
reject(new Error(errorMsg)); return reject(new Error(errorMsg));
} }
if (Notification.permission !== "granted") {
const errorMsg = "Notification permission not granted";
console.warn(errorMsg);
return reject(new Error(errorMsg));
}
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
const options: PushSubscriptionOptions = {
userVisibleOnly: true,
applicationServerKey: applicationServerKey,
};
navigator.serviceWorker.ready
.then((registration) => {
return registration.pushManager.subscribe(options);
})
.then((subscription) => {
console.log("Push subscription successful:", subscription);
resolve();
})
.catch((error) => {
console.error(
"Subscription or server communication failed:",
error,
options,
);
// Inform the user about the issue
alert(
"We encountered an issue setting up push notifications. " +
"If you wish to revoke notification permissions, please do so in your browser settings.",
);
reject(error);
});
}); });
} }

View File

@@ -4,20 +4,27 @@
* See also ../libs/veramo/setup.ts * See also ../libs/veramo/setup.ts
*/ */
export enum AppString { export enum AppString {
APP_NAME = "TimeSafari", APP_NAME = "Time Safari",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch", PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch", TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000", LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER, DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app",
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
DEFAULT_PUSH_SERVER = TEST1_PUSH_SERVER,
} }
/** /**
* See notiwind package * The possible values for "group" and "type" are in App.vue.
* From the notiwind package
*/ */
export interface NotificationIface { export interface NotificationIface {
group: string; group: string; // "alert" | "modal"
type: string; // "toast" | "info" | "success" | "warning" | "danger" type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string; title: string;
text: string; text: string;

View File

@@ -45,5 +45,8 @@ db.on("populate", () => {
db.settings.add({ db.settings.add({
id: MASTER_SETTINGS_KEY, id: MASTER_SETTINGS_KEY,
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER, apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
// remember that things you add from now on aren't automatically in the DB for old users
webPushServer: AppString.DEFAULT_PUSH_SERVER,
}); });
}); });

View File

@@ -20,9 +20,9 @@ export type Settings = {
lastViewedClaimId?: string; // Last viewed claim ID lastViewedClaimId?: string; // Last viewed claim ID
lastNotifiedClaimId?: string; // Last notified claim ID lastNotifiedClaimId?: string; // Last notified claim ID
isRegistered?: boolean; isRegistered?: boolean;
webPushServer?: string; // Web Push server URL
// Array of named search boxes defined by bounding boxes // Array of named search boxes defined by bounding boxes
searchBoxes?: Array<{ searchBoxes?: Array<{
name: string; name: string;
bbox: BoundingBox; bbox: BoundingBox;

View File

@@ -3,7 +3,7 @@
import { register } from "register-service-worker"; import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
register(`${process.env.BASE_URL}service-worker.js`, { register("/additional-scripts.js", {
ready() { ready() {
console.log( console.log(
"App is being served from cache by a service worker.\n" + "App is being served from cache by a service worker.\n" +

View File

@@ -324,30 +324,71 @@
<fa icon="floppy-disk" class="fa-fw" color="white"></fa> <fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button> </button>
<button <button
class="px-4 rounded bg-slate-200 border border-slate-400" class="px-3 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.PROD_ENDORSER_API_SERVER)" @click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
> >
Use Prod Use Prod
</button> </button>
<button <button
class="px-4 rounded bg-slate-200 border border-slate-400" class="px-3 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.TEST_ENDORSER_API_SERVER)" @click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
> >
Use Test Use Test
</button> </button>
<button <button
class="px-4 rounded bg-slate-200 border border-slate-400" class="px-3 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.LOCAL_ENDORSER_API_SERVER)" @click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
> >
Use Local Use Local
</button> </button>
</div> </div>
<div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">
Notification Push Server
</h2>
<input
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
v-model="webPushServerInput"
/>
<button
v-if="webPushServerInput != webPushServer"
class="px-4 rounded bg-red-500 border border-slate-400"
@click="onClickSavePushServer()"
>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
>
Use Prod
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.TEST1_PUSH_SERVER"
>
Use Test 1
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.TEST2_PUSH_SERVER"
>
Use Test 2
</button>
</div>
<span class="px-4 text-sm" v-if="!webPushServerInput">
When that setting is blank, this app will use the default web push
server URL:
{{ AppConstants.DEFAULT_PUSH_SERVER }}
</span>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError, AxiosRequestConfig } from "axios";
import "dexie-export-import"; import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
@@ -381,7 +422,7 @@ interface IAccount {
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
Constants = AppString; AppConstants = AppString;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
@@ -392,6 +433,8 @@ export default class AccountViewView extends Vue {
numAccounts = 0; numAccounts = 0;
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
webPushServer = "";
webPushServerInput = "";
limits: RateLimits | null = null; limits: RateLimits | null = null;
limitsMessage = ""; limitsMessage = "";
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
@@ -516,6 +559,8 @@ export default class AccountViewView extends Vue {
(settings?.firstName || "") + (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.webPushServer = (settings?.webPushServer as string) || "";
this.webPushServerInput = (settings?.webPushServer as string) || "";
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
} }
@@ -740,7 +785,7 @@ export default class AccountViewView extends Vue {
private async fetchRateLimits(identity: IIdentifier) { private async fetchRateLimits(identity: IIdentifier) {
const url = `${this.apiServer}/api/report/rateLimits`; const url = `${this.apiServer}/api/report/rateLimits`;
const headers = await this.getHeaders(identity); const headers = await this.getHeaders(identity);
return await this.axios.get(url, { headers }); return await this.axios.get(url, { headers } as AxiosRequestConfig);
} }
/** /**
@@ -844,8 +889,12 @@ export default class AccountViewView extends Vue {
this.apiServer = this.apiServerInput; this.apiServer = this.apiServerInput;
} }
setApiServerInput(value: string) { async onClickSavePushServer() {
this.apiServerInput = value; await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
webPushServer: this.webPushServerInput,
});
this.webPushServer = this.webPushServerInput;
} }
} }
</script> </script>

View File

@@ -20,7 +20,7 @@
</h1> </h1>
</div> </div>
<div @click="onCopyToClipboard()"> <div @click="onCopyToClipboard()" v-if="activeDid">
<!-- <!--
Play with display options: https://qr-code-styling.com/ Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3 See docs: https://www.npmjs.com/package/qr-code-generator-vue3
@@ -32,6 +32,15 @@
class="flex justify-center" class="flex justify-center"
/> />
</div> </div>
<div class="text-center" v-else>
You have no identitifiers yet, so
<router-link :to="{ name: 'start' }" class="text-blue-500">
create your identifier.
</router-link>
<br />
We recommend you do that first; otherwise, these contacts won't see your
activity.
</div>
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1> <h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
<qrcode-stream @detect="onScanDetect" @error="onScanError" /> <qrcode-stream @detect="onScanDetect" @error="onScanError" />
@@ -91,7 +100,7 @@ export default class ContactQRScanShow extends Vue {
if (!identity) { if (!identity) {
throw new Error( throw new Error(
"Attempted to load Give records with no identity available.", "Attempted to show contact info with no identity available.",
); );
} }
return identity; return identity;
@@ -106,17 +115,7 @@ export default class ContactQRScanShow extends Vue {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
if (!account) { if (account) {
this.$notify(
{
group: "alert",
type: "warning",
title: "",
text: "You have no identity yet.",
},
-1,
);
} else {
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
const publicKeyHex = identity.keys[0].publicKeyHex; const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64"); const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");

View File

@@ -27,7 +27,7 @@
</span> </span>
<input <input
type="text" type="text"
placeholder="DID, Name, Public Key" placeholder="DID, Name, Public Key (base 16 or 64)"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2" class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
v-model="contactInput" v-model="contactInput"
/> />
@@ -110,48 +110,50 @@
</div> </div>
<div id="ContactActions" class="flex gap-1.5 mt-2"> <div id="ContactActions" class="flex gap-1.5 mt-2">
<button <div v-if="activeDid">
v-if="contact.seesMe" <button
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md" v-if="contact.seesMe"
@click="setVisibility(contact, false, true)" class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
title="They can see you" @click="setVisibility(contact, false, true)"
> title="They can see you"
<fa icon="eye" class="fa-fw" /> >
</button> <fa icon="eye" class="fa-fw" />
<button </button>
v-else <button
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="setVisibility(contact, true, true)"
title="They cannot see you"
>
<fa icon="eye-slash" class="fa-fw" />
</button>
<button
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="checkVisibility(contact)"
title="Check Visibility"
>
<fa icon="rotate" class="fa-fw" />
</button>
<button
@click="register(contact)"
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
>
<fa
v-if="contact.registered"
icon="person-circle-check"
class="fa-fw"
title="Registered"
/>
<fa
v-else v-else
icon="person-circle-question" class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
class="fa-fw" @click="setVisibility(contact, true, true)"
title="Registration Unknown" title="They cannot see you"
/> >
</button> <fa icon="eye-slash" class="fa-fw" />
</button>
<button
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="checkVisibility(contact)"
title="Check Visibility"
v-if="activeDid"
>
<fa icon="rotate" class="fa-fw" />
</button>
<button
@click="register(contact)"
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
v-if="activeDid"
>
<fa
v-if="contact.registered"
icon="person-circle-check"
class="fa-fw"
title="Registered"
/>
<fa
v-else
icon="person-circle-question"
class="fa-fw"
title="Registration Unknown"
/>
</button>
</div>
<button <button
@click="deleteContact(contact)" @click="deleteContact(contact)"
@@ -263,6 +265,7 @@ import {
SimpleSigner, SimpleSigner,
} from "@/libs/crypto"; } from "@/libs/crypto";
import { import {
CONTACT_URL_PREFIX,
GiveServerRecord, GiveServerRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
RegisterVerifiableCredential, RegisterVerifiableCredential,
@@ -303,6 +306,7 @@ export default class ContactsView extends Vue {
givenToMeUnconfirmed: Record<string, number> = {}; givenToMeUnconfirmed: Record<string, number> = {};
hourDescriptionInput = ""; hourDescriptionInput = "";
hourInput = "0"; hourInput = "0";
isRegistered = false;
showGiveNumbers = false; showGiveNumbers = false;
showGiveTotals = true; showGiveTotals = true;
showGiveConfirmed = true; showGiveConfirmed = true;
@@ -312,6 +316,7 @@ export default class ContactsView extends Vue {
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.isRegistered = !!settings?.isRegistered;
this.showGiveNumbers = !!settings?.showContactGivesInline; this.showGiveNumbers = !!settings?.showContactGivesInline;
if (this.showGiveNumbers) { if (this.showGiveNumbers) {
@@ -475,6 +480,12 @@ export default class ContactsView extends Vue {
); );
return; return;
} }
if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
await this.newContactFromScan(this.contactInput);
return;
}
let did = this.contactInput; let did = this.contactInput;
let name, publicKeyBase64; let name, publicKeyBase64;
const commaPos1 = this.contactInput.indexOf(","); const commaPos1 = this.contactInput.indexOf(",");
@@ -493,7 +504,7 @@ export default class ContactsView extends Vue {
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64"); publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
} }
const newContact = { did, name, publicKeyBase64 }; const newContact = { did, name, publicKeyBase64 };
return this.addContact(newContact); await this.addContact(newContact);
} }
async newContactFromScan(url: string): Promise<void> { async newContactFromScan(url: string): Promise<void> {
@@ -540,31 +551,39 @@ 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); let addedMessage;
if (this.activeDid) {
this.setVisibility(newContact, true, false);
addedMessage =
newContact.name +
" was added, and your activity is visible to them.";
} else {
addedMessage = newContact.name + " was added.";
}
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Contact Added", title: "Contact Added",
text: text: addedMessage,
newContact.name +
" was added, and your activity is visible to them.",
}, },
-1, 5000,
);
// 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,
); );
if (this.isRegistered) {
// 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,
);
}
}) })
.catch((err) => { .catch((err) => {
console.error("Error when adding contact to storage:", err); console.error("Error when adding contact to storage:", err);

View File

@@ -358,9 +358,7 @@ export default class DiscoverView extends Vue {
const plans: ProjectData[] = results.data; const plans: ProjectData[] = results.data;
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId = plan.handleId, rowid } = plan; const { name, description, handleId = plan.handleId, rowid } = plan;
if (beforeId !== plan["rowid"]) { this.projects.push({ name, description, handleId, rowid });
this.projects.push({ name, description, handleId, rowid });
}
} }
} else { } else {
this.projects = results.data; this.projects = results.data;

View File

@@ -104,7 +104,7 @@
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm" class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedId" v-if="record.jwtId == feedLastViewedId"
> >
You've seen all claims below: You've seen all the following
</div> </div>
<div class="flex"> <div class="flex">
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa> <fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
@@ -171,13 +171,7 @@ export default class HomeView extends Vue {
.equals(activeDid) .equals(activeDid)
.first()) as Account; .first()) as Account;
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse(account?.identity || "null");
return identity; // may be null
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
);
}
return identity;
} }
public async getHeaders(identity: IIdentifier) { public async getHeaders(identity: IIdentifier) {

View File

@@ -76,7 +76,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
components: {}, components: {},
}) })
export default class ImportAccountView extends Vue { export default class ImportAccountView extends Vue {
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; // for legacy imports, likely never used
mnemonic = ""; mnemonic = "";
address = ""; address = "";

View File

@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa ><fa icon="chevron-left" class="fa-fw"></fa
></router-link> ></router-link>
[New/Edit] Plan Edit Idea
</h1> </h1>
</div> </div>
@@ -24,7 +24,7 @@
<input <input
type="text" type="text"
placeholder="Project Name" placeholder="Idea Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="projectName" v-model="projectName"
/> />
@@ -34,10 +34,10 @@
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
rows="5" rows="5"
v-model="description" v-model="description"
maxlength="500" maxlength="5000"
></textarea> ></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4"> <div class="text-xs text-slate-500 italic -mt-3 mb-4">
{{ description.length }}/500 max. characters {{ description.length }}/5000 max. characters
</div> </div>
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
@@ -293,6 +293,20 @@ export default class NewEditProjectView extends Vue {
2000, 2000,
this, this,
); );
} else {
console.log(
"Got unexpected 'data' inside response from server",
resp,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Saving Idea",
text: "Server did not save the idea. Try again.",
},
-1,
);
} }
} catch (error) { } catch (error) {
let userMessage = "There was an error saving the project."; let userMessage = "There was an error saving the project.";

View File

@@ -12,7 +12,7 @@
> >
<fa icon="chevron-left" class="fa-fw"></fa> <fa icon="chevron-left" class="fa-fw"></fa>
</button> </button>
View Plan Idea
</h1> </h1>
</div> </div>
@@ -148,7 +148,7 @@
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4"> <div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4">
<div class="bg-slate-100 px-4 py-3 rounded-md"> <div class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">
Offered To This Project Offered To This Idea
</h3> </h3>
<div v-if="offersToThis.length === 0"> <div v-if="offersToThis.length === 0">
@@ -180,9 +180,7 @@
</div> </div>
<div class="bg-slate-100 px-4 py-3 rounded-md"> <div class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">Given To This Idea</h3>
Given To This Project
</h3>
<div v-if="givesToThis.length === 0">(None yet. Record one above.)</div> <div v-if="givesToThis.length === 0">(None yet. Record one above.)</div>
@@ -216,7 +214,7 @@
class="bg-slate-100 px-4 py-3 rounded-md" class="bg-slate-100 px-4 py-3 rounded-md"
> >
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">
Contributions To This Project Contributions To This Idea
</h3> </h3>
<ul> <ul>
<li v-for="plan in fulfillersToThis" :key="plan.handleId"> <li v-for="plan in fulfillersToThis" :key="plan.handleId">
@@ -232,7 +230,7 @@
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md"> <div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">
Contributions By This Project Contributions By This Idea
</h3> </h3>
<button <button
@click="onClickLoadProject(fulfilledByThis.handleId)" @click="onClickLoadProject(fulfilledByThis.handleId)"

View File

@@ -1,33 +1,69 @@
const notifications = require("./safari-notifications.js"); /* eslint-env serviceworker */
/* global workbox */
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
);
self.addEventListener("install", (event) => {
console.error("Adding event listener for:", event);
importScripts(
"safari-notifications.js",
"nacl.js",
"noble-curves.js",
"noble-hashes.js",
);
});
self.addEventListener("push", function (event) { self.addEventListener("push", function (event) {
let payload; event.waitUntil(
if (event.data) { (async () => {
payload = JSON.parse(event.data.text()); try {
let payload;
if (event.data) {
payload = JSON.parse(event.data.text());
}
const message = await self.getNotificationCount();
if (message) {
console.log("Will notify user:", message);
const title = payload ? payload.title : "Custom Title";
const options = {
body: message,
icon: payload ? payload.icon : "icon.png",
badge: payload ? payload.badge : "badge.png",
};
await self.registration.showNotification(title, options);
} else {
console.log("No notification message, so will not tell the user.");
}
} catch (error) {
console.error("Error processing the push event:", error);
}
})(),
);
});
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SEND_LOCAL_DATA") {
self.secret = event.data.data;
event.ports[0].postMessage({ success: true });
} }
const title = payload ? payload.title : "Custom Title";
const options = {
body: payload ? payload.body : "Custom body text",
icon: payload ? payload.icon : "icon.png",
badge: payload ? payload.badge : "badge.png",
};
event.waitUntil(self.registration.showNotification(title, options));
}); });
self.addEventListener("activate", (event) => {
self.addEventListener("message", function (event) { event.waitUntil(clients.claim());
const data = event.data; console.log("Service worker activated", event);
const result = notifications.getNotificationCount()
switch (data.command) {
case "account":
break;
default:
console.log("Unknown command:", data.command);
}
}); });
self.addEventListener("fetch", (event) => {
console.log("Got fetch event", event.request);
});
self.addEventListener("error", (event) => {
console.error("Error in Service Worker:", event.message);
console.error("File:", event.filename);
console.error("Line:", event.lineno);
console.error("Column:", event.colno);
console.error("Error Object:", event.error);
});
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);

1051
sw_scripts/nacl.js Normal file

File diff suppressed because it is too large Load Diff

5248
sw_scripts/noble-curves.js Normal file

File diff suppressed because it is too large Load Diff

3068
sw_scripts/noble-hashes.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,9 @@ module.exports = defineConfig({
iconPaths: { iconPaths: {
faviconSVG: "img/icons/safari-pinned-tab.svg", faviconSVG: "img/icons/safari-pinned-tab.svg",
}, },
workboxPluginMode: "InjectManifest",
workboxOptions: { workboxOptions: {
importScripts: ["additional-scripts.js"], swSrc: "./sw_scripts/additional-scripts.js",
}, },
}, },
}); });

View File

@@ -390,3 +390,13 @@ While notifications are turned on, the user can tap on the App Notifications tog
- "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on). - "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on).
- "Leave it On" to make no changes and dismiss the dialog. - "Leave it On" to make no changes and dismiss the dialog.
# NOTIFICATION STATES
* Unpermissioned. Push server cannot send notifications to the user because it does not have permission.
This may be the same as when the user gave permission in the past but has since revoked it at the OS or browser
level, outside the app. (User can change to Permissioned when the user gives permission.)
* Permissioned. (User can change to Unpermissioned via the OS or browser settings.)
* Active. (User can change to Muted when the user mutes notifications.)
* Muted. (User can change to Active when the user toggles it.)
(Turning mute off automatically after some amount of time is not planned in version 1.)