Compare commits
36 Commits
home-infin
...
0.1.8
| Author | SHA1 | Date | |
|---|---|---|---|
| d26d1d3601 | |||
| 1e6159869f | |||
| 75d15ddeb9 | |||
| 051a0a97d8 | |||
| f8d3fe2ee1 | |||
| 4f0a046723 | |||
| c4a0458c08 | |||
| 25b1598fcb | |||
| ddbb700c34 | |||
| fd8877900b | |||
| 05c6ddda02 | |||
| 853eb3c623 | |||
| 44cfe0d88e | |||
| 7fe256dc9e | |||
| e739d0be7c | |||
| 8d873b51bd | |||
| d7f4acb702 | |||
| f8002c4550 | |||
| d6b1386741 | |||
| 50fdd95c60 | |||
| 91c6c7c11c | |||
| 4e28dc8de6 | |||
| fb425f0d51 | |||
| a19aebcb37 | |||
| d0697c1ef4 | |||
| 1dd2333624 | |||
|
|
b4b78f6a2c | ||
|
|
3c0f6ce0de | ||
| 5534f8fa50 | |||
| a5004d475e | |||
| b445b1234f | |||
| 17c96dd01a | |||
| 6ad17101b2 | |||
| b4085ffaa7 | |||
| 4f2cb55753 | |||
| ebf9164ecc |
26
CHANGELOG.md
@@ -9,7 +9,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
## [0.1.6]
|
## [0.1.8] - 2023.12.27
|
||||||
|
### Added
|
||||||
|
- DB logging for service-worker events
|
||||||
|
- Help page for notifications
|
||||||
|
- Test notification & web-push triggers inside app
|
||||||
|
- Check that the app is installed
|
||||||
|
### Fixed
|
||||||
|
- Project issuer display name
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.7] - 2023.12.19 - 91c6c7c11c71f96006cc876fc946f1f98a274ba2
|
||||||
|
### Changed
|
||||||
|
- Icons
|
||||||
|
### Fixed
|
||||||
|
- Notification switch now shows message
|
||||||
|
- Prod/test server warning message at top of page
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.6] - 2023.12.17 - b445b1234fbfcf6b37d695373f259aab0eda1118
|
||||||
|
### Added
|
||||||
|
- Infinite scroll on home page
|
||||||
|
### Changed
|
||||||
|
- UI improvements
|
||||||
|
- Show web-push subscription info
|
||||||
|
- Icon
|
||||||
|
|
||||||
|
|
||||||
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad
|
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad
|
||||||
|
|||||||
25
README.md
@@ -22,17 +22,17 @@ npm run lint
|
|||||||
|
|
||||||
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/",`
|
||||||
|
|
||||||
* `npx prettier --write ./sw_scripts/`
|
* Update the CHANGELOG.md & the version in package.json, run `npm install`, and commit.
|
||||||
|
|
||||||
...to make sure the service worker scripts are in proper form
|
* [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
|
||||||
|
|
||||||
* Update the CHANGELOG.md & the version in package.json, run `npm install`, and commit. Tag wth the new version: `git tag 0.1.0`
|
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test".
|
||||||
|
|
||||||
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be PROD.
|
|
||||||
|
|
||||||
* `npm run build`
|
* `npm run build`
|
||||||
|
|
||||||
* Revert src/constants/app.ts & change version to "-beta"
|
* `npx prettier --write ./sw_scripts/`
|
||||||
|
|
||||||
|
...to make sure the service worker scripts are in proper form. It's only important if you changed something in that directory.
|
||||||
|
|
||||||
* `cp sw_scripts/[ns]* dist/`
|
* `cp sw_scripts/[ns]* dist/`
|
||||||
|
|
||||||
@@ -40,6 +40,8 @@ If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js,
|
|||||||
|
|
||||||
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntu@endorser.ch:time-safari`
|
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntu@endorser.ch:time-safari`
|
||||||
|
|
||||||
|
* Revert src/constants/app.ts and/or package.json, edit package.json to increment version & add "-beta", `npm install`, and commit.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
@@ -110,10 +112,12 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
|
|||||||
|
|
||||||
### Clear/Reset data & restart
|
### Clear/Reset data & restart
|
||||||
|
|
||||||
* Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data".)
|
* Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.)
|
||||||
* Unregister service worker (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`).
|
* Clear notification permission. (in Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".)
|
||||||
* Clear notification permission (in Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications").
|
* Unregister service worker. (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`.)
|
||||||
* Clear Cache Storage (in Chrome, in dev tools under Application; in Firefox, in dev tools under Storage).
|
* Clear Cache Storage. (in Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.)
|
||||||
|
|
||||||
|
(If you find more, add them to the HelpNotificationsView.vue file.)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -136,3 +140,4 @@ Gifts make the world go 'round!
|
|||||||
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
|
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
|
||||||
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
|
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
|
||||||
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
|
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
|
||||||
|
* Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e)
|
||||||
|
|||||||
8
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari_Test",
|
||||||
"version": "0.1.6-beta",
|
"version": "0.1.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari_Test",
|
||||||
"version": "0.1.6-beta",
|
"version": "0.1.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari_Test",
|
||||||
"version": "0.1.6-beta",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
|
|||||||
@@ -2,35 +2,19 @@
|
|||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
- 08 notifications :
|
- 08 notifications :
|
||||||
- get it to work on Android
|
- .2 after turning on notification, don't wait in push server but wait in client for message test
|
||||||
- get it to work on iOS
|
- insert tooling (exportable logs?) so that we can see problems and troubleshoot as we onboard
|
||||||
- lock down regenerate_vapid endpoint (so only we admins can do it on demand)
|
- if navigator.serviceWorker is null, then tell the user to wait
|
||||||
- make the app behave correctly when App Notifications are turned off
|
- Local install works after cleared out cache in Chrome
|
||||||
- remove sleep in py-push-server app.py?
|
|
||||||
- write troubleshooting docs for notifications
|
|
||||||
- make the "App Notifications" toggle on when they turn notifications on
|
|
||||||
- make the "App Notifications" toggle off when they turn notifications off
|
|
||||||
- in py-push-server, when sending a push to a subscriber and we get on a 410 "error #106", delete the subscription record
|
|
||||||
- https://gitea.anomalistdesign.com/trent_larson/py-push-server/pulls/3/files
|
|
||||||
- remove "notification push server" advanced setting since it only makes sense on the current domain
|
|
||||||
- prompt user to install on their home screen
|
|
||||||
|
|
||||||
- back-and-forth on discovery & project pages led to "You need an identity to load your projects." error on product page when I had an identity
|
- fix maskable icon
|
||||||
- fix the projects on /discover to show the issuer (currently all "Someone Anonymous")
|
|
||||||
|
- give users next public key hash
|
||||||
|
|
||||||
- .3 bug - make or edit a project, choose "Include location", and see the map display shows on top of the bottom icons assignee-group:ui
|
- .3 bug - make or edit a project, choose "Include location", and see the map display shows on top of the bottom icons assignee-group:ui
|
||||||
|
|
||||||
- .5 Add infinite scroll to gifts on the home page
|
|
||||||
|
|
||||||
- .5 If notifications are not enabled, add message to front page with link/button to enable
|
- .5 If notifications are not enabled, add message to front page with link/button to enable
|
||||||
|
|
||||||
- 01 server - show all claim details when issued by the issuer
|
|
||||||
- add note after contact addition that they can see your info
|
|
||||||
- enhance help page instructions for debugging
|
|
||||||
- add way to test quickly a push notification
|
|
||||||
- help instructions for PWA install problems (secret failed, must reinstall)
|
|
||||||
- look at other examples for better UI friend.tech
|
|
||||||
|
|
||||||
- show VC details... somehow:
|
- show VC details... somehow:
|
||||||
- 01 show my VCs - most interesting, or via search
|
- 01 show my VCs - most interesting, or via search
|
||||||
- 01 allow download of each VC (& confirmations, to show that they actually own their data)
|
- 01 allow download of each VC (& confirmations, to show that they actually own their data)
|
||||||
@@ -50,10 +34,17 @@ tasks:
|
|||||||
- Other features - donation vs give, show offers, show give & outstanding totals, show network view, restrict registration, connect to contacts
|
- Other features - donation vs give, show offers, show give & outstanding totals, show network view, restrict registration, connect to contacts
|
||||||
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
|
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
|
||||||
|
|
||||||
- remove 'rowid' references (that are sqlite-specific)
|
|
||||||
- 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)
|
||||||
- 01 make the prod build copy the sw_scripts
|
- 02 watch for the service worker activation before showing the button to turn on notifications
|
||||||
|
- 01 server - show all claim details when issued by the issuer
|
||||||
|
- bug - got error adding on Firefox user #0 as contact for themselves
|
||||||
|
- bug (that is hard to reproduce) - back-and-forth on discovery & project pages led to "You need an identity to load your projects." error on product page when I had an identity
|
||||||
|
- bug (that is hard to reproduce) - in Chrome, install app then delete app and try from Chrome browser and see log errors "Uncaught TypeError: self.appendDailyLog is not a function"
|
||||||
|
- bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent
|
||||||
- 01 send visibility signal as a VC and store it
|
- 01 send visibility signal as a VC and store it
|
||||||
|
- 04 remove 'rowid' references (that are sqlite-specific); may involve server
|
||||||
|
- 04 look at other examples for better UI friend.tech
|
||||||
|
- 01 make the prod build copy the sw_scripts
|
||||||
- .5 Add start date to project
|
- .5 Add start date to project
|
||||||
- .3 check that Android shows "back" buttons on screens without bottom tray
|
- .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?
|
||||||
@@ -61,12 +52,13 @@ tasks:
|
|||||||
- .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 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
|
|
||||||
- switch some checks for activeDid to check for isRegistered
|
- 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"
|
- .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)
|
- .5 fix cert generation on server (since it didn't happen automatically for Nov 30)
|
||||||
|
- warn if they're using the web (android only?)
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
|
||||||
|
https://web.dev/articles/get-installed-related-apps
|
||||||
|
|
||||||
- 04 fix lack of initial notification in Firefox (on MacOS, maybe others)
|
- 04 fix lack of initial notification in Firefox (on MacOS, maybe others)
|
||||||
|
|
||||||
@@ -89,6 +81,7 @@ tasks:
|
|||||||
- 24 Move to Vite
|
- 24 Move to Vite
|
||||||
- 32 accept images for projects
|
- 32 accept images for projects
|
||||||
- 32 accept images for contacts
|
- 32 accept images for contacts
|
||||||
|
- import project interactions from GitHub/GitLab and manage signing
|
||||||
|
|
||||||
- linking between projects or plans :
|
- linking between projects or plans :
|
||||||
- show total time given to & from a project
|
- show total time given to & from a project
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 463 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 799 B After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 37 KiB |
37
src/App.vue
@@ -281,7 +281,7 @@ interface VapidResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
import { AppString } from "@/constants/app";
|
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
||||||
import { db } from "@/db/index";
|
import { db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ export default class App extends Vue {
|
|||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
let pushUrl: string = AppString.DEFAULT_PUSH_SERVER;
|
let pushUrl = DEFAULT_PUSH_SERVER;
|
||||||
if (settings?.webPushServer) {
|
if (settings?.webPushServer) {
|
||||||
pushUrl = settings.webPushServer;
|
pushUrl = settings.webPushServer;
|
||||||
}
|
}
|
||||||
@@ -329,10 +329,7 @@ export default class App extends Vue {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (window.location.host.startsWith("localhost")) {
|
if (window.location.host.startsWith("localhost")) {
|
||||||
console.log(
|
console.log("Ignoring the error getting VAPID for local development.");
|
||||||
"Ignoring this error getting VAPID for local development:",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Got an error initializing notifications:", error);
|
console.error("Got an error initializing notifications:", error);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -373,6 +370,7 @@ export default class App extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private askPermission(): Promise<NotificationPermission> {
|
private askPermission(): Promise<NotificationPermission> {
|
||||||
|
console.log("Requesting permission for notifications:", navigator);
|
||||||
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
|
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
|
||||||
return Promise.reject("Service worker not available.");
|
return Promise.reject("Service worker not available.");
|
||||||
}
|
}
|
||||||
@@ -423,7 +421,7 @@ export default class App extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async turnOnNotifications() {
|
public async turnOnNotifications() {
|
||||||
return this.askPermission()
|
return this.askPermission()
|
||||||
.then((permission) => {
|
.then((permission) => {
|
||||||
console.log("Permission granted:", permission);
|
console.log("Permission granted:", permission);
|
||||||
@@ -445,7 +443,18 @@ export default class App extends Vue {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log("Subscription data sent to server.");
|
console.log(
|
||||||
|
"Subscription data sent to server and all finished successfully.",
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Notifications Turned On",
|
||||||
|
text: "Notifications are on. You should see one on your device; if not, see the 'Troubleshoot' page.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -462,9 +471,7 @@ export default class App extends Vue {
|
|||||||
"An error occurred setting notification permissions:",
|
"An error occurred setting notification permissions:",
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
alert(
|
alert("Some error occurred setting notification permissions.");
|
||||||
"Some error occurred setting notification permissions. See logs.",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,11 +518,7 @@ export default class App extends Vue {
|
|||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(
|
console.error("Push subscription failed:", error, options);
|
||||||
"Subscription or server communication failed:",
|
|
||||||
error,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Inform the user about the issue
|
// Inform the user about the issue
|
||||||
alert(
|
alert(
|
||||||
@@ -531,7 +534,7 @@ export default class App extends Vue {
|
|||||||
private sendSubscriptionToServer(
|
private sendSubscriptionToServer(
|
||||||
subscription: PushSubscription,
|
subscription: PushSubscription,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log("About to send subscription", subscription);
|
console.log("About to send subscription...", subscription);
|
||||||
return fetch("/web-push/subscribe", {
|
return fetch("/web-push/subscribe", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
BIN
src/assets/help/apple-icon.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/assets/help/chrome-install-pwa.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/assets/help/mac-installed-app-settings.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
src/assets/help/windows-system-enable-notifications.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
@@ -123,9 +123,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text: err.message || "There was an error retrieving your settings.",
|
||||||
err.message ||
|
|
||||||
"There was an error retrieving the latest sweet, sweet action.",
|
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -105,9 +105,7 @@ export default class OfferDialog extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text: err.message || "There was an error retrieving your settings.",
|
||||||
err.message ||
|
|
||||||
"There was an error retrieving the latest sweet, sweet action.",
|
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
58
src/components/TopMessage.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-center text-red-500">{{ message }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||||
|
import { db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import { AppString } from "@/constants/app";
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class TopMessage extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
@Prop selected = "";
|
||||||
|
|
||||||
|
message = "";
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
if (
|
||||||
|
settings?.warnIfTestServer &&
|
||||||
|
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||||
|
) {
|
||||||
|
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||||
|
this.message = "You're linked to a non-prod server, user " + didPrefix;
|
||||||
|
} else if (
|
||||||
|
settings?.warnIfProdServer &&
|
||||||
|
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
||||||
|
) {
|
||||||
|
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||||
|
this.message =
|
||||||
|
"You're linked to the production server, user " + didPrefix;
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Detecting Server",
|
||||||
|
text: JSON.stringify(err),
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -4,21 +4,20 @@
|
|||||||
* See also ../libs/veramo/setup.ts
|
* See also ../libs/veramo/setup.ts
|
||||||
*/
|
*/
|
||||||
export enum AppString {
|
export enum AppString {
|
||||||
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,
|
|
||||||
|
|
||||||
PROD_PUSH_SERVER = "https://timesafari.app",
|
PROD_PUSH_SERVER = "https://timesafari.app",
|
||||||
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
||||||
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
|
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
|
||||||
|
|
||||||
DEFAULT_PUSH_SERVER = TEST1_PUSH_SERVER,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER;
|
||||||
|
|
||||||
|
export const DEFAULT_PUSH_SERVER =
|
||||||
|
window.location.protocol + "//" + window.location.host;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The possible values for "group" and "type" are in App.vue.
|
* The possible values for "group" and "type" are in App.vue.
|
||||||
* From the notiwind package
|
* From the notiwind package
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import BaseDexie, { Table } from "dexie";
|
import BaseDexie, { Table } from "dexie";
|
||||||
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
||||||
import { Account, AccountsSchema } from "./tables/accounts";
|
import { Account, AccountsSchema } from "./tables/accounts";
|
||||||
import { Contact, ContactsSchema } from "./tables/contacts";
|
import { Contact, ContactSchema } from "./tables/contacts";
|
||||||
|
import { Log, LogSchema } from "./tables/logs";
|
||||||
import {
|
import {
|
||||||
MASTER_SETTINGS_KEY,
|
MASTER_SETTINGS_KEY,
|
||||||
Settings,
|
Settings,
|
||||||
SettingsSchema,
|
SettingsSchema,
|
||||||
} from "./tables/settings";
|
} from "./tables/settings";
|
||||||
import { AppString } from "@/constants/app";
|
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||||
|
|
||||||
// Define types for tables that hold sensitive and non-sensitive data
|
// Define types for tables that hold sensitive and non-sensitive data
|
||||||
type SensitiveTables = { accounts: Table<Account> };
|
type SensitiveTables = { accounts: Table<Account> };
|
||||||
type NonsensitiveTables = {
|
type NonsensitiveTables = {
|
||||||
contacts: Table<Contact>;
|
contacts: Table<Contact>;
|
||||||
|
logs: Table<Log>;
|
||||||
settings: Table<Settings>;
|
settings: Table<Settings>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,7 +28,11 @@ export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
|||||||
const SensitiveSchemas = { ...AccountsSchema };
|
const SensitiveSchemas = { ...AccountsSchema };
|
||||||
|
|
||||||
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
||||||
const NonsensitiveSchemas = { ...ContactsSchema, ...SettingsSchema };
|
const NonsensitiveSchemas = {
|
||||||
|
...ContactSchema,
|
||||||
|
...LogSchema,
|
||||||
|
...SettingsSchema,
|
||||||
|
};
|
||||||
|
|
||||||
// Manage the encryption key. If not present in localStorage, create and store it.
|
// Manage the encryption key. If not present in localStorage, create and store it.
|
||||||
const secret =
|
const secret =
|
||||||
@@ -38,15 +44,14 @@ encrypted(accountsDB, { secretKey: secret });
|
|||||||
|
|
||||||
// Define the schema for our databases
|
// Define the schema for our databases
|
||||||
accountsDB.version(1).stores(SensitiveSchemas);
|
accountsDB.version(1).stores(SensitiveSchemas);
|
||||||
db.version(1).stores(NonsensitiveSchemas);
|
// v1 was contacts & settings
|
||||||
|
// v2 added logs
|
||||||
|
db.version(2).stores(NonsensitiveSchemas);
|
||||||
|
|
||||||
// Event handler to initialize the non-sensitive database with default settings
|
// Event handler to initialize the non-sensitive database with default settings
|
||||||
db.on("populate", () => {
|
db.on("populate", () => {
|
||||||
db.settings.add({
|
db.settings.add({
|
||||||
id: MASTER_SETTINGS_KEY,
|
id: MASTER_SETTINGS_KEY,
|
||||||
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
|
apiServer: 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,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ export interface Contact {
|
|||||||
registered?: boolean;
|
registered?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContactsSchema = {
|
export const ContactSchema = {
|
||||||
contacts: "&did, name, publicKeyBase64, registered, seesMe",
|
contacts: "&did, name, publicKeyBase64, registered, seesMe",
|
||||||
};
|
};
|
||||||
|
|||||||
10
src/db/tables/logs.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export interface Log {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogSchema = {
|
||||||
|
// Currently keyed by "date" because A) today's log data is what we need so we append, and
|
||||||
|
// B) we don't want it to grow so we remove everything if this is the first entry today.
|
||||||
|
// See safari-notifications.js logMessage for the associated logic.
|
||||||
|
logs: "date, message",
|
||||||
|
};
|
||||||
@@ -12,15 +12,17 @@ export type BoundingBox = {
|
|||||||
* Settings type encompasses user-specific configuration details.
|
* Settings type encompasses user-specific configuration details.
|
||||||
*/
|
*/
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
id: number; // Only one entry using MASTER_SETTINGS_KEY
|
id: number; // Only one entry, keyed with MASTER_SETTINGS_KEY
|
||||||
|
|
||||||
activeDid?: string; // Active Decentralized ID
|
activeDid?: string; // Active Decentralized ID
|
||||||
apiServer?: string; // API server URL
|
apiServer?: string; // API server URL
|
||||||
firstName?: string; // User's first name
|
firstName?: string; // User's first name
|
||||||
lastName?: string; // deprecated - put all names in firstName
|
|
||||||
lastViewedClaimId?: string; // Last viewed claim ID
|
|
||||||
lastNotifiedClaimId?: string; // Last notified claim ID
|
|
||||||
isRegistered?: boolean;
|
isRegistered?: boolean;
|
||||||
webPushServer?: string; // Web Push server URL
|
lastName?: string; // deprecated - put all names in firstName
|
||||||
|
lastNotifiedClaimId?: string; // Last notified claim ID
|
||||||
|
lastViewedClaimId?: string; // Last viewed claim ID
|
||||||
|
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
|
||||||
|
reminderOn?: boolean; // Toggle to enable or disable reminders
|
||||||
|
|
||||||
// Array of named search boxes defined by bounding boxes
|
// Array of named search boxes defined by bounding boxes
|
||||||
searchBoxes?: Array<{
|
searchBoxes?: Array<{
|
||||||
@@ -30,8 +32,9 @@ export type Settings = {
|
|||||||
|
|
||||||
showContactGivesInline?: boolean; // Display contact inline or not
|
showContactGivesInline?: boolean; // Display contact inline or not
|
||||||
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
||||||
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
|
warnIfProdServer?: boolean; // Warn if using a production server
|
||||||
reminderOn?: boolean; // Toggle to enable or disable reminders
|
warnIfTestServer?: boolean; // Warn if using a testing server
|
||||||
|
webPushServer?: string; // Web Push server URL
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -486,6 +486,10 @@ export interface ProjectData {
|
|||||||
* URL referencing information about the project
|
* URL referencing information about the project
|
||||||
**/
|
**/
|
||||||
handleId: string;
|
handleId: string;
|
||||||
|
/**
|
||||||
|
* The DID of the issuer
|
||||||
|
*/
|
||||||
|
issuerDid: string;
|
||||||
/**
|
/**
|
||||||
* The Identier of the project
|
* The Identier of the project
|
||||||
**/
|
**/
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
|
|||||||
import {
|
import {
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
faBan,
|
faBan,
|
||||||
faBitcoinSign,
|
faBitcoinSign,
|
||||||
faBurst,
|
faBurst,
|
||||||
@@ -65,6 +66,7 @@ import {
|
|||||||
library.add(
|
library.add(
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
faBan,
|
faBan,
|
||||||
faBitcoinSign,
|
faBitcoinSign,
|
||||||
faBurst,
|
faBurst,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Profile"></QuickNav>
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
@@ -103,26 +105,10 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||||
<label
|
<div
|
||||||
for="toggleNotifications"
|
v-if="!notificationMaybeChanged"
|
||||||
class="flex items-center justify-between cursor-pointer"
|
class="flex items-center justify-between cursor-pointer"
|
||||||
@click="
|
@click="showNotificationChoice()"
|
||||||
!toggleNotifications
|
|
||||||
? this.$notify(
|
|
||||||
{
|
|
||||||
group: 'modal',
|
|
||||||
type: 'notification-permission',
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
: this.$notify(
|
|
||||||
{
|
|
||||||
group: 'modal',
|
|
||||||
type: 'notification-off',
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<!-- label -->
|
<!-- label -->
|
||||||
<div>App Notifications</div>
|
<div>App Notifications</div>
|
||||||
@@ -131,8 +117,8 @@
|
|||||||
<!-- input -->
|
<!-- input -->
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="toggleNotifications"
|
v-model="isSubscribed"
|
||||||
name="toggleNotifications"
|
name="toggleNotificationsInput"
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
/>
|
/>
|
||||||
<!-- line -->
|
<!-- line -->
|
||||||
@@ -142,7 +128,14 @@
|
|||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
Notification status may have changed. Revisit this page to see the
|
||||||
|
latest setting.
|
||||||
|
</div>
|
||||||
|
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications">
|
||||||
|
Test your notification setup.
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
||||||
@@ -215,7 +208,7 @@
|
|||||||
<div v-if="showAdvanced">
|
<div v-if="showAdvanced">
|
||||||
<p class="text-rose-600 mb-8">
|
<p class="text-rose-600 mb-8">
|
||||||
Beware: the features here can be confusing and even change data in ways
|
Beware: the features here can be confusing and even change data in ways
|
||||||
you do not expect. But we support your freedoms!
|
you do not expect. But we support your freedom!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Deep Identity Details -->
|
<!-- Deep Identity Details -->
|
||||||
@@ -304,7 +297,7 @@
|
|||||||
:to="{ name: 'statistics' }"
|
:to="{ name: 'statistics' }"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
See Global Animated History of Giving
|
See Animated Global History of Giving
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<!-- id used by puppeteer test script -->
|
<!-- id used by puppeteer test script -->
|
||||||
@@ -316,13 +309,6 @@
|
|||||||
Switch Identity
|
Switch Identity
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<button
|
|
||||||
@click="alertWebPushSubscription()"
|
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
|
||||||
>
|
|
||||||
Show Subscription from Web Push Server
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="flex py-4">
|
<div class="flex py-4">
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
||||||
<input
|
<input
|
||||||
@@ -357,6 +343,46 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<label
|
||||||
|
for="toggleProdWarningMessage"
|
||||||
|
class="flex items-center justify-between cursor-pointer my-4"
|
||||||
|
@click="toggleProdWarning"
|
||||||
|
>
|
||||||
|
<!-- label -->
|
||||||
|
<h2>Show warning if on prod server</h2>
|
||||||
|
<!-- toggle -->
|
||||||
|
<div class="relative ml-2">
|
||||||
|
<!-- input -->
|
||||||
|
<input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
|
||||||
|
<!-- line -->
|
||||||
|
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||||
|
<!-- dot -->
|
||||||
|
<div
|
||||||
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
for="toggleTestWarningMessage"
|
||||||
|
class="flex items-center justify-between cursor-pointer my-4"
|
||||||
|
@click="toggleTestWarning"
|
||||||
|
>
|
||||||
|
<!-- label -->
|
||||||
|
<h2>Show warning if on non-prod server</h2>
|
||||||
|
<!-- toggle -->
|
||||||
|
<div class="relative ml-2">
|
||||||
|
<!-- input -->
|
||||||
|
<input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
|
||||||
|
<!-- line -->
|
||||||
|
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||||
|
<!-- dot -->
|
||||||
|
<div
|
||||||
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="flex py-4">
|
<div class="flex py-4">
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||||
Notification Push Server
|
Notification Push Server
|
||||||
@@ -408,6 +434,7 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
@@ -432,7 +459,7 @@ interface IAccount {
|
|||||||
derivationPath: string;
|
derivationPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({ components: { QuickNav } })
|
@Component({ components: { QuickNav, TopMessage } })
|
||||||
export default class AccountViewView extends Vue {
|
export default class AccountViewView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
@@ -444,6 +471,8 @@ export default class AccountViewView extends Vue {
|
|||||||
derivationPath = "";
|
derivationPath = "";
|
||||||
givenName = "";
|
givenName = "";
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
|
isSubscribed = false;
|
||||||
|
notificationMaybeChanged = false;
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
publicHex = "";
|
publicHex = "";
|
||||||
publicBase64 = "";
|
publicBase64 = "";
|
||||||
@@ -462,13 +491,62 @@ export default class AccountViewView extends Vue {
|
|||||||
showAdvanced = false;
|
showAdvanced = false;
|
||||||
|
|
||||||
subscription: PushSubscription | null = null;
|
subscription: PushSubscription | null = null;
|
||||||
|
warnIfProdServer = false;
|
||||||
|
warnIfTestServer = false;
|
||||||
|
|
||||||
private isSubscribed = false;
|
/**
|
||||||
get toggleNotifications() {
|
* Async function executed when the component is created.
|
||||||
return this.isSubscribed;
|
* Initializes the component's state with values from the database,
|
||||||
|
* handles identity-related tasks, and checks limitations.
|
||||||
|
*
|
||||||
|
* @throws Will display specific messages to the user based on different errors.
|
||||||
|
*/
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
|
||||||
|
// Initialize component state with values from the database or defaults
|
||||||
|
this.initializeState(settings);
|
||||||
|
|
||||||
|
// Get and process the identity
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
if (identity) {
|
||||||
|
this.processIdentity(identity);
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
this.handleError(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
set toggleNotifications(value) {
|
|
||||||
this.isSubscribed = value;
|
async mounted() {
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
this.subscription = await registration.pushManager.getSubscription();
|
||||||
|
this.isSubscribed = !!this.subscription;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Mount error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes component state with values from the database or defaults.
|
||||||
|
* @param {SettingsType} settings - Object containing settings from the database.
|
||||||
|
*/
|
||||||
|
initializeState(settings: Settings | undefined) {
|
||||||
|
this.activeDid = (settings?.activeDid as string) || "";
|
||||||
|
this.apiServer = (settings?.apiServer as string) || "";
|
||||||
|
this.apiServerInput = (settings?.apiServer as string) || "";
|
||||||
|
this.givenName =
|
||||||
|
(settings?.firstName || "") +
|
||||||
|
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
||||||
|
this.isRegistered = !!settings?.isRegistered;
|
||||||
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
this.warnIfProdServer = !!settings?.warnIfProdServer;
|
||||||
|
this.warnIfTestServer = !!settings?.warnIfTestServer;
|
||||||
|
this.webPushServer = (settings?.webPushServer as string) || "";
|
||||||
|
this.webPushServerInput = (settings?.webPushServer as string) || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
|
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
|
||||||
@@ -536,6 +614,16 @@ export default class AccountViewView extends Vue {
|
|||||||
this.updateShowContactAmounts();
|
this.updateShowContactAmounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleProdWarning() {
|
||||||
|
this.warnIfProdServer = !this.warnIfProdServer;
|
||||||
|
this.updateWarnIfProdServer(this.warnIfProdServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTestWarning() {
|
||||||
|
this.warnIfTestServer = !this.warnIfTestServer;
|
||||||
|
this.updateWarnIfTestServer(this.warnIfTestServer);
|
||||||
|
}
|
||||||
|
|
||||||
readableTime(timeStr: string) {
|
readableTime(timeStr: string) {
|
||||||
return timeStr.substring(0, timeStr.indexOf("T"));
|
return timeStr.substring(0, timeStr.indexOf("T"));
|
||||||
}
|
}
|
||||||
@@ -545,60 +633,6 @@ export default class AccountViewView extends Vue {
|
|||||||
this.numAccounts = await accountsDB.accounts.count();
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Async function executed when the component is created.
|
|
||||||
* Initializes the component's state with values from the database,
|
|
||||||
* handles identity-related tasks, and checks limitations.
|
|
||||||
*
|
|
||||||
* @throws Will display specific messages to the user based on different errors.
|
|
||||||
*/
|
|
||||||
async created() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
|
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
||||||
|
|
||||||
// Initialize component state with values from the database or defaults
|
|
||||||
this.initializeState(settings);
|
|
||||||
|
|
||||||
// Get and process the identity
|
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
|
||||||
if (identity) {
|
|
||||||
this.processIdentity(identity);
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
this.handleError(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
try {
|
|
||||||
const registration = await navigator.serviceWorker.ready;
|
|
||||||
this.subscription = await registration.pushManager.getSubscription();
|
|
||||||
this.toggleNotifications = !!this.subscription;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Mount error:", error);
|
|
||||||
this.toggleNotifications = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes component state with values from the database or defaults.
|
|
||||||
* @param {SettingsType} settings - Object containing settings from the database.
|
|
||||||
*/
|
|
||||||
initializeState(settings: Settings | undefined) {
|
|
||||||
this.activeDid = (settings?.activeDid as string) || "";
|
|
||||||
this.apiServer = (settings?.apiServer as string) || "";
|
|
||||||
this.apiServerInput = (settings?.apiServer as string) || "";
|
|
||||||
this.givenName =
|
|
||||||
(settings?.firstName || "") +
|
|
||||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
|
||||||
this.webPushServer = (settings?.webPushServer as string) || "";
|
|
||||||
this.webPushServerInput = (settings?.webPushServer as string) || "";
|
|
||||||
this.showContactGives = !!settings?.showContactGivesInline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the identity and updates the component's state.
|
* Processes the identity and updates the component's state.
|
||||||
* @param {IdentityType} identity - Object containing identity information.
|
* @param {IdentityType} identity - Object containing identity information.
|
||||||
@@ -623,6 +657,31 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async showNotificationChoice() {
|
||||||
|
if (!this.subscription) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "notification-permission",
|
||||||
|
title: "", // unused, only here to satisfy type check
|
||||||
|
text: "", // unused, only here to satisfy type check
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "notification-off",
|
||||||
|
title: "", // unused, only here to satisfy type check
|
||||||
|
text: "", // unused, only here to satisfy type check
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.notificationMaybeChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles errors and updates the component's state accordingly.
|
* Handles errors and updates the component's state accordingly.
|
||||||
* @param {Error} err - The error object.
|
* @param {Error} err - The error object.
|
||||||
@@ -661,12 +720,58 @@ export default class AccountViewView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error Updating Contact Setting",
|
title: "Error Updating Contact Setting",
|
||||||
text: "Clear your cache and start over (after data backup).",
|
text: "The setting may not have saved. Try again, maybe after restarting the app.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
console.error(
|
console.error(
|
||||||
"Telling user to clear cache after contact setting update because:",
|
"Telling user to try again after contact setting update because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateWarnIfProdServer(newSetting: boolean) {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
warnIfProdServer: newSetting,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Prod Warning",
|
||||||
|
text: "The setting may not have saved. Try again, maybe after restarting the app.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to try again after setting update because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateWarnIfTestServer(newSetting: boolean) {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
warnIfTestServer: newSetting,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Test Warning",
|
||||||
|
text: "The setting may not have saved. Try again, maybe after restarting the app.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to try again after setting update because:",
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -939,13 +1044,5 @@ export default class AccountViewView extends Vue {
|
|||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
alertWebPushSubscription() {
|
|
||||||
console.log(
|
|
||||||
"Web push subscription:",
|
|
||||||
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
|
|
||||||
);
|
|
||||||
alert(JSON.stringify(this.subscription));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
<!-- Details -->
|
<!-- Details -->
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="block pb-4 flex gap-4 overflow-hidden">
|
<div class="block flex gap-4 overflow-hidden">
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<h2 class="text-xl">{{ veriClaim.id }}</h2>
|
<h2 class="text-md font-bold">{{ veriClaim.id }}</h2>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<div>
|
<div>
|
||||||
{{ veriClaim.claimType }}
|
{{ veriClaim.claimType }}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-bold text-2xl">Confirmations</h2>
|
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Confirmations</h2>
|
||||||
|
|
||||||
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
|
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
|
||||||
<span v-else-if="totalConfirmers() === 1">
|
<span v-else-if="totalConfirmers() === 1">
|
||||||
@@ -136,22 +136,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-bold text-2xl mt-8">Claim</h2>
|
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Claim</h2>
|
||||||
<pre>{{ util.inspect(veriClaim, false, null) }}</pre>
|
<pre class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md">
|
||||||
|
{{ util.inspect(veriClaim, false, null) }}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="font-bold text-2xl mt-8">Full Claim</h2>
|
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Full Claim</h2>
|
||||||
<p>
|
<p class="mb-4">
|
||||||
The full claim includes the claim as it was originally issued, including
|
The full claim includes the claim as it was originally issued, including
|
||||||
the signature (ie. the proof of issuance by that person).
|
the signature (ie. the proof of issuance by that person).
|
||||||
</p>
|
</p>
|
||||||
<div v-if="!fullClaim">
|
<div v-if="!fullClaim">
|
||||||
<div v-if="fullClaimMessage">
|
<p v-if="fullClaimMessage" class="mb-4">
|
||||||
{{ fullClaimMessage }}
|
{{ fullClaimMessage }}
|
||||||
</div>
|
</p>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"
|
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
@click="showFullClaim(veriClaim.id)"
|
@click="showFullClaim(veriClaim.id)"
|
||||||
>
|
>
|
||||||
Load Full Claim Details
|
Load Full Claim Details
|
||||||
@@ -161,10 +163,12 @@
|
|||||||
<pre>{{ util.inspect(fullClaim, false, null) }}</pre>
|
<pre>{{ util.inspect(fullClaim, false, null) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a :href="apiServer + '/api/claim/' + veriClaim.id" target="_blank">
|
<a
|
||||||
<button class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4">
|
:href="apiServer + '/api/claim/' + veriClaim.id"
|
||||||
View on the Public Server
|
target="_blank"
|
||||||
</button>
|
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
View on the Public Server
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -25,6 +25,13 @@
|
|||||||
<span class="justify-around">(Only 50 most recent)</span>
|
<span class="justify-around">(Only 50 most recent)</span>
|
||||||
<span />
|
<span />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex justify-around">
|
||||||
|
<span />
|
||||||
|
<span class="justify-around">
|
||||||
|
(This does not include claims by them if they're not visible to you.)
|
||||||
|
</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<table
|
<table
|
||||||
@@ -178,6 +185,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.log("Error retrieving settings or gives.", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -185,7 +193,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
err.userMessage ||
|
err.userMessage ||
|
||||||
"There was an error retrieving the latest sweet, sweet action.",
|
"There was an error retrieving your settings and/or contacts and/or gives.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export default class ContactGiftingView extends Vue {
|
|||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.log("Error retrieving settings & contacts:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -152,7 +153,7 @@ export default class ContactGiftingView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
err.message ||
|
err.message ||
|
||||||
"There was an error retrieving the latest sweet, sweet action.",
|
"There was an error retrieving your settings and/or contacts.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -908,13 +908,13 @@ export default class ContactsView extends Vue {
|
|||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
} else if (!parseFloat(this.hourInput)) {
|
} else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Input Error",
|
title: "Input Error",
|
||||||
text: "Giving 0 hours does nothing.",
|
text: "Giving no hours or descrption does nothing.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Discover"></QuickNav>
|
<QuickNav selected="Discover"></QuickNav>
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
@@ -136,6 +137,7 @@ import { didInfo, ProjectData } from "@/libs/endorserServer";
|
|||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -149,6 +151,7 @@ interface Notification {
|
|||||||
QuickNav,
|
QuickNav,
|
||||||
InfiniteScroll,
|
InfiniteScroll,
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
|
TopMessage,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class DiscoverView extends Vue {
|
export default class DiscoverView extends Vue {
|
||||||
@@ -274,8 +277,8 @@ export default class DiscoverView extends Vue {
|
|||||||
const plans: ProjectData[] = results.data;
|
const plans: ProjectData[] = results.data;
|
||||||
if (plans) {
|
if (plans) {
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, rowid } = plan;
|
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||||
this.projects.push({ name, description, handleId, rowid });
|
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
||||||
}
|
}
|
||||||
this.remoteCount = this.projects.length;
|
this.remoteCount = this.projects.length;
|
||||||
} else {
|
} else {
|
||||||
@@ -357,8 +360,14 @@ export default class DiscoverView extends Vue {
|
|||||||
if (beforeId) {
|
if (beforeId) {
|
||||||
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, issuerDid, rowid } = plan;
|
||||||
this.projects.push({ name, description, handleId, rowid });
|
this.projects.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
handleId,
|
||||||
|
issuerDid,
|
||||||
|
rowid,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.projects = results.data;
|
this.projects = results.data;
|
||||||
|
|||||||
@@ -22,15 +22,104 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>Here are things to try to get notifications working.</p>
|
<p>Here are ways to test notifications and get them working.</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">Test</h2>
|
<h2 class="text-xl font-semibold">Full Test</h2>
|
||||||
<p>Somehow call the service-worker self.showNotification</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">Check OS-level permissions</h2>
|
|
||||||
<div>
|
<div>
|
||||||
Walk-throughs & screenshots, maybe for all combinations of OS &
|
<p>
|
||||||
browsers.
|
If this works then you're all set.
|
||||||
|
<button
|
||||||
|
@click="sendTestWebPushMessage(true)"
|
||||||
|
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Send Yourself a Test Web Push Message (Through Push Server but
|
||||||
|
Skipping Client Filter)
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">If this app is not installed...</h2>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
For best results on mobile, install this app on your device (as
|
||||||
|
opposed to using it inside the broser app). In Chrome, it may prompt
|
||||||
|
you, and you can also look for the "Install" command in the browser
|
||||||
|
settings; on the the deskop, look for this icon in the address bar:
|
||||||
|
<img
|
||||||
|
src="../assets/help/chrome-install-pwa.png"
|
||||||
|
alt="Chrome 'install' icon"
|
||||||
|
class="ml-4"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
If "you must enable notifications"...
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Wait for about 10 seconds (for the service worker to activate), then
|
||||||
|
<button class="text-blue-500" @click="showNotificationChoice()">
|
||||||
|
click here.
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">Check App Permissions</h2>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
In Apple iOS, check "Settings" -> "Notifications", look for the Time
|
||||||
|
Safari app (or the browser you're using), and make sure notifications
|
||||||
|
are enabled.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In Android, hold on to the app icon, then select "App Info", then
|
||||||
|
"Notifications" and make sure they're enabled. If it's still a problem
|
||||||
|
then go further:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you installed the app with Chrome, make sure there are no other
|
||||||
|
tabs with it open. Here are some ways to clear caches that can mess
|
||||||
|
things up (and note that this clears out data from the installed app
|
||||||
|
-- which is good to do while the app is installed):
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li class="list-disc ml-4">
|
||||||
|
Go to Chrome "App Info", then "Storage & Cache" and "Clear Storage".
|
||||||
|
</li>
|
||||||
|
<li class="list-disc ml-4">
|
||||||
|
Go to Chrome "Settings", then "Privacy and Security" and "Clear
|
||||||
|
"Clear browsing data", then "Cookies and site data". Also make sure
|
||||||
|
the "Time Range" at the top shows "All time".
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
On a Mac, go to "Settings" and check "Notifications".
|
||||||
|
<img
|
||||||
|
src="../assets/help/mac-installed-app-settings.png"
|
||||||
|
alt="Mac app settings"
|
||||||
|
class="ml-4"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">Check Browser Permissions</h2>
|
||||||
|
<div>
|
||||||
|
<p>In Apple iOS, check Settings -> Notifications.</p>
|
||||||
|
<p>In Android, check Settings -> Notifications.</p>
|
||||||
|
|
||||||
|
You can find more details about compatibility
|
||||||
|
<a
|
||||||
|
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
|
||||||
|
class="text-blue-500"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">Check OS Permissions</h2>
|
||||||
|
<div class="px-2">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3>
|
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3>
|
||||||
<div>
|
<div>
|
||||||
@@ -39,39 +128,113 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-semibold">Mobile Phone - Google Android</h3>
|
<h3 class="text-lg font-semibold">Mobile Phone - Google Android</h3>
|
||||||
<div>See the browser section.</div>
|
<div>
|
||||||
|
We recommend Chrome. It must be version 42 or higher. Check your
|
||||||
|
version under Settings -> About Chrome.
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-semibold">Desktop - Mac</h3>
|
<h3 class="text-lg font-semibold">Desktop - Mac</h3>
|
||||||
<div>Requires Mac OS 13.</div>
|
<div>
|
||||||
</div>
|
<span>
|
||||||
</div>
|
Requires Mac OS 13; see your macOS version under Apple -> About
|
||||||
|
This Mac.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">Check browser-level permissions</h2>
|
<h3 class="text-lg font-semibold">Windows desktop</h3>
|
||||||
<p>Walk-throughs & screenshots for browser settings</p>
|
In Windows, check Settings -> Notifications.
|
||||||
<div>
|
<img
|
||||||
|
src="../assets/help/windows-system-enable-notifications.png"
|
||||||
|
alt="Windows system settings"
|
||||||
|
class="ml-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3>
|
You can find more details about compatibility
|
||||||
<div>Make sure your OS (above) supports it.</div>
|
<a
|
||||||
|
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
|
||||||
<h3 class="text-lg font-semibold">Mobile Phone - Android</h3>
|
class="text-blue-500"
|
||||||
<div>Chrome requires version 50.</div>
|
target="_blank"
|
||||||
|
>
|
||||||
|
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">Explain full reset to start again</h2>
|
<h2 class="text-xl font-semibold">Reinstall</h2>
|
||||||
<p>
|
<div>
|
||||||
Walk-throughs for clearing everything & subscribing anew to get a
|
<p>
|
||||||
message
|
If all else fails, uninstall the app, ensure all the browser tabs with
|
||||||
</p>
|
it are closed, and clear out caches and storage.
|
||||||
|
</p>
|
||||||
|
<ul class="ml-4 list-disc">
|
||||||
|
<li>
|
||||||
|
Clear cache for site. (In Chrome, go to `chrome://settings/cookies`
|
||||||
|
and "all site data and permissions"; in Firefox, go to
|
||||||
|
`about:preferences` and search for "cache" then "Manage Data", and
|
||||||
|
also manually remove the IndexedDB data if the DBs still show.)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Clear notification permission. (in Chrome, go to
|
||||||
|
`chrome://settings/content/notifications`; in Firefox, go to
|
||||||
|
`about:preferences` and search for "notifications".)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Unregister service worker. (in Chrome, go to
|
||||||
|
`chrome://serviceworker-internals/`; in Firefox, go to
|
||||||
|
`about:serviceworkers`.)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Clear Cache Storage. (in Chrome, in dev tools under Application; in
|
||||||
|
Firefox, in dev tools under Storage.)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>Then reinstall the app.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">Auto-detection</h2>
|
<button
|
||||||
<p>Show results of auto-detection whether they're turned on</p>
|
@click="showTestNotification()"
|
||||||
|
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Send Test Notification Directly to Device (Not Through Push Server)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="alertWebPushSubscription()"
|
||||||
|
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Show Web Push Subscription Info
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="sendTestWebPushMessage(true)"
|
||||||
|
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Send Yourself a Test Web Push Message (Through Push Server but Skipping
|
||||||
|
Client Filter)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="sendTestWebPushMessage()"
|
||||||
|
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Send Yourself a Test Web Push Message (Through Push Server and Client
|
||||||
|
Filter)
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import axios from "axios";
|
||||||
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";
|
||||||
|
import { db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const Buffer = require("buffer/").Buffer;
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -83,5 +246,156 @@ interface Notification {
|
|||||||
@Component({ components: { QuickNav } })
|
@Component({ components: { QuickNav } })
|
||||||
export default class HelpNotificationsView extends Vue {
|
export default class HelpNotificationsView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
subscription: PushSubscription | null = null;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
this.subscription = await registration.pushManager.getSubscription();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Mount error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alertWebPushSubscription() {
|
||||||
|
console.log(
|
||||||
|
"Web push subscription:",
|
||||||
|
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
|
||||||
|
);
|
||||||
|
alert(JSON.stringify(this.subscription));
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTestWebPushMessage(skipFilter: boolean = false) {
|
||||||
|
if (!this.subscription) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Not Subscribed",
|
||||||
|
text: "You must enable notifications before testing the web push.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
|
||||||
|
if (settings?.webPushServer) {
|
||||||
|
pushUrl = settings.webPushServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
|
||||||
|
// This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
|
||||||
|
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
|
||||||
|
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
|
||||||
|
|
||||||
|
const auth = Buffer.from(this.subscription.getKey("auth"));
|
||||||
|
const authB64 = auth
|
||||||
|
.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=+$/, "");
|
||||||
|
const p256dh = Buffer.from(this.subscription.getKey("p256dh"));
|
||||||
|
const p256dhB64 = p256dh
|
||||||
|
.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=+$/, "");
|
||||||
|
const newPayload = {
|
||||||
|
endpoint: this.subscription.endpoint,
|
||||||
|
keys: {
|
||||||
|
auth: authB64,
|
||||||
|
p256dh: p256dhB64,
|
||||||
|
},
|
||||||
|
message: `Test, where you will see this message ${
|
||||||
|
skipFilter ? "un" : ""
|
||||||
|
}filtered.`,
|
||||||
|
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
|
||||||
|
};
|
||||||
|
console.log("Sending a test web push message:", newPayload);
|
||||||
|
const payloadStr = JSON.stringify(newPayload);
|
||||||
|
const response = await axios.post(
|
||||||
|
pushUrl + "/web-push/send-test",
|
||||||
|
payloadStr,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Got response from web push server:", response);
|
||||||
|
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Test Web Push Sent",
|
||||||
|
text: "Check your device for the test web push message, depending on the filtering you chose.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Got an error sending test notification:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Sending Test",
|
||||||
|
text: "Got an error sending the test web push notification.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showTestNotification() {
|
||||||
|
const TEST_NOTIFICATION_TITLE = "It Worked";
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then((registration) => {
|
||||||
|
return registration.showNotification(TEST_NOTIFICATION_TITLE, {
|
||||||
|
body: "This is your test notification.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Sent",
|
||||||
|
text: `A notification was triggered, so one should show on your device entitled '${TEST_NOTIFICATION_TITLE}'.`,
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Got a notification error:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Failed",
|
||||||
|
text: "Got an error sending a notification.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotificationChoice() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "notification-permission",
|
||||||
|
title: "", // unused, only here to satisfy type check
|
||||||
|
text: "", // unused, only here to satisfy type check
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -30,16 +30,17 @@
|
|||||||
<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 giving 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 see what people have given, and also recognize
|
||||||
leaves a permanent record -- one that came from you, and the recipient
|
gifts you've seen, in a way that leaves a permanent record -- one that
|
||||||
can prove it was for them. This is personally gratifying, but it extends
|
came from you, and the recipient can prove it was for them. This is
|
||||||
to broader work: volunteers can get confirmation of activity and
|
personally gratifying, but it extends to broader work: volunteers get
|
||||||
selectively show off their contributions and network.
|
confirmation of activity, and selectively show off their contributions
|
||||||
|
and network.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You can also record projects and plans and invite others to collaborate.
|
You can show giving and also offer help to ideas, based on others'
|
||||||
Soon you'll be able to see when others are interested and see how much
|
willingness to help out, too. You can record your own ideas and invite
|
||||||
they're willing to contribute, even if there are conditions.
|
others to collaborate.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This app uses the power of cryptography to build a reputation, recording
|
This app uses the power of cryptography to build a reputation, recording
|
||||||
@@ -181,6 +182,15 @@
|
|||||||
different page.
|
different page.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
Where do I get help with notifications?
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<router-link class="text-blue-500" to="/help-notifications"
|
||||||
|
>Here.</router-link
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
How do I access even more functionality?
|
How do I access even more functionality?
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Home"></QuickNav>
|
<QuickNav selected="Home"></QuickNav>
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<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">
|
||||||
@@ -8,6 +10,21 @@
|
|||||||
|
|
||||||
<!-- show the actions for recognizing a give -->
|
<!-- show the actions for recognizing a give -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
|
<div
|
||||||
|
v-if="!isInstalled()"
|
||||||
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
You should install this as an app.
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'help-notifications' }"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
|
Go here for instructions.
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!activeDid"
|
v-if="!activeDid"
|
||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||||
@@ -146,6 +163,7 @@ import EntityIcon from "@/components/EntityIcon.vue";
|
|||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
@@ -166,7 +184,13 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { GiftedDialog, QuickNav, EntityIcon, InfiniteScroll },
|
components: {
|
||||||
|
GiftedDialog,
|
||||||
|
QuickNav,
|
||||||
|
EntityIcon,
|
||||||
|
InfiniteScroll,
|
||||||
|
TopMessage,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class HomeView extends Vue {
|
export default class HomeView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
@@ -225,6 +249,7 @@ export default class HomeView extends Vue {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.log("Error retrieving settings and/or feed.", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -232,13 +257,25 @@ export default class HomeView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
err.userMessage ||
|
err.userMessage ||
|
||||||
"There was an error retrieving the latest sweet, sweet action.",
|
"There was an error retrieving your settings and/or the latest activity.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// from https://benborgers.com/posts/pwa-detect-installed
|
||||||
|
isInstalled() {
|
||||||
|
// For iOS
|
||||||
|
if ("standalone" in window.navigator) return true;
|
||||||
|
|
||||||
|
// For Android
|
||||||
|
if (window.matchMedia("(display-mode: standalone)").matches) return true;
|
||||||
|
|
||||||
|
// If neither is true, it's not installed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public async buildHeaders() {
|
public async buildHeaders() {
|
||||||
const headers: HeadersInit = {
|
const headers: HeadersInit = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav />
|
<QuickNav />
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
@@ -46,6 +48,7 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline"
|
class="underline"
|
||||||
>Map View
|
>Map View
|
||||||
|
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="url">
|
<div v-if="url">
|
||||||
@@ -282,6 +285,7 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
|
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import OfferDialog from "@/components/OfferDialog.vue";
|
import OfferDialog from "@/components/OfferDialog.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
@@ -306,7 +310,7 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
|
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav, TopMessage },
|
||||||
})
|
})
|
||||||
export default class ProjectViewView extends Vue {
|
export default class ProjectViewView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Projects"></QuickNav>
|
<QuickNav selected="Projects"></QuickNav>
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- 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">
|
||||||
@@ -79,6 +81,7 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { ProjectData } from "@/libs/endorserServer";
|
import { ProjectData } from "@/libs/endorserServer";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
@@ -89,7 +92,7 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { InfiniteScroll, QuickNav, EntityIcon },
|
components: { InfiniteScroll, QuickNav, EntityIcon, TopMessage },
|
||||||
})
|
})
|
||||||
export default class ProjectsView extends Vue {
|
export default class ProjectsView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
@@ -122,8 +125,8 @@ export default class ProjectsView extends Vue {
|
|||||||
if (resp.status === 200 || !resp.data.data) {
|
if (resp.status === 200 || !resp.data.data) {
|
||||||
const plans: ProjectData[] = resp.data.data;
|
const plans: ProjectData[] = resp.data.data;
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, rowid } = plan;
|
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||||
this.projects.push({ name, description, handleId, rowid });
|
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Bad server response & data:", resp.status, resp.data);
|
console.log("Bad server response & data:", resp.status, resp.data);
|
||||||
|
|||||||
@@ -4,62 +4,121 @@ importScripts(
|
|||||||
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
|
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
|
||||||
);
|
);
|
||||||
|
|
||||||
self.addEventListener("install", (event) => {
|
function logConsoleAndDb(message, arg1, arg2) {
|
||||||
console.log("Adding event listener for:", event);
|
// in chrome://serviceworker-internals note that the arg1 and arg2 here will show as "[object Object]" in that page but will show as expandable objects in the console
|
||||||
importScripts(
|
console.log(`${new Date().toISOString()} ${message}`, arg1, arg2);
|
||||||
|
if (self.appendDailyLog) {
|
||||||
|
let fullMessage = `${new Date().toISOString()} ${message}`;
|
||||||
|
if (arg1) {
|
||||||
|
fullMessage += `\n${JSON.stringify(arg1)}`;
|
||||||
|
}
|
||||||
|
if (arg2) {
|
||||||
|
fullMessage += `\n${JSON.stringify(arg2)}`;
|
||||||
|
}
|
||||||
|
self.appendDailyLog(fullMessage);
|
||||||
|
} else {
|
||||||
|
// sometimes we get the error: "Uncaught TypeError: self.appendDailyLog is not a function"
|
||||||
|
console.log("Not logging to DB because self.appendDailyLog doesn't exist.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener("install", async (event) => {
|
||||||
|
console.log("Service worker got install event. Importing scripts...", event);
|
||||||
|
await importScripts(
|
||||||
"safari-notifications.js",
|
"safari-notifications.js",
|
||||||
"nacl.js",
|
"nacl.js",
|
||||||
"noble-curves.js",
|
"noble-curves.js",
|
||||||
"noble-hashes.js",
|
"noble-hashes.js",
|
||||||
);
|
);
|
||||||
|
// this should now be available
|
||||||
|
logConsoleAndDb("Service worker imported all scripts.");
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
logConsoleAndDb("Service worker is activating...", event);
|
||||||
|
// see https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
|
||||||
|
// and https://web.dev/articles/service-worker-lifecycle#clientsclaim
|
||||||
|
event.waitUntil(clients.claim());
|
||||||
|
logConsoleAndDb("Service worker is activated.");
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("push", function (event) {
|
self.addEventListener("push", function (event) {
|
||||||
|
let text = null;
|
||||||
|
if (event.data) {
|
||||||
|
text = event.data.text();
|
||||||
|
}
|
||||||
|
logConsoleAndDb("Service worker received a push event.", text, event);
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
let payload;
|
let payload;
|
||||||
if (event.data) {
|
if (text) {
|
||||||
payload = JSON.parse(event.data.text());
|
try {
|
||||||
|
payload = JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
// don't use payload since it is not JSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a special value that tells the service worker to trigger its daily check.
|
||||||
|
// See https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/c1ed026662e754348a5f91542680bd4f57e5b81e/app.py#L217
|
||||||
|
const DAILY_UPDATE_TITLE = "DAILY_CHECK";
|
||||||
|
|
||||||
|
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
|
||||||
|
// This is shared with the notification-test code and should be a constant. Look for the same name in HelpNotificationsView.vue
|
||||||
|
// Make sure it is something other than the DAILY_UPDATE_TITLE.
|
||||||
|
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
|
||||||
|
|
||||||
|
let title;
|
||||||
|
let message = "Got some empty message.";
|
||||||
|
if (payload && payload.title == DIRECT_PUSH_TITLE) {
|
||||||
|
// skip any search logic and show the message directly
|
||||||
|
title = "Direct Notification";
|
||||||
|
message = payload.message || "No details were provided.";
|
||||||
|
} else {
|
||||||
|
// any other title will run through regular filtering logic
|
||||||
|
if (payload && payload.title === DAILY_UPDATE_TITLE) {
|
||||||
|
title = "Daily Update";
|
||||||
|
} else {
|
||||||
|
title = payload.title || "Update";
|
||||||
|
}
|
||||||
|
message = await self.getNotificationCount();
|
||||||
}
|
}
|
||||||
const message = await self.getNotificationCount();
|
|
||||||
if (message) {
|
if (message) {
|
||||||
console.log("Will notify user:", message);
|
|
||||||
const title = payload ? payload.title : "Custom Title";
|
|
||||||
const options = {
|
const options = {
|
||||||
body: message,
|
body: message,
|
||||||
icon: payload ? payload.icon : "icon.png",
|
icon: payload ? payload.icon : "icon.png",
|
||||||
badge: payload ? payload.badge : "badge.png",
|
badge: payload ? payload.badge : "badge.png",
|
||||||
};
|
};
|
||||||
await self.registration.showNotification(title, options);
|
await self.registration.showNotification(title, options);
|
||||||
|
logConsoleAndDb("Notified user:", options);
|
||||||
} else {
|
} else {
|
||||||
console.log("No notification message, so will not tell the user.");
|
logConsoleAndDb("No notification message.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing the push event:", error);
|
logConsoleAndDb("Error with push event", event, error);
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("message", (event) => {
|
self.addEventListener("message", (event) => {
|
||||||
|
logConsoleAndDb("Service worker got a message...", event);
|
||||||
if (event.data && event.data.type === "SEND_LOCAL_DATA") {
|
if (event.data && event.data.type === "SEND_LOCAL_DATA") {
|
||||||
self.secret = event.data.data;
|
self.secret = event.data.data;
|
||||||
event.ports[0].postMessage({ success: true });
|
event.ports[0].postMessage({ success: true });
|
||||||
}
|
}
|
||||||
});
|
logConsoleAndDb("Service worker posted a message.");
|
||||||
|
|
||||||
self.addEventListener("activate", (event) => {
|
|
||||||
event.waitUntil(clients.claim());
|
|
||||||
console.log("Service worker activated", event);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("fetch", (event) => {
|
self.addEventListener("fetch", (event) => {
|
||||||
console.log("Got fetch event", event.request);
|
logConsoleAndDb("Service worker got fetch event.", event);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("error", (event) => {
|
self.addEventListener("error", (event) => {
|
||||||
console.error("Error in Service Worker:", event.message);
|
logConsoleAndDb("Service worker error", event);
|
||||||
|
console.error("Full Error:", event);
|
||||||
|
console.error("Message:", event.message);
|
||||||
console.error("File:", event.filename);
|
console.error("File:", event.filename);
|
||||||
console.error("Line:", event.lineno);
|
console.error("Line:", event.lineno);
|
||||||
console.error("Column:", event.colno);
|
console.error("Column:", event.colno);
|
||||||
|
|||||||
@@ -395,12 +395,42 @@ async function setMostRecentNotified(id) {
|
|||||||
data["lastNotifiedClaimId"] = id;
|
data["lastNotifiedClaimId"] = id;
|
||||||
await updateRecord(store, data);
|
await updateRecord(store, data);
|
||||||
} else {
|
} else {
|
||||||
console.error("IndexedDB settings record not found.");
|
console.error(
|
||||||
|
"safari-notifications setMostRecentNotified IndexedDB settings record not found",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.oncomplete = () => db.close();
|
transaction.oncomplete = () => db.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("IndexedDB error:", error);
|
console.error(
|
||||||
|
"safari-notifications setMostRecentNotified IndexedDB error",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appendDailyLog(message) {
|
||||||
|
try {
|
||||||
|
const db = await openIndexedDB("TimeSafari");
|
||||||
|
const transaction = db.transaction("logs", "readwrite");
|
||||||
|
const store = transaction.objectStore("logs");
|
||||||
|
// will only keep one day's worth of logs
|
||||||
|
const todayKey = new Date().toDateString();
|
||||||
|
const previous = await getRecord(store, todayKey);
|
||||||
|
if (!previous) {
|
||||||
|
await store.clear(); // clear out anything older than today
|
||||||
|
}
|
||||||
|
let fullMessage = (previous && previous.message) || "";
|
||||||
|
if (fullMessage) {
|
||||||
|
fullMessage += "\n";
|
||||||
|
}
|
||||||
|
fullMessage += message;
|
||||||
|
await updateRecord(store, { date: todayKey, message: fullMessage });
|
||||||
|
transaction.oncomplete = () => db.close();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("safari-notifications logMessage IndexedDB error", error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,6 +450,7 @@ function getRecord(store, key) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note that this assumes there is only one record in the store.
|
||||||
function updateRecord(store, data) {
|
function updateRecord(store, data) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.put(data);
|
const request = store.put(data);
|
||||||
@@ -430,20 +461,23 @@ function updateRecord(store, data) {
|
|||||||
|
|
||||||
async function fetchAllAccounts() {
|
async function fetchAllAccounts() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let openRequest = indexedDB.open("TimeSafariAccounts");
|
const openRequest = indexedDB.open("TimeSafariAccounts");
|
||||||
|
|
||||||
openRequest.onupgradeneeded = function (event) {
|
openRequest.onupgradeneeded = function (event) {
|
||||||
let db = event.target.result;
|
const db = event.target.result;
|
||||||
if (!db.objectStoreNames.contains("accounts")) {
|
if (!db.objectStoreNames.contains("accounts")) {
|
||||||
db.createObjectStore("accounts", { keyPath: "id" });
|
db.createObjectStore("accounts", { keyPath: "id" });
|
||||||
}
|
}
|
||||||
|
if (!db.objectStoreNames.contains("worker_log")) {
|
||||||
|
db.createObjectStore("worker_log");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
openRequest.onsuccess = function (event) {
|
openRequest.onsuccess = function (event) {
|
||||||
let db = event.target.result;
|
const db = event.target.result;
|
||||||
let transaction = db.transaction("accounts", "readonly");
|
const transaction = db.transaction("accounts", "readonly");
|
||||||
let objectStore = transaction.objectStore("accounts");
|
const objectStore = transaction.objectStore("accounts");
|
||||||
let getAllRequest = objectStore.getAll();
|
const getAllRequest = objectStore.getAll();
|
||||||
|
|
||||||
getAllRequest.onsuccess = function () {
|
getAllRequest.onsuccess = function () {
|
||||||
resolve(getAllRequest.result);
|
resolve(getAllRequest.result);
|
||||||
@@ -460,78 +494,77 @@ async function fetchAllAccounts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getNotificationCount() {
|
async function getNotificationCount() {
|
||||||
let secret = null;
|
|
||||||
let accounts = [];
|
let accounts = [];
|
||||||
let result = null;
|
let result = null;
|
||||||
if ("secret" in self) {
|
// 1 is our master settings ID; see MASTER_SETTINGS_KEY
|
||||||
secret = self.secret;
|
const settings = await getSettingById(1);
|
||||||
const secretUint8Array = self.decodeBase64(secret);
|
let lastNotifiedClaimId = null;
|
||||||
// 1 is our master settings ID; see MASTER_SETTINGS_KEY
|
if ("lastNotifiedClaimId" in settings) {
|
||||||
const settings = await getSettingById(1);
|
lastNotifiedClaimId = settings["lastNotifiedClaimId"];
|
||||||
let lastNotifiedClaimId = null;
|
}
|
||||||
if ("lastNotifiedClaimId" in settings) {
|
const activeDid = settings["activeDid"];
|
||||||
lastNotifiedClaimId = settings["lastNotifiedClaimId"];
|
accounts = await fetchAllAccounts();
|
||||||
}
|
let activeAccount = null;
|
||||||
const activeDid = settings["activeDid"];
|
for (let i = 0; i < accounts.length; i++) {
|
||||||
accounts = await fetchAllAccounts();
|
if (accounts[i]["did"] == activeDid) {
|
||||||
let did = null;
|
activeAccount = accounts[i];
|
||||||
for (var i = 0; i < accounts.length; i++) {
|
break;
|
||||||
let account = accounts[i];
|
|
||||||
let did = account["did"];
|
|
||||||
if (did == activeDid) {
|
|
||||||
let publicKeyHex = account["publicKeyHex"];
|
|
||||||
let identity = account["identity"];
|
|
||||||
const messageWithNonceAsUint8Array = self.decodeBase64(identity);
|
|
||||||
const nonce = messageWithNonceAsUint8Array.slice(0, 24);
|
|
||||||
const message = messageWithNonceAsUint8Array.slice(24, identity.length);
|
|
||||||
const decoder = new TextDecoder("utf-8");
|
|
||||||
const decrypted = self.secretbox.open(message, nonce, secretUint8Array);
|
|
||||||
|
|
||||||
const msg = decoder.decode(decrypted);
|
|
||||||
const identifier = JSON.parse(JSON.parse(msg));
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
};
|
|
||||||
|
|
||||||
headers["Authorization"] = "Bearer " + (await accessToken(identifier));
|
|
||||||
|
|
||||||
let response = await fetch(
|
|
||||||
settings["apiServer"] + "/api/v2/report/claims",
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: headers,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (response.status == 200) {
|
|
||||||
let json = await response.json();
|
|
||||||
let claims = json["data"];
|
|
||||||
let newClaims = 0;
|
|
||||||
for (var i = 0; i < claims.length; i++) {
|
|
||||||
let claim = claims[i];
|
|
||||||
if (claim["id"] === lastNotifiedClaimId) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
newClaims++;
|
|
||||||
}
|
|
||||||
if (newClaims > 0) {
|
|
||||||
result = `There are ${newClaims} new activities on TimeSafari`;
|
|
||||||
}
|
|
||||||
const most_recent_notified = claims[0]["id"];
|
|
||||||
await setMostRecentNotified(most_recent_notified);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"The service worker got a bad response status when fetching claims:",
|
|
||||||
response.status,
|
|
||||||
response,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const identity = activeAccount && activeAccount["identity"];
|
||||||
|
if (identity && "secret" in self) {
|
||||||
|
const secret = self.secret;
|
||||||
|
const secretUint8Array = self.decodeBase64(secret);
|
||||||
|
const messageWithNonceAsUint8Array = self.decodeBase64(identity);
|
||||||
|
const nonce = messageWithNonceAsUint8Array.slice(0, 24);
|
||||||
|
const message = messageWithNonceAsUint8Array.slice(24, identity.length);
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
const decrypted = self.secretbox.open(message, nonce, secretUint8Array);
|
||||||
|
const msg = decoder.decode(decrypted);
|
||||||
|
const identifier = JSON.parse(JSON.parse(msg));
|
||||||
|
|
||||||
|
headers["Authorization"] = "Bearer " + (await accessToken(identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
settings["apiServer"] + "/api/v2/report/claims",
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: headers,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response.status == 200) {
|
||||||
|
const json = await response.json();
|
||||||
|
const claims = json["data"];
|
||||||
|
let newClaims = 0;
|
||||||
|
for (let i = 0; i < claims.length; i++) {
|
||||||
|
const claim = claims[i];
|
||||||
|
if (claim["id"] === lastNotifiedClaimId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
newClaims++;
|
||||||
|
}
|
||||||
|
if (newClaims > 0) {
|
||||||
|
result = `There are ${newClaims} new activities on Time Safari`;
|
||||||
|
}
|
||||||
|
const most_recent_notified = claims[0]["id"];
|
||||||
|
await setMostRecentNotified(most_recent_notified);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"safari-notifications getNotificationsCount got a bad response status when fetching claims",
|
||||||
|
response.status,
|
||||||
|
response,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.appendDailyLog = appendDailyLog;
|
||||||
self.getNotificationCount = getNotificationCount;
|
self.getNotificationCount = getNotificationCount;
|
||||||
self.decodeBase64 = decodeBase64;
|
self.decodeBase64 = decodeBase64;
|
||||||
|
|||||||