Compare commits
48 Commits
1.0.0
...
gifting-pe
| Author | SHA1 | Date | |
|---|---|---|---|
| 25512d3db1 | |||
|
|
bf9fee7ee9 | ||
|
|
08c46a27d3 | ||
|
|
c9405839c3 | ||
| 0e6a9c4f89 | |||
|
|
b6278ca148 | ||
|
|
d8e237f8cb | ||
|
|
4b539ccc55 | ||
|
|
ea49173885 | ||
|
|
447a7cb089 | ||
|
|
c0ddba8898 | ||
|
|
fe4ae90849 | ||
|
|
ce04312baa | ||
|
|
a8cc480960 | ||
|
|
357822d713 | ||
| 3baa6633a6 | |||
| bda98eb632 | |||
| eea1cb995a | |||
| 276e0a741b | |||
| e46d6133fb | |||
| 94994a7251 | |||
| 838723c26b | |||
| bb6eb92ba1 | |||
| a997d4cb92 | |||
| 73733345ff | |||
|
|
ca22161f12 | ||
|
|
d3b80fbe47 | ||
|
|
0342c872f4 | ||
|
|
a7e65b3b49 | ||
| 3118f71320 | |||
| d12f23aa81 | |||
| e9a8a3c1e7 | |||
| 1e0efe6011 | |||
| 16557f1e4b | |||
| c4a54967bc | |||
| 20ade415dc | |||
| 6689520270 | |||
| 3fd6c2b80d | |||
|
|
eb7605991c | ||
| fa21660fd1 | |||
|
|
df1c1f0186 | ||
|
|
3daf1c8a5c | ||
|
|
7eefee1ea5 | ||
|
|
140c36a416 | ||
|
|
988244b7ae | ||
|
|
4b355a5448 | ||
|
|
b511f9cd24 | ||
|
|
579cecbe6e |
16
BUILDING.md
16
BUILDING.md
@@ -41,6 +41,7 @@ Install dependencies:
|
||||
1. Run the production build:
|
||||
|
||||
```bash
|
||||
rm -rf dist
|
||||
npm run build:web
|
||||
```
|
||||
|
||||
@@ -62,11 +63,13 @@ Install dependencies:
|
||||
|
||||
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
||||
|
||||
* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build`
|
||||
|
||||
* Commit everything (since the commit hash is used the app).
|
||||
|
||||
* Put the commit hash in the changelog (which will help you remember to bump the version in the step later).
|
||||
|
||||
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 1.0.0 && git push origin 1.0.0`.
|
||||
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 1.0.2 && git push origin 1.0.2`.
|
||||
|
||||
* For test, build the app (because test server is not yet set up to build):
|
||||
|
||||
@@ -90,13 +93,13 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.
|
||||
|
||||
* `pkgx +npm sh`
|
||||
|
||||
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.5.9 && npm install && npm run build:web && cd -`
|
||||
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 1.0.2 && npm install && npm run build:web && cd -`
|
||||
|
||||
(The plain `npm run build:web` uses the .env.production file.)
|
||||
|
||||
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
||||
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev-2 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
||||
|
||||
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
||||
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, commit, and push. Also record what version is on production.
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
@@ -359,12 +362,9 @@ Prerequisites: macOS with Xcode installed
|
||||
4. Bump the version to match Android & package.json:
|
||||
|
||||
```
|
||||
cd ios/App
|
||||
xcrun agvtool new-version 33
|
||||
cd ios/App && xcrun agvtool new-version 35 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.2;/g" App.xcodeproj/project.pbxproj && cd -
|
||||
# Unfortunately this edits Info.plist directly.
|
||||
#xcrun agvtool new-marketing-version 0.4.5
|
||||
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.7;/g" > temp && mv temp App.xcodeproj/project.pbxproj
|
||||
cd -
|
||||
```
|
||||
|
||||
5. Open the project in Xcode:
|
||||
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -6,12 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d
|
||||
### Added
|
||||
- Version on feed title
|
||||
|
||||
## [1.0.0] - 2025.06.20 - 9b69c0b22c7e3ac0584219f5ac434a02bda2e01b
|
||||
|
||||
## [1.0.1] - 2025.06.20
|
||||
### Added
|
||||
- Allow a user to block someone else's content from view
|
||||
|
||||
|
||||
## [1.0.0] - 2025.06.20 - 5aa693de6337e5dbb278bfddc6bd39094bc14f73
|
||||
### Added
|
||||
- Web-oriented migration from IndexedDB to SQLite
|
||||
|
||||
|
||||
## [0.5.8]
|
||||
### Added
|
||||
- /deep-link/ path for URLs that are shared with people
|
||||
### Changed
|
||||
- External links now go to /deep-link/...
|
||||
- Feed visuals now have arrow imagery from giver to receiver
|
||||
|
||||
|
||||
## [0.4.7]
|
||||
### Fixed
|
||||
- Cameras everywhere
|
||||
|
||||
@@ -31,8 +31,8 @@ android {
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 33
|
||||
versionName "0.5.7"
|
||||
versionCode 35
|
||||
versionName "1.0.2"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -100,6 +100,7 @@ try {
|
||||
- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas
|
||||
- `src/services/deepLinks.ts`: Deep link processing service
|
||||
- `src/main.capacitor.ts`: Capacitor integration
|
||||
- `src/views/DeepLinkRedirectView.vue`: Page to handle links to both mobile and web
|
||||
|
||||
## Type Safety Examples
|
||||
|
||||
|
||||
@@ -403,7 +403,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -413,7 +413,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.5.7;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -430,7 +430,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -440,7 +440,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.5.7;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3-beta",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "timesafari",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3-beta",
|
||||
"dependencies": {
|
||||
"@capacitor-community/sqlite": "6.0.2",
|
||||
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3-beta",
|
||||
"description": "Time Safari Application",
|
||||
"author": {
|
||||
"name": "Time Safari Team"
|
||||
|
||||
@@ -1,99 +1,534 @@
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h1 class="text-xl font-bold text-center mb-4">
|
||||
{{ customTitle }}
|
||||
</h1>
|
||||
<input
|
||||
v-model="description"
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
:placeholder="prompt || 'What was given?'"
|
||||
/>
|
||||
<div class="flex flex-row justify-center">
|
||||
<span
|
||||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
|
||||
@click="changeUnitCode()"
|
||||
<!-- Step 1: Giver -->
|
||||
<div v-show="firstStep" id="sectionGiftedGiver">
|
||||
<label class="block font-bold mb-4">
|
||||
{{
|
||||
stepType === "recipient"
|
||||
? "Choose who received the gift:"
|
||||
: showProjects
|
||||
? "Choose a project benefitted from:"
|
||||
: "Choose a person received from:"
|
||||
}}
|
||||
</label>
|
||||
|
||||
<!-- Unified Quick-pick grid for People and Projects -->
|
||||
<ul
|
||||
:class="
|
||||
shouldShowProjects
|
||||
? 'grid grid-cols-3 md:grid-cols-4 gap-x-2 gap-y-4 text-center mb-4'
|
||||
: 'grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-2 gap-y-4 text-center mb-4'
|
||||
"
|
||||
>
|
||||
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
|
||||
</span>
|
||||
<div
|
||||
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="amountInput === '0' ? null : decrement()"
|
||||
>
|
||||
<font-awesome icon="chevron-left" />
|
||||
</div>
|
||||
<input
|
||||
id="inputGivenAmount"
|
||||
v-model="amountInput"
|
||||
type="number"
|
||||
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
|
||||
/>
|
||||
<div
|
||||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="increment()"
|
||||
>
|
||||
<font-awesome icon="chevron-right" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-center">
|
||||
<span>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'gifted-details',
|
||||
query: {
|
||||
amountInput,
|
||||
description,
|
||||
giverDid: giver?.did,
|
||||
giverName: giver?.name,
|
||||
offerId,
|
||||
fulfillsProjectId: toProjectId,
|
||||
providerProjectId: fromProjectId,
|
||||
recipientDid: receiver?.did,
|
||||
recipientName: receiver?.name,
|
||||
unitCode,
|
||||
},
|
||||
}"
|
||||
class="text-blue-500"
|
||||
>
|
||||
Photo & more options ...
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-center mb-2 mt-6 italic">
|
||||
Sign & Send to publish to the world
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="pl-2 text-blue-500 cursor-pointer"
|
||||
@click="explainData()"
|
||||
/>
|
||||
</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<template v-if="shouldShowProjects">
|
||||
<!-- show projects -->
|
||||
<li
|
||||
v-for="project in projects.slice(0, 7)"
|
||||
:key="project.handleId"
|
||||
class="cursor-pointer"
|
||||
@click="
|
||||
stepType === 'recipient'
|
||||
? selectRecipientProject(project)
|
||||
: selectProject(project)
|
||||
"
|
||||
>
|
||||
<div class="relative w-fit mx-auto mb-1">
|
||||
<ProjectIcon
|
||||
:entity-id="project.handleId"
|
||||
:icon-size="48"
|
||||
:image-url="project.image"
|
||||
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<h3
|
||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
{{ project.name }}
|
||||
</h3>
|
||||
<div class="text-xs text-slate-500 truncate">
|
||||
<font-awesome icon="user" class="fa-fw text-slate-400" />
|
||||
{{
|
||||
didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
|
||||
}}
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="projects.length === 0"
|
||||
class="text-xs text-slate-500 italic col-span-full"
|
||||
>
|
||||
(No projects found.)
|
||||
</li>
|
||||
<li v-if="projects.length > 0">
|
||||
<router-link :to="{ name: 'discover' }" class="cursor-pointer">
|
||||
<font-awesome
|
||||
icon="circle-right"
|
||||
class="text-blue-500 text-5xl mb-1"
|
||||
/>
|
||||
<h3
|
||||
class="text-xs text-slate-400 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
Show All
|
||||
</h3>
|
||||
</router-link>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- show people (contacts) -->
|
||||
<li
|
||||
v-if="
|
||||
stepType === 'recipient' ||
|
||||
(stepType === 'giver' && isFromProjectView)
|
||||
"
|
||||
:class="{
|
||||
'cursor-pointer': !wouldCreateConflict(activeDid),
|
||||
'cursor-not-allowed opacity-50': wouldCreateConflict(activeDid)
|
||||
}"
|
||||
@click="
|
||||
!wouldCreateConflict(activeDid) &&
|
||||
(stepType === 'recipient'
|
||||
? selectRecipient({ did: activeDid, name: 'You' })
|
||||
: selectGiver({ did: activeDid, name: 'You' }))
|
||||
"
|
||||
>
|
||||
<font-awesome
|
||||
:class="{
|
||||
'text-blue-500 text-5xl mb-1': !wouldCreateConflict(activeDid),
|
||||
'text-slate-400 text-5xl mb-1': wouldCreateConflict(activeDid)
|
||||
}"
|
||||
icon="hand"
|
||||
/>
|
||||
<h3
|
||||
:class="{
|
||||
'text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden': !wouldCreateConflict(activeDid),
|
||||
'text-xs text-slate-400 font-medium text-ellipsis whitespace-nowrap overflow-hidden': wouldCreateConflict(activeDid)
|
||||
}"
|
||||
>
|
||||
You
|
||||
</h3>
|
||||
</li>
|
||||
<li
|
||||
class="cursor-pointer"
|
||||
@click="
|
||||
stepType === 'recipient' ? selectRecipient() : selectGiver()
|
||||
"
|
||||
>
|
||||
<font-awesome
|
||||
icon="circle-question"
|
||||
class="text-slate-400 text-5xl mb-1"
|
||||
/>
|
||||
<h3
|
||||
class="text-xs text-slate-400 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
(Unnamed)
|
||||
</h3>
|
||||
</li>
|
||||
<li
|
||||
v-if="allContacts.length === 0"
|
||||
class="text-xs text-slate-500 italic col-span-full"
|
||||
>
|
||||
(Add friends to see more people worthy of recognition.)
|
||||
</li>
|
||||
<li
|
||||
v-for="contact in allContacts.slice(0, 10)"
|
||||
:key="contact.did"
|
||||
:class="{
|
||||
'cursor-pointer': !wouldCreateConflict(contact.did),
|
||||
'cursor-not-allowed opacity-50': wouldCreateConflict(contact.did)
|
||||
}"
|
||||
@click="
|
||||
!wouldCreateConflict(contact.did) &&
|
||||
(stepType === 'recipient'
|
||||
? selectRecipient(contact)
|
||||
: selectGiver(contact))
|
||||
"
|
||||
>
|
||||
<div class="relative w-fit mx-auto mb-1">
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full"
|
||||
/>
|
||||
<div
|
||||
class="rounded-full bg-slate-400 absolute bottom-0 right-0 p-1 translate-x-1/3"
|
||||
>
|
||||
<font-awesome
|
||||
icon="clock"
|
||||
class="block text-white text-xs w-[1em]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3
|
||||
:class="{
|
||||
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden': !wouldCreateConflict(contact.did) && contact.name,
|
||||
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden text-slate-400 italic': !contact.name,
|
||||
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden text-slate-400': wouldCreateConflict(contact.did) && contact.name
|
||||
}"
|
||||
>
|
||||
{{ contact.name || "(No name)" }}
|
||||
</h3>
|
||||
</li>
|
||||
<li v-if="allContacts.length > 0" class="cursor-pointer">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'contact-gift',
|
||||
query: {
|
||||
stepType: stepType,
|
||||
giverEntityType: giverEntityType,
|
||||
recipientEntityType: recipientEntityType,
|
||||
...(stepType === 'giver'
|
||||
? {
|
||||
recipientProjectId: toProjectId,
|
||||
recipientProjectName: receiver?.name,
|
||||
recipientProjectImage: receiver?.image,
|
||||
recipientProjectHandleId: receiver?.handleId,
|
||||
recipientDid: receiver?.did,
|
||||
}
|
||||
: {
|
||||
giverProjectId: fromProjectId,
|
||||
giverProjectName: giver?.name,
|
||||
giverProjectImage: giver?.image,
|
||||
giverProjectHandleId: giver?.handleId,
|
||||
giverDid: giver?.did,
|
||||
}),
|
||||
fromProjectId: fromProjectId,
|
||||
toProjectId: toProjectId,
|
||||
showProjects: (showProjects || false).toString(),
|
||||
isFromProjectView: (isFromProjectView || false).toString(),
|
||||
},
|
||||
}"
|
||||
>
|
||||
<font-awesome
|
||||
icon="circle-right"
|
||||
class="text-blue-500 text-5xl mb-1"
|
||||
/>
|
||||
<h3
|
||||
class="text-xs text-slate-400 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
Show All
|
||||
</h3>
|
||||
</router-link>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
||||
@click="confirm"
|
||||
>
|
||||
Sign & Send
|
||||
</button>
|
||||
<button
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Gift -->
|
||||
<div v-show="!firstStep" id="sectionGiftedGift">
|
||||
<div class="grid grid-cols-2 gap-2 mb-4">
|
||||
<!-- Giver Button -->
|
||||
<button
|
||||
v-if="
|
||||
(giverEntityType === 'person' || giverEntityType === 'project') &&
|
||||
!(isFromProjectView && giverEntityType === 'project')
|
||||
"
|
||||
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
|
||||
@click="goBackToStep1('giver')"
|
||||
>
|
||||
<div>
|
||||
<template v-if="giverEntityType === 'project'">
|
||||
<ProjectIcon
|
||||
v-if="giver?.handleId"
|
||||
:entity-id="giver.handleId"
|
||||
:icon-size="32"
|
||||
:image-url="giver.image"
|
||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<EntityIcon
|
||||
v-if="giver?.did"
|
||||
:contact="giver"
|
||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
||||
/>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="circle-question"
|
||||
class="text-slate-400 text-3xl"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="text-start min-w-0">
|
||||
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
|
||||
{{
|
||||
giverEntityType === "project"
|
||||
? "Benefited from:"
|
||||
: "Received from:"
|
||||
}}
|
||||
</p>
|
||||
<h3
|
||||
v-if="giver?.name && giver.name !== giver.did"
|
||||
class="font-semibold truncate"
|
||||
>
|
||||
{{ giver.name }}
|
||||
</h3>
|
||||
<h3
|
||||
v-if="giver?.name && giver.name === giver.did"
|
||||
class="font-semibold truncate text-slate-400 italic"
|
||||
>
|
||||
(No name)
|
||||
</h3>
|
||||
<h3
|
||||
v-else-if="!giver?.name"
|
||||
class="font-semibold truncate text-slate-400 italic"
|
||||
>
|
||||
(Unnamed)
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p class="ms-auto text-sm text-blue-500 pe-1">
|
||||
<font-awesome icon="pen" title="Change" />
|
||||
</p>
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
|
||||
>
|
||||
<div>
|
||||
<template v-if="giverEntityType === 'project'">
|
||||
<ProjectIcon
|
||||
v-if="giver?.handleId"
|
||||
:entity-id="giver.handleId"
|
||||
:icon-size="32"
|
||||
:image-url="giver.image"
|
||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<EntityIcon
|
||||
v-if="giver?.did"
|
||||
:contact="giver"
|
||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
||||
/>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="circle-question"
|
||||
class="text-slate-400 text-3xl"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="text-start min-w-0">
|
||||
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
|
||||
{{
|
||||
giverEntityType === "project"
|
||||
? "Benefited from:"
|
||||
: "Received from:"
|
||||
}}
|
||||
</p>
|
||||
<h3
|
||||
v-if="giver?.name && giver.name !== giver.did"
|
||||
class="font-semibold truncate"
|
||||
>
|
||||
{{ giver.name }}
|
||||
</h3>
|
||||
<h3
|
||||
v-if="giver?.name && giver.name === giver.did"
|
||||
class="font-semibold truncate text-slate-400 italic"
|
||||
>
|
||||
(No name)
|
||||
</h3>
|
||||
<h3
|
||||
v-else-if="!giver?.name"
|
||||
class="font-semibold truncate text-slate-400 italic"
|
||||
>
|
||||
(Unnamed)
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p class="ms-auto text-sm text-slate-400 pe-1">
|
||||
<font-awesome icon="lock" title="Can't be changed" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Recipient Button -->
|
||||
<button
|
||||
v-if="recipientEntityType === 'person'"
|
||||
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
|
||||
@click="goBackToStep1('recipient')"
|
||||
>
|
||||
<div>
|
||||
<EntityIcon
|
||||
v-if="receiver?.did"
|
||||
:contact="receiver"
|
||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
||||
/>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="circle-question"
|
||||
class="text-slate-400 text-3xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-start min-w-0">
|
||||
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
|
||||
Given to:
|
||||
</p>
|
||||
<h3
|
||||
v-if="receiver?.name && receiver.name !== receiver.did"
|
||||
class="font-semibold truncate"
|
||||
>
|
||||
{{ receiver.name }}
|
||||
</h3>
|
||||
<h3
|
||||
v-if="receiver?.name && receiver.name === receiver.did"
|
||||
class="font-semibold truncate text-slate-400 italic"
|
||||
>
|
||||
(No name)
|
||||
</h3>
|
||||
<h3
|
||||
v-else-if="!receiver?.name"
|
||||
class="font-semibold truncate text-slate-400 italic"
|
||||
>
|
||||
(Unnamed)
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p class="ms-auto text-sm text-blue-500 pe-1">
|
||||
<font-awesome icon="pen" title="Change" />
|
||||
</p>
|
||||
</button>
|
||||
<div
|
||||
v-else-if="recipientEntityType === 'project'"
|
||||
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
|
||||
>
|
||||
<div>
|
||||
<ProjectIcon
|
||||
v-if="receiver?.handleId"
|
||||
:entity-id="receiver.handleId"
|
||||
:icon-size="32"
|
||||
:image-url="receiver.image"
|
||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-start min-w-0">
|
||||
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
|
||||
Given to project:
|
||||
</p>
|
||||
<h3
|
||||
v-if="receiver?.name"
|
||||
class="font-semibold truncate"
|
||||
>
|
||||
{{ receiver.name }}
|
||||
</h3>
|
||||
<h3
|
||||
v-else
|
||||
class="font-semibold truncate text-slate-400 italic"
|
||||
>
|
||||
(Unnamed)
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p class="ms-auto text-sm text-slate-400 pe-1">
|
||||
<font-awesome icon="lock" title="Can't be changed" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="description"
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 px-3 py-2 mb-4 placeholder:italic"
|
||||
:placeholder="prompt || 'What was given?'"
|
||||
/>
|
||||
<div class="flex mb-4">
|
||||
<button
|
||||
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="amountInput === '0' ? null : decrement()"
|
||||
>
|
||||
<font-awesome icon="chevron-left" />
|
||||
</button>
|
||||
<input
|
||||
id="inputGivenAmount"
|
||||
v-model="amountInput"
|
||||
type="number"
|
||||
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
|
||||
/>
|
||||
<button
|
||||
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="increment()"
|
||||
>
|
||||
<font-awesome icon="chevron-right" />
|
||||
</button>
|
||||
|
||||
<select
|
||||
v-model="unitCode"
|
||||
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
|
||||
>
|
||||
<option
|
||||
v-for="(displayName, code) in unitOptions"
|
||||
:key="code"
|
||||
:value="code"
|
||||
>
|
||||
{{ displayName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'gifted-details',
|
||||
query: giftedDetailsQuery,
|
||||
}"
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg mb-4"
|
||||
>
|
||||
Photo & more options…
|
||||
</router-link>
|
||||
<p class="text-center text-sm mb-4">
|
||||
<b class="font-medium">Sign & Send</b> to publish to the world
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="fa-fw text-blue-500 text-base cursor-pointer"
|
||||
@click="explainData()"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<!-- Conflict warning -->
|
||||
<div v-if="hasPersonConflict" class="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p class="text-red-700 text-sm text-center">
|
||||
<font-awesome icon="exclamation-triangle" class="fa-fw mr-1" />
|
||||
Cannot record: Same person selected as both giver and recipient
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<button
|
||||
:disabled="hasPersonConflict"
|
||||
:class="{
|
||||
'block w-full text-center text-md uppercase font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg': !hasPersonConflict,
|
||||
'block w-full text-center text-md uppercase font-bold bg-gradient-to-b from-slate-300 to-slate-500 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-400 px-1.5 py-2 rounded-lg cursor-not-allowed': hasPersonConflict
|
||||
}"
|
||||
@click="confirm"
|
||||
>
|
||||
Sign & Send
|
||||
</button>
|
||||
<button
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
import { Vue, Component, Prop, Watch } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
didInfo,
|
||||
serverMessageForUser,
|
||||
getHeaders,
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
@@ -102,13 +537,38 @@ import * as databaseUtil from "../db/databaseUtil";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
import ProjectIcon from "../components/ProjectIcon.vue";
|
||||
import { PlanData } from "../interfaces/records";
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
components: {
|
||||
EntityIcon,
|
||||
ProjectIcon,
|
||||
},
|
||||
})
|
||||
export default class GiftedDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
@Prop() fromProjectId = "";
|
||||
@Prop() toProjectId = "";
|
||||
@Prop({ default: false }) showProjects = false;
|
||||
@Prop() isFromProjectView = false;
|
||||
|
||||
@Watch("showProjects")
|
||||
onShowProjectsChange() {
|
||||
this.updateEntityTypes();
|
||||
}
|
||||
|
||||
@Watch("fromProjectId")
|
||||
onFromProjectIdChange() {
|
||||
this.updateEntityTypes();
|
||||
}
|
||||
|
||||
@Watch("toProjectId")
|
||||
onToProjectIdChange() {
|
||||
this.updateEntityTypes();
|
||||
}
|
||||
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
@@ -119,6 +579,7 @@ export default class GiftedDialog extends Vue {
|
||||
callbackOnSuccess?: (amount: number) => void = () => {};
|
||||
customTitle?: string;
|
||||
description = "";
|
||||
firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description)
|
||||
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
|
||||
offerId = "";
|
||||
prompt = "";
|
||||
@@ -128,6 +589,80 @@ export default class GiftedDialog extends Vue {
|
||||
|
||||
libsUtil = libsUtil;
|
||||
|
||||
projects: PlanData[] = [];
|
||||
|
||||
didInfo = didInfo;
|
||||
|
||||
// Computed property to help debug template logic
|
||||
get shouldShowProjects() {
|
||||
const result =
|
||||
(this.stepType === "giver" && this.giverEntityType === "project") ||
|
||||
(this.stepType === "recipient" && this.recipientEntityType === "project");
|
||||
return result;
|
||||
}
|
||||
|
||||
// Computed property to check if current selection would create a conflict
|
||||
get hasPersonConflict() {
|
||||
// Only check for conflicts when both entities are persons
|
||||
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if giver and recipient are the same person
|
||||
if (this.giver?.did && this.receiver?.did && this.giver.did === this.receiver.did) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Computed property to check if a contact would create a conflict when selected
|
||||
wouldCreateConflict(contactDid: string) {
|
||||
// Only check for conflicts when both entities are persons
|
||||
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.stepType === "giver") {
|
||||
// If selecting as giver, check if it conflicts with current recipient
|
||||
return this.receiver?.did === contactDid;
|
||||
} else if (this.stepType === "recipient") {
|
||||
// If selecting as recipient, check if it conflicts with current giver
|
||||
return this.giver?.did === contactDid;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
stepType = "giver";
|
||||
giverEntityType = "person" as "person" | "project";
|
||||
recipientEntityType = "person" as "person" | "project";
|
||||
|
||||
updateEntityTypes() {
|
||||
// Reset and set entity types based on current context
|
||||
this.giverEntityType = "person";
|
||||
this.recipientEntityType = "person";
|
||||
|
||||
// Determine entity types based on current context
|
||||
if (this.showProjects) {
|
||||
// HomeView "Project" button or ProjectViewView "Given by This"
|
||||
this.giverEntityType = "project";
|
||||
this.recipientEntityType = "person";
|
||||
} else if (this.fromProjectId) {
|
||||
// ProjectViewView "Given by This" button (project is giver)
|
||||
this.giverEntityType = "project";
|
||||
this.recipientEntityType = "person";
|
||||
} else if (this.toProjectId) {
|
||||
// ProjectViewView "Given to This" button (project is recipient)
|
||||
this.giverEntityType = "person";
|
||||
this.recipientEntityType = "project";
|
||||
} else {
|
||||
// HomeView "Person" button
|
||||
this.giverEntityType = "person";
|
||||
this.recipientEntityType = "person";
|
||||
}
|
||||
}
|
||||
|
||||
async open(
|
||||
giver?: libsUtil.GiverReceiverInputInfo,
|
||||
receiver?: libsUtil.GiverReceiverInputInfo,
|
||||
@@ -140,10 +675,14 @@ export default class GiftedDialog extends Vue {
|
||||
this.giver = giver;
|
||||
this.prompt = prompt || "";
|
||||
this.receiver = receiver;
|
||||
// if we show "given to user" selection, default checkbox to true
|
||||
this.amountInput = "0";
|
||||
this.callbackOnSuccess = callbackOnSuccess;
|
||||
this.offerId = offerId || "";
|
||||
this.firstStep = !giver;
|
||||
this.stepType = "giver";
|
||||
|
||||
// Update entity types based on current props
|
||||
this.updateEntityTypes();
|
||||
|
||||
try {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
@@ -174,7 +713,16 @@ export default class GiftedDialog extends Vue {
|
||||
this.allContacts,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
if (
|
||||
this.giverEntityType === "project" ||
|
||||
this.recipientEntityType === "project"
|
||||
) {
|
||||
await this.loadProjects();
|
||||
} else {
|
||||
// Clear projects array when not needed
|
||||
this.projects = [];
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
@@ -224,6 +772,7 @@ export default class GiftedDialog extends Vue {
|
||||
this.amountInput = "0";
|
||||
this.prompt = "";
|
||||
this.unitCode = "HUR";
|
||||
this.firstStep = true;
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
@@ -265,6 +814,20 @@ export default class GiftedDialog extends Vue {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for person conflict
|
||||
if (this.hasPersonConflict) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You cannot select the same person as both giver and recipient.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.close();
|
||||
this.$notify(
|
||||
@@ -304,20 +867,52 @@ export default class GiftedDialog extends Vue {
|
||||
unitCode: string = "HUR",
|
||||
) {
|
||||
try {
|
||||
// Determine the correct parameters based on entity types
|
||||
let fromDid: string | undefined;
|
||||
let toDid: string | undefined;
|
||||
let fulfillsProjectHandleId: string | undefined;
|
||||
let providerPlanHandleId: string | undefined;
|
||||
|
||||
if (this.giverEntityType === "project" && this.recipientEntityType === "person") {
|
||||
// Project-to-person gift
|
||||
fromDid = undefined; // No person giver
|
||||
toDid = recipientDid as string; // Person recipient
|
||||
fulfillsProjectHandleId = undefined; // No project recipient
|
||||
providerPlanHandleId = this.giver?.handleId; // Project giver
|
||||
} else if (this.giverEntityType === "person" && this.recipientEntityType === "project") {
|
||||
// Person-to-project gift
|
||||
fromDid = giverDid as string; // Person giver
|
||||
toDid = undefined; // No person recipient
|
||||
fulfillsProjectHandleId = this.toProjectId; // Project recipient
|
||||
providerPlanHandleId = undefined; // No project giver
|
||||
} else if (this.giverEntityType === "project" && this.recipientEntityType === "project") {
|
||||
// Project-to-project gift
|
||||
fromDid = undefined; // No person giver
|
||||
toDid = undefined; // No person recipient
|
||||
fulfillsProjectHandleId = this.toProjectId; // Project recipient
|
||||
providerPlanHandleId = this.giver?.handleId; // Project giver
|
||||
} else {
|
||||
// Person-to-person gift
|
||||
fromDid = giverDid as string;
|
||||
toDid = recipientDid as string;
|
||||
fulfillsProjectHandleId = undefined;
|
||||
providerPlanHandleId = undefined;
|
||||
}
|
||||
|
||||
const result = await createAndSubmitGive(
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
this.activeDid,
|
||||
giverDid as string,
|
||||
recipientDid as string,
|
||||
fromDid,
|
||||
toDid,
|
||||
description,
|
||||
amount,
|
||||
unitCode,
|
||||
this.toProjectId,
|
||||
fulfillsProjectHandleId,
|
||||
this.offerId,
|
||||
false,
|
||||
undefined,
|
||||
this.fromProjectId,
|
||||
providerPlanHandleId,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
@@ -378,6 +973,118 @@ export default class GiftedDialog extends Vue {
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
selectGiver(contact?: Contact) {
|
||||
if (contact) {
|
||||
this.giver = {
|
||||
did: contact.did,
|
||||
name: contact.name || contact.did,
|
||||
};
|
||||
} else {
|
||||
this.giver = {
|
||||
did: "",
|
||||
name: "",
|
||||
};
|
||||
}
|
||||
this.firstStep = false;
|
||||
}
|
||||
|
||||
goBackToStep1(step: string) {
|
||||
this.stepType = step;
|
||||
this.firstStep = true;
|
||||
}
|
||||
|
||||
async loadProjects() {
|
||||
try {
|
||||
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
|
||||
method: "GET",
|
||||
headers: await getHeaders(this.activeDid),
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed to load projects");
|
||||
}
|
||||
|
||||
const results = await response.json();
|
||||
if (results.data) {
|
||||
this.projects = results.data;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error loading projects:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to load projects",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
selectProject(project: PlanData) {
|
||||
this.giver = {
|
||||
name: project.name,
|
||||
image: project.image,
|
||||
handleId: project.handleId,
|
||||
};
|
||||
this.receiver = {
|
||||
did: this.activeDid,
|
||||
name: "You",
|
||||
};
|
||||
this.firstStep = false;
|
||||
}
|
||||
|
||||
selectRecipient(contact?: Contact) {
|
||||
if (contact) {
|
||||
this.receiver = {
|
||||
did: contact.did,
|
||||
name: contact.name || contact.did,
|
||||
};
|
||||
} else {
|
||||
this.receiver = {
|
||||
did: "",
|
||||
name: "",
|
||||
};
|
||||
}
|
||||
this.firstStep = false;
|
||||
}
|
||||
|
||||
selectRecipientProject(project: PlanData) {
|
||||
this.receiver = {
|
||||
// no did, because it's a project
|
||||
name: project.name,
|
||||
image: project.image,
|
||||
handleId: project.handleId,
|
||||
};
|
||||
this.firstStep = false;
|
||||
}
|
||||
|
||||
// Computed property for the query parameters
|
||||
get giftedDetailsQuery() {
|
||||
return {
|
||||
amountInput: this.amountInput,
|
||||
description: this.description,
|
||||
giverDid: this.giverEntityType === "person" ? this.giver?.did : undefined,
|
||||
giverName: this.giver?.name,
|
||||
offerId: this.offerId,
|
||||
fulfillsProjectId: this.giverEntityType === "person" && this.recipientEntityType === "project"
|
||||
? this.toProjectId
|
||||
: undefined,
|
||||
providerProjectId: this.giverEntityType === "project" && this.recipientEntityType === "person"
|
||||
? this.giver?.handleId
|
||||
: this.fromProjectId,
|
||||
recipientDid: this.receiver?.did,
|
||||
recipientName: this.receiver?.name,
|
||||
unitCode: this.unitCode,
|
||||
};
|
||||
}
|
||||
|
||||
// Computed property to get unit options
|
||||
get unitOptions() {
|
||||
return this.libsUtil.UNIT_SHORT;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
If you'd like an introduction,
|
||||
<a
|
||||
class="text-blue-500"
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
@click="copyToClipboard('A link to this page', deepLinkUrl)"
|
||||
>click here to copy this page, paste it into a message, and ask if
|
||||
they'll tell you more about the {{ roleName }}.</a
|
||||
>
|
||||
@@ -104,7 +104,7 @@ import * as R from "ramda";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||
|
||||
@Component
|
||||
export default class HiddenDidDialog extends Vue {
|
||||
@@ -117,7 +117,8 @@ export default class HiddenDidDialog extends Vue {
|
||||
activeDid = "";
|
||||
allMyDids: Array<string> = [];
|
||||
canShare = false;
|
||||
windowLocation = window.location.href;
|
||||
deepLinkPathSuffix = "";
|
||||
deepLinkUrl = window.location.href; // this is changed to a deep link in the setup
|
||||
|
||||
R = R;
|
||||
serverUtil = serverUtil;
|
||||
@@ -129,17 +130,21 @@ export default class HiddenDidDialog extends Vue {
|
||||
}
|
||||
|
||||
open(
|
||||
deepLinkPathSuffix: string,
|
||||
roleName: string,
|
||||
visibleToDids: string[],
|
||||
allContacts: Array<Contact>,
|
||||
activeDid: string,
|
||||
allMyDids: Array<string>,
|
||||
) {
|
||||
this.deepLinkPathSuffix = deepLinkPathSuffix;
|
||||
this.roleName = roleName;
|
||||
this.visibleToDids = visibleToDids;
|
||||
this.allContacts = allContacts;
|
||||
this.activeDid = activeDid;
|
||||
this.allMyDids = allMyDids;
|
||||
|
||||
this.deepLinkUrl = APP_SERVER + "/deep-link/" + this.deepLinkPathSuffix;
|
||||
this.isOpen = true;
|
||||
}
|
||||
|
||||
@@ -173,11 +178,11 @@ export default class HiddenDidDialog extends Vue {
|
||||
}
|
||||
|
||||
onClickShareClaim() {
|
||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
||||
this.copyToClipboard("A link to this page", this.deepLinkUrl);
|
||||
window.navigator.share({
|
||||
title: "Help Connect Me",
|
||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||
url: this.windowLocation,
|
||||
url: this.deepLinkUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ const secretBase64 = arrayBufferToBase64(randomBytes);
|
||||
const MIGRATIONS = [
|
||||
{
|
||||
name: "001_initial",
|
||||
// see ../db/tables files for explanations of the fields
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -119,6 +118,12 @@ const MIGRATIONS = [
|
||||
);
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "002_add_iViewContent_to_contacts",
|
||||
sql: `
|
||||
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -219,9 +219,9 @@ export async function logConsoleAndDb(
|
||||
isError = false,
|
||||
): Promise<void> {
|
||||
if (isError) {
|
||||
logger.error(`${new Date().toISOString()} ${message}`);
|
||||
logger.error(`${new Date().toISOString()}`, message);
|
||||
} else {
|
||||
logger.log(`${new Date().toISOString()} ${message}`);
|
||||
logger.log(`${new Date().toISOString()}`, message);
|
||||
}
|
||||
await logToDb(message);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
export interface ContactMethod {
|
||||
export type ContactMethod = {
|
||||
label: string;
|
||||
type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
||||
value: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Contact {
|
||||
export type Contact = {
|
||||
//
|
||||
// When adding a property, consider whether it should be added when exporting & sharing contacts.
|
||||
// When adding a property, consider whether it should be added when exporting & sharing contacts, eg. DataExportSection
|
||||
|
||||
did: string;
|
||||
contactMethods?: Array<ContactMethod>;
|
||||
iViewContent?: boolean;
|
||||
name?: string;
|
||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
||||
notes?: string;
|
||||
@@ -17,9 +18,15 @@ export interface Contact {
|
||||
publicKeyBase64?: string;
|
||||
seesMe?: boolean; // cached value of the server setting
|
||||
registered?: boolean; // cached value of the server setting
|
||||
}
|
||||
};
|
||||
|
||||
export type ContactWithJsonStrings = Contact & {
|
||||
/**
|
||||
* This is for those cases (eg. with a DB) where every field is a primitive (and not an object).
|
||||
*
|
||||
* This is so that we can reuse most of the type and don't have to maintain another copy.
|
||||
* Another approach uses typescript conditionals: https://chatgpt.com/share/6855cdc3-ab5c-8007-8525-726612016eb2
|
||||
*/
|
||||
export type ContactWithJsonStrings = Omit<Contact, "contactMethods"> & {
|
||||
contactMethods?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -29,18 +29,17 @@ import { z } from "zod";
|
||||
|
||||
// Add a union type of all valid route paths
|
||||
export const VALID_DEEP_LINK_ROUTES = [
|
||||
"user-profile",
|
||||
"project",
|
||||
"onboard-meeting-setup",
|
||||
"invite-one-accept",
|
||||
"contact-import",
|
||||
"confirm-gift",
|
||||
// note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts
|
||||
"claim",
|
||||
"claim-cert",
|
||||
"claim-add-raw",
|
||||
"contact-edit",
|
||||
"contacts",
|
||||
"claim-cert",
|
||||
"confirm-gift",
|
||||
"contact-import",
|
||||
"did",
|
||||
"invite-one-accept",
|
||||
"onboard-meeting-setup",
|
||||
"project",
|
||||
"user-profile",
|
||||
] as const;
|
||||
|
||||
// Create a type from the array
|
||||
@@ -58,44 +57,39 @@ export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES);
|
||||
|
||||
// Parameter validation schemas for each route type
|
||||
export const deepLinkSchemas = {
|
||||
"user-profile": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
project: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"onboard-meeting-setup": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"invite-one-accept": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"contact-import": z.object({
|
||||
jwt: z.string(),
|
||||
}),
|
||||
"confirm-gift": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
// note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts
|
||||
claim: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"claim-cert": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"claim-add-raw": z.object({
|
||||
id: z.string(),
|
||||
claim: z.string().optional(),
|
||||
claimJwtId: z.string().optional(),
|
||||
}),
|
||||
"contact-edit": z.object({
|
||||
did: z.string(),
|
||||
"claim-cert": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
contacts: z.object({
|
||||
contacts: z.string(), // JSON string of contacts array
|
||||
"confirm-gift": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"contact-import": z.object({
|
||||
jwt: z.string(),
|
||||
}),
|
||||
did: z.object({
|
||||
did: z.string(),
|
||||
}),
|
||||
"invite-one-accept": z.object({
|
||||
jwt: z.string(),
|
||||
}),
|
||||
"onboard-meeting-setup": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
project: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"user-profile": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
export type DeepLinkParams = {
|
||||
|
||||
@@ -1074,7 +1074,8 @@ export async function generateEndorserJwtUrlForAccount(
|
||||
|
||||
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
|
||||
|
||||
const viewPrefix = APP_SERVER + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
|
||||
const viewPrefix =
|
||||
APP_SERVER + "/deep-link" + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
|
||||
return viewPrefix + vcJwt;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowRotateBackward,
|
||||
faArrowUpRightFromSquare,
|
||||
faArrowUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
faCircleUser,
|
||||
faClock,
|
||||
faCoins,
|
||||
@@ -60,6 +61,7 @@ import {
|
||||
faLightbulb,
|
||||
faLink,
|
||||
faLocationDot,
|
||||
faLock,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
@@ -79,6 +81,7 @@ import {
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faSquarePlus,
|
||||
faThumbtack,
|
||||
faTrashCan,
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
@@ -92,8 +95,8 @@ library.add(
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowRotateBackward,
|
||||
faArrowUpRightFromSquare,
|
||||
faArrowUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
@@ -111,6 +114,7 @@ library.add(
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
faCircleUser,
|
||||
faClock,
|
||||
faCoins,
|
||||
@@ -142,6 +146,7 @@ library.add(
|
||||
faLightbulb,
|
||||
faLink,
|
||||
faLocationDot,
|
||||
faLock,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
@@ -161,6 +166,7 @@ library.add(
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faSquarePlus,
|
||||
faThumbtack,
|
||||
faTrashCan,
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
updateDefaultSettings,
|
||||
} from "../db/index";
|
||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { Contact, ContactWithJsonStrings } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
||||
import {
|
||||
@@ -50,6 +50,8 @@ import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
|
||||
export interface GiverReceiverInputInfo {
|
||||
did?: string;
|
||||
name?: string;
|
||||
image?: string;
|
||||
handleId?: string;
|
||||
}
|
||||
|
||||
export enum OnboardPage {
|
||||
@@ -974,19 +976,16 @@ export interface DatabaseExport {
|
||||
*/
|
||||
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
||||
// Convert each contact to a plain object and ensure all fields are included
|
||||
const rows = contacts.map((contact) => ({
|
||||
did: contact.did,
|
||||
name: contact.name || null,
|
||||
contactMethods: contact.contactMethods
|
||||
? JSON.stringify(parseJsonField(contact.contactMethods, []))
|
||||
: null,
|
||||
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
|
||||
notes: contact.notes || null,
|
||||
profileImageUrl: contact.profileImageUrl || null,
|
||||
publicKeyBase64: contact.publicKeyBase64 || null,
|
||||
seesMe: contact.seesMe || false,
|
||||
registered: contact.registered || false,
|
||||
}));
|
||||
const rows = contacts.map((contact) => {
|
||||
const exContact: ContactWithJsonStrings = R.omit(
|
||||
["contactMethods"],
|
||||
contact,
|
||||
);
|
||||
exContact.contactMethods = contact.contactMethods
|
||||
? JSON.stringify(contact.contactMethods, [])
|
||||
: undefined;
|
||||
return exContact;
|
||||
});
|
||||
|
||||
return {
|
||||
data: {
|
||||
|
||||
@@ -34,8 +34,7 @@ import router from "./router";
|
||||
import { handleApiError } from "./services/api";
|
||||
import { AxiosError } from "axios";
|
||||
import { DeepLinkHandler } from "./services/deepLinks";
|
||||
import { logConsoleAndDb } from "./db/databaseUtil";
|
||||
import { logger } from "./utils/logger";
|
||||
import { logger, safeStringify } from "./utils/logger";
|
||||
|
||||
logger.log("[Capacitor] Starting initialization");
|
||||
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
||||
@@ -72,10 +71,10 @@ const handleDeepLink = async (data: { url: string }) => {
|
||||
await router.isReady();
|
||||
await deepLinkHandler.handleDeepLink(data.url);
|
||||
} catch (error) {
|
||||
logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
|
||||
logger.error("[DeepLink] Error handling deep link: ", error);
|
||||
handleApiError(
|
||||
{
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
message: error instanceof Error ? error.message : safeStringify(error),
|
||||
} as AxiosError,
|
||||
"deep-link",
|
||||
);
|
||||
|
||||
@@ -10,15 +10,11 @@ import { FontAwesomeIcon } from "./libs/fontawesome";
|
||||
import Camera from "simple-vue-camera";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
const platform = process.env.VITE_PLATFORM;
|
||||
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||
|
||||
logger.log("Platform", JSON.stringify({ platform }));
|
||||
logger.log("PWA enabled", JSON.stringify({ pwa_enabled }));
|
||||
// const platform = process.env.VITE_PLATFORM;
|
||||
// const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||
|
||||
// Global Error Handler
|
||||
function setupGlobalErrorHandler(app: VueApp) {
|
||||
logger.log("[App Init] Setting up global error handler");
|
||||
app.config.errorHandler = (
|
||||
err: unknown,
|
||||
instance: ComponentPublicInstance | null,
|
||||
@@ -38,30 +34,13 @@ function setupGlobalErrorHandler(app: VueApp) {
|
||||
|
||||
// Function to initialize the app
|
||||
export function initializeApp() {
|
||||
logger.log("[App Init] Starting app initialization");
|
||||
logger.log("[App Init] Platform:", process.env.VITE_PLATFORM);
|
||||
|
||||
const app = createApp(App);
|
||||
logger.log("[App Init] Vue app created");
|
||||
|
||||
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera);
|
||||
logger.log("[App Init] Components registered");
|
||||
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
logger.log("[App Init] Pinia store initialized");
|
||||
|
||||
app.use(VueAxios, axios);
|
||||
logger.log("[App Init] Axios initialized");
|
||||
|
||||
app.use(router);
|
||||
logger.log("[App Init] Router initialized");
|
||||
|
||||
app.use(Notifications);
|
||||
logger.log("[App Init] Notifications initialized");
|
||||
|
||||
setupGlobalErrorHandler(app);
|
||||
logger.log("[App Init] App initialization complete");
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ import { logger } from "./utils/logger";
|
||||
const platform = process.env.VITE_PLATFORM;
|
||||
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||
|
||||
logger.info("[Web] PWA enabled", { pwa_enabled });
|
||||
logger.info("[Web] Platform", { platform });
|
||||
|
||||
// Only import service worker for web builds
|
||||
if (platform !== "electron" && pwa_enabled) {
|
||||
import("./registerServiceWorker"); // Web PWA support
|
||||
@@ -31,7 +28,7 @@ function sqlInit() {
|
||||
if (platform === "web" || platform === "development") {
|
||||
sqlInit();
|
||||
} else {
|
||||
logger.info("[Web] SQL not initialized for platform", { platform });
|
||||
logger.warn("[Web] SQL not initialized for platform", { platform });
|
||||
}
|
||||
|
||||
app.mount("#app");
|
||||
|
||||
@@ -83,6 +83,11 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "discover",
|
||||
component: () => import("../views/DiscoverView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/deep-link/:path*",
|
||||
name: "deep-link",
|
||||
component: () => import("../views/DeepLinkRedirectView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/gifted-details",
|
||||
name: "gifted-details",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { AxiosError } from "axios";
|
||||
import { logger } from "../utils/logger";
|
||||
import { logger, safeStringify } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* Handles API errors with platform-specific logging and error processing.
|
||||
@@ -37,7 +37,8 @@ import { logger } from "../utils/logger";
|
||||
*/
|
||||
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
||||
if (process.env.VITE_PLATFORM === "capacitor") {
|
||||
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
||||
const endpointStr = safeStringify(endpoint); // we've seen this as an object in deep links
|
||||
logger.error(`[Capacitor API Error] ${endpointStr}:`, {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
data: error.response?.data,
|
||||
|
||||
@@ -27,18 +27,16 @@
|
||||
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
|
||||
*
|
||||
* Supported Routes:
|
||||
* - user-profile: View user profile
|
||||
* - project: View project details
|
||||
* - onboard-meeting-setup: Setup onboarding meeting
|
||||
* - invite-one-accept: Accept invitation
|
||||
* - contact-import: Import contacts
|
||||
* - confirm-gift: Confirm gift
|
||||
* - claim: View claim
|
||||
* - claim-cert: View claim certificate
|
||||
* - claim-add-raw: Add raw claim
|
||||
* - contact-edit: Edit contact
|
||||
* - contacts: View contacts
|
||||
* - claim-cert: View claim certificate
|
||||
* - confirm-gift
|
||||
* - contact-import: Import contacts
|
||||
* - did: View DID
|
||||
* - invite-one-accept: Accept invitation
|
||||
* - onboard-meeting-members
|
||||
* - project: View project details
|
||||
* - user-profile: View user profile
|
||||
*
|
||||
* @example
|
||||
* const handler = new DeepLinkHandler(router);
|
||||
@@ -81,14 +79,15 @@ export class DeepLinkHandler {
|
||||
string,
|
||||
{ name: string; paramKey?: string }
|
||||
> = {
|
||||
// note that similar lists are in src/interfaces/deepLinks.ts
|
||||
claim: { name: "claim" },
|
||||
"claim-add-raw": { name: "claim-add-raw" },
|
||||
"claim-cert": { name: "claim-cert" },
|
||||
"confirm-gift": { name: "confirm-gift" },
|
||||
"contact-import": { name: "contact-import", paramKey: "jwt" },
|
||||
did: { name: "did", paramKey: "did" },
|
||||
"invite-one-accept": { name: "invite-one-accept", paramKey: "jwt" },
|
||||
"onboard-meeting-members": { name: "onboard-meeting-members" },
|
||||
"onboard-meeting-setup": { name: "onboard-meeting-setup" },
|
||||
project: { name: "project" },
|
||||
"user-profile": { name: "user-profile" },
|
||||
};
|
||||
@@ -99,7 +98,7 @@ export class DeepLinkHandler {
|
||||
*
|
||||
* @param url - The deep link URL to parse (format: scheme://path[?query])
|
||||
* @throws {DeepLinkError} If URL format is invalid
|
||||
* @returns Parsed URL components (path, params, query)
|
||||
* @returns Parsed URL components (path: string, params: {KEY: string}, query: {KEY: string})
|
||||
*/
|
||||
private parseDeepLink(url: string) {
|
||||
const parts = url.split("://");
|
||||
@@ -115,7 +114,16 @@ export class DeepLinkHandler {
|
||||
});
|
||||
|
||||
const [path, queryString] = parts[1].split("?");
|
||||
const [routePath, param] = path.split("/");
|
||||
const [routePath, ...pathParams] = path.split("/");
|
||||
// logger.info(
|
||||
// "[DeepLink] Debug:",
|
||||
// "Route Path:",
|
||||
// routePath,
|
||||
// "Path Params:",
|
||||
// pathParams,
|
||||
// "Query String:",
|
||||
// queryString,
|
||||
// );
|
||||
|
||||
// Validate route exists before proceeding
|
||||
if (!this.ROUTE_MAP[routePath]) {
|
||||
@@ -134,45 +142,14 @@ export class DeepLinkHandler {
|
||||
}
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
if (param) {
|
||||
if (pathParams) {
|
||||
// Now we know routePath exists in ROUTE_MAP
|
||||
const routeConfig = this.ROUTE_MAP[routePath];
|
||||
params[routeConfig.paramKey ?? "id"] = param;
|
||||
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
|
||||
}
|
||||
return { path: routePath, params, query };
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes incoming deep links and routes them appropriately.
|
||||
* Handles validation, error handling, and routing to the correct view.
|
||||
*
|
||||
* @param url - The deep link URL to process
|
||||
* @throws {DeepLinkError} If URL processing fails
|
||||
*/
|
||||
async handleDeepLink(url: string): Promise<void> {
|
||||
try {
|
||||
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
|
||||
const { path, params, query } = this.parseDeepLink(url);
|
||||
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
||||
const sanitizedParams = Object.fromEntries(
|
||||
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
|
||||
);
|
||||
await this.validateAndRoute(path, sanitizedParams, query);
|
||||
} catch (error) {
|
||||
const deepLinkError = error as DeepLinkError;
|
||||
logConsoleAndDb(
|
||||
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
||||
true,
|
||||
);
|
||||
|
||||
throw {
|
||||
code: deepLinkError.code || "UNKNOWN_ERROR",
|
||||
message: deepLinkError.message,
|
||||
details: deepLinkError.details,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes the deep link to appropriate view with validated parameters.
|
||||
* Validates route and parameters using Zod schemas before routing.
|
||||
@@ -243,6 +220,39 @@ export class DeepLinkHandler {
|
||||
code: "INVALID_PARAMETERS",
|
||||
message: (error as Error).message,
|
||||
details: error,
|
||||
params: params,
|
||||
query: query,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes incoming deep links and routes them appropriately.
|
||||
* Handles validation, error handling, and routing to the correct view.
|
||||
*
|
||||
* @param url - The deep link URL to process
|
||||
* @throws {DeepLinkError} If URL processing fails
|
||||
*/
|
||||
async handleDeepLink(url: string): Promise<void> {
|
||||
try {
|
||||
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
|
||||
const { path, params, query } = this.parseDeepLink(url);
|
||||
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
||||
const sanitizedParams = Object.fromEntries(
|
||||
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
|
||||
);
|
||||
await this.validateAndRoute(path, sanitizedParams, query);
|
||||
} catch (error) {
|
||||
const deepLinkError = error as DeepLinkError;
|
||||
logConsoleAndDb(
|
||||
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
||||
true,
|
||||
);
|
||||
|
||||
throw {
|
||||
code: deepLinkError.code || "UNKNOWN_ERROR",
|
||||
message: deepLinkError.message,
|
||||
details: deepLinkError.details,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
generateUpdateStatement,
|
||||
generateInsertStatement,
|
||||
} from "../db/databaseUtil";
|
||||
import { updateDefaultSettings } from "../db/databaseUtil";
|
||||
import { importFromMnemonic } from "../libs/util";
|
||||
|
||||
/**
|
||||
@@ -156,11 +155,14 @@ export async function getDexieContacts(): Promise<Contact[]> {
|
||||
await db.open();
|
||||
const contacts = await db.contacts.toArray();
|
||||
logger.info(
|
||||
`[MigrationService] Retrieved ${contacts.length} contacts from Dexie`,
|
||||
`[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from Dexie`,
|
||||
);
|
||||
return contacts;
|
||||
} catch (error) {
|
||||
logger.error("[MigrationService] Error retrieving Dexie contacts:", error);
|
||||
logger.error(
|
||||
"[IndexedDBMigrationService] Error retrieving Dexie contacts:",
|
||||
error,
|
||||
);
|
||||
throw new Error(`Failed to retrieve Dexie contacts: ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -214,11 +216,14 @@ export async function getSqliteContacts(): Promise<Contact[]> {
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[MigrationService] Retrieved ${contacts.length} contacts from SQLite`,
|
||||
`[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from SQLite`,
|
||||
);
|
||||
return contacts;
|
||||
} catch (error) {
|
||||
logger.error("[MigrationService] Error retrieving SQLite contacts:", error);
|
||||
logger.error(
|
||||
"[IndexedDBMigrationService] Error retrieving SQLite contacts:",
|
||||
error,
|
||||
);
|
||||
throw new Error(`Failed to retrieve SQLite contacts: ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -251,11 +256,14 @@ export async function getDexieSettings(): Promise<Settings[]> {
|
||||
await db.open();
|
||||
const settings = await db.settings.toArray();
|
||||
logger.info(
|
||||
`[MigrationService] Retrieved ${settings.length} settings from Dexie`,
|
||||
`[IndexedDBMigrationService] Retrieved ${settings.length} settings from Dexie`,
|
||||
);
|
||||
return settings;
|
||||
} catch (error) {
|
||||
logger.error("[MigrationService] Error retrieving Dexie settings:", error);
|
||||
logger.error(
|
||||
"[IndexedDBMigrationService] Error retrieving Dexie settings:",
|
||||
error,
|
||||
);
|
||||
throw new Error(`Failed to retrieve Dexie settings: ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -309,11 +317,14 @@ export async function getSqliteSettings(): Promise<Settings[]> {
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[MigrationService] Retrieved ${settings.length} settings from SQLite`,
|
||||
`[IndexedDBMigrationService] Retrieved ${settings.length} settings from SQLite`,
|
||||
);
|
||||
return settings;
|
||||
} catch (error) {
|
||||
logger.error("[MigrationService] Error retrieving SQLite settings:", error);
|
||||
logger.error(
|
||||
"[IndexedDBMigrationService] Error retrieving SQLite settings:",
|
||||
error,
|
||||
);
|
||||
throw new Error(`Failed to retrieve SQLite settings: ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -353,11 +364,14 @@ export async function getSqliteAccounts(): Promise<string[]> {
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[MigrationService] Retrieved ${dids.length} accounts from SQLite`,
|
||||
`[IndexedDBMigrationService] Retrieved ${dids.length} accounts from SQLite`,
|
||||
);
|
||||
return dids;
|
||||
} catch (error) {
|
||||
logger.error("[MigrationService] Error retrieving SQLite accounts:", error);
|
||||
logger.error(
|
||||
"[IndexedDBMigrationService] Error retrieving SQLite accounts:",
|
||||
error,
|
||||
);
|
||||
throw new Error(`Failed to retrieve SQLite accounts: ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -391,11 +405,14 @@ export async function getDexieAccounts(): Promise<Account[]> {
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
logger.info(
|
||||
`[MigrationService] Retrieved ${accounts.length} accounts from Dexie`,
|
||||
`[IndexedDBMigrationService] Retrieved ${accounts.length} accounts from Dexie`,
|
||||
);
|
||||
return accounts;
|
||||
} catch (error) {
|
||||
logger.error("[MigrationService] Error retrieving Dexie accounts:", error);
|
||||
logger.error(
|
||||
"[IndexedDBMigrationService] Error retrieving Dexie accounts:",
|
||||
error,
|
||||
);
|
||||
throw new Error(`Failed to retrieve Dexie accounts: ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -429,7 +446,7 @@ export async function getDexieAccounts(): Promise<Account[]> {
|
||||
* ```
|
||||
*/
|
||||
export async function compareDatabases(): Promise<DataComparison> {
|
||||
logger.info("[MigrationService] Starting database comparison");
|
||||
logger.info("[IndexedDBMigrationService] Starting database comparison");
|
||||
|
||||
const [
|
||||
dexieContacts,
|
||||
@@ -470,7 +487,7 @@ export async function compareDatabases(): Promise<DataComparison> {
|
||||
},
|
||||
};
|
||||
|
||||
logger.info("[MigrationService] Database comparison completed", {
|
||||
logger.info("[IndexedDBMigrationService] Database comparison completed", {
|
||||
dexieContacts: dexieContacts.length,
|
||||
sqliteContacts: sqliteContacts.length,
|
||||
dexieSettings: dexieSettings.length,
|
||||
@@ -679,6 +696,7 @@ function compareAccounts(dexieAccounts: Account[], sqliteDids: string[]) {
|
||||
* ```
|
||||
*/
|
||||
function contactsEqual(contact1: Contact, contact2: Contact): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ifEmpty = (arg: any, def: any) => (arg ? arg : def);
|
||||
const contact1Methods =
|
||||
contact1.contactMethods &&
|
||||
@@ -954,7 +972,7 @@ export function generateComparisonYaml(comparison: DataComparison): string {
|
||||
export async function migrateContacts(
|
||||
overwriteExisting: boolean = false,
|
||||
): Promise<MigrationResult> {
|
||||
logger.info("[MigrationService] Starting contact migration", {
|
||||
logger.info("[IndexedDBMigrationService] Starting contact migration", {
|
||||
overwriteExisting,
|
||||
});
|
||||
|
||||
@@ -990,7 +1008,7 @@ export async function migrateContacts(
|
||||
);
|
||||
await platformService.dbExec(sql, params);
|
||||
result.contactsMigrated++;
|
||||
logger.info(`[MigrationService] Updated contact: ${contact.did}`);
|
||||
logger.info(`[IndexedDBMigrationService] Updated contact: ${contact.did}`);
|
||||
} else {
|
||||
result.warnings.push(
|
||||
`Contact ${contact.did} already exists, skipping`,
|
||||
@@ -1004,17 +1022,17 @@ export async function migrateContacts(
|
||||
);
|
||||
await platformService.dbExec(sql, params);
|
||||
result.contactsMigrated++;
|
||||
logger.info(`[MigrationService] Added contact: ${contact.did}`);
|
||||
logger.info(`[IndexedDBMigrationService] Added contact: ${contact.did}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `Failed to migrate contact ${contact.did}: ${error}`;
|
||||
logger.error("[MigrationService]", errorMsg);
|
||||
logger.error("[IndexedDBMigrationService]", errorMsg);
|
||||
result.errors.push(errorMsg);
|
||||
result.success = false;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("[MigrationService] Contact migration completed", {
|
||||
logger.info("[IndexedDBMigrationService] Contact migration completed", {
|
||||
contactsMigrated: result.contactsMigrated,
|
||||
errors: result.errors.length,
|
||||
warnings: result.warnings.length,
|
||||
@@ -1023,7 +1041,7 @@ export async function migrateContacts(
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMsg = `Contact migration failed: ${error}`;
|
||||
logger.error("[MigrationService]", errorMsg);
|
||||
logger.error("[IndexedDBMigrationService]", errorMsg);
|
||||
result.errors.push(errorMsg);
|
||||
result.success = false;
|
||||
return result;
|
||||
@@ -1063,7 +1081,7 @@ export async function migrateContacts(
|
||||
* ```
|
||||
*/
|
||||
export async function migrateSettings(): Promise<MigrationResult> {
|
||||
logger.info("[MigrationService] Starting settings migration");
|
||||
logger.info("[IndexedDBMigrationService] Starting settings migration");
|
||||
|
||||
const result: MigrationResult = {
|
||||
success: true,
|
||||
@@ -1076,17 +1094,17 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
||||
|
||||
try {
|
||||
const dexieSettings = await getDexieSettings();
|
||||
logger.info("[MigrationService] Migrating settings", {
|
||||
logger.info("[IndexedDBMigrationService] Migrating settings", {
|
||||
dexieSettings: dexieSettings.length,
|
||||
});
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
|
||||
// Create an array of promises for all settings migrations
|
||||
const migrationPromises = dexieSettings.map(async (setting) => {
|
||||
logger.info("[MigrationService] Starting to migrate settings", setting);
|
||||
let sqliteSettingRaw:
|
||||
| { columns: string[]; values: unknown[][] }
|
||||
| undefined;
|
||||
logger.info(
|
||||
"[IndexedDBMigrationService] Starting to migrate settings",
|
||||
setting,
|
||||
);
|
||||
|
||||
// adjust SQL based on the accountDid key, maybe null
|
||||
let conditional: string;
|
||||
@@ -1098,15 +1116,18 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
||||
conditional = "accountDid = ?";
|
||||
preparams = [setting.accountDid];
|
||||
}
|
||||
sqliteSettingRaw = await platformService.dbQuery(
|
||||
const sqliteSettingRaw = await platformService.dbQuery(
|
||||
"SELECT * FROM settings WHERE " + conditional,
|
||||
preparams,
|
||||
);
|
||||
|
||||
logger.info("[MigrationService] Migrating one set of settings:", {
|
||||
setting,
|
||||
sqliteSettingRaw,
|
||||
});
|
||||
logger.info(
|
||||
"[IndexedDBMigrationService] Migrating one set of settings:",
|
||||
{
|
||||
setting,
|
||||
sqliteSettingRaw,
|
||||
},
|
||||
);
|
||||
if (sqliteSettingRaw?.values?.length) {
|
||||
// should cover the master settings, where accountDid is null
|
||||
delete setting.id; // don't conflict with the id in the sqlite database
|
||||
@@ -1117,7 +1138,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
||||
conditional,
|
||||
preparams,
|
||||
);
|
||||
logger.info("[MigrationService] Updating settings", {
|
||||
logger.info("[IndexedDBMigrationService] Updating settings", {
|
||||
sql,
|
||||
params,
|
||||
});
|
||||
@@ -1127,10 +1148,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
||||
// insert new setting
|
||||
delete setting.id; // don't conflict with the id in the sqlite database
|
||||
delete setting.activeDid; // ensure we don't set the activeDid (since master settings are an update and don't hit this case)
|
||||
const { sql, params } = generateInsertStatement(
|
||||
setting,
|
||||
"settings",
|
||||
);
|
||||
const { sql, params } = generateInsertStatement(setting, "settings");
|
||||
await platformService.dbExec(sql, params);
|
||||
result.settingsMigrated++;
|
||||
}
|
||||
@@ -1140,7 +1158,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
||||
const updatedSettings = await Promise.all(migrationPromises);
|
||||
|
||||
logger.info(
|
||||
"[MigrationService] Finished migrating settings",
|
||||
"[IndexedDBMigrationService] Finished migrating settings",
|
||||
updatedSettings,
|
||||
result,
|
||||
);
|
||||
@@ -1148,7 +1166,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[MigrationService] Complete settings migration failed:",
|
||||
"[IndexedDBMigrationService] Complete settings migration failed:",
|
||||
error,
|
||||
);
|
||||
const errorMessage = `Settings migration failed: ${error}`;
|
||||
@@ -1192,7 +1210,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
||||
* ```
|
||||
*/
|
||||
export async function migrateAccounts(): Promise<MigrationResult> {
|
||||
logger.info("[MigrationService] Starting account migration");
|
||||
logger.info("[IndexedDBMigrationService] Starting account migration");
|
||||
|
||||
const result: MigrationResult = {
|
||||
success: true,
|
||||
@@ -1248,14 +1266,17 @@ export async function migrateAccounts(): Promise<MigrationResult> {
|
||||
);
|
||||
}
|
||||
|
||||
logger.info("[MigrationService] Successfully migrated account", {
|
||||
did,
|
||||
dateCreated: account.dateCreated,
|
||||
});
|
||||
logger.info(
|
||||
"[IndexedDBMigrationService] Successfully migrated account",
|
||||
{
|
||||
did,
|
||||
dateCreated: account.dateCreated,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage = `Failed to migrate account ${did}: ${error}`;
|
||||
result.errors.push(errorMessage);
|
||||
logger.error("[MigrationService] Account migration failed:", {
|
||||
logger.error("[IndexedDBMigrationService] Account migration failed:", {
|
||||
error,
|
||||
did,
|
||||
});
|
||||
@@ -1272,7 +1293,7 @@ export async function migrateAccounts(): Promise<MigrationResult> {
|
||||
result.errors.push(errorMessage);
|
||||
result.success = false;
|
||||
logger.error(
|
||||
"[MigrationService] Complete account migration failed:",
|
||||
"[IndexedDBMigrationService] Complete account migration failed:",
|
||||
error,
|
||||
);
|
||||
return result;
|
||||
@@ -1306,11 +1327,11 @@ export async function migrateAll(): Promise<MigrationResult> {
|
||||
|
||||
try {
|
||||
logger.info(
|
||||
"[MigrationService] Starting complete migration from Dexie to SQLite",
|
||||
"[IndexedDBMigrationService] Starting complete migration from Dexie to SQLite",
|
||||
);
|
||||
|
||||
// Step 1: Migrate Accounts (foundational)
|
||||
logger.info("[MigrationService] Step 1: Migrating accounts...");
|
||||
logger.info("[IndexedDBMigrationService] Step 1: Migrating accounts...");
|
||||
const accountsResult = await migrateAccounts();
|
||||
if (!accountsResult.success) {
|
||||
result.errors.push(
|
||||
@@ -1322,7 +1343,7 @@ export async function migrateAll(): Promise<MigrationResult> {
|
||||
result.warnings.push(...accountsResult.warnings);
|
||||
|
||||
// Step 2: Migrate Settings (depends on accounts)
|
||||
logger.info("[MigrationService] Step 2: Migrating settings...");
|
||||
logger.info("[IndexedDBMigrationService] Step 2: Migrating settings...");
|
||||
const settingsResult = await migrateSettings();
|
||||
if (!settingsResult.success) {
|
||||
result.errors.push(
|
||||
@@ -1335,7 +1356,7 @@ export async function migrateAll(): Promise<MigrationResult> {
|
||||
|
||||
// Step 4: Migrate Contacts (independent, but after accounts for consistency)
|
||||
// ... but which is better done through the contact import view
|
||||
// logger.info("[MigrationService] Step 4: Migrating contacts...");
|
||||
// logger.info("[IndexedDBMigrationService] Step 4: Migrating contacts...");
|
||||
// const contactsResult = await migrateContacts();
|
||||
// if (!contactsResult.success) {
|
||||
// result.errors.push(
|
||||
@@ -1354,7 +1375,7 @@ export async function migrateAll(): Promise<MigrationResult> {
|
||||
result.contactsMigrated;
|
||||
|
||||
logger.info(
|
||||
`[MigrationService] Complete migration successful: ${totalMigrated} total records migrated`,
|
||||
`[IndexedDBMigrationService] Complete migration successful: ${totalMigrated} total records migrated`,
|
||||
{
|
||||
accounts: result.accountsMigrated,
|
||||
settings: result.settingsMigrated,
|
||||
@@ -1367,7 +1388,10 @@ export async function migrateAll(): Promise<MigrationResult> {
|
||||
} catch (error) {
|
||||
const errorMessage = `Complete migration failed: ${error}`;
|
||||
result.errors.push(errorMessage);
|
||||
logger.error("[MigrationService] Complete migration failed:", error);
|
||||
logger.error(
|
||||
"[IndexedDBMigrationService] Complete migration failed:",
|
||||
error,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ class MigrationRegistry {
|
||||
*/
|
||||
registerMigration(migration: Migration): void {
|
||||
this.migrations.push(migration);
|
||||
logger.info(`[MigrationService] Registered migration: ${migration.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +41,6 @@ class MigrationRegistry {
|
||||
*/
|
||||
clearMigrations(): void {
|
||||
this.migrations = [];
|
||||
logger.info("[MigrationService] Cleared all registered migrations");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,10 +92,6 @@ export async function runMigrations<T>(
|
||||
);
|
||||
const appliedMigrations = extractMigrationNames(appliedMigrationsResult);
|
||||
|
||||
logger.info(
|
||||
`[MigrationService] Found ${appliedMigrations.size} applied migrations`,
|
||||
);
|
||||
|
||||
// Get all registered migrations
|
||||
const migrations = migrationRegistry.getMigrations();
|
||||
|
||||
@@ -106,21 +100,12 @@ export async function runMigrations<T>(
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[MigrationService] Running ${migrations.length} registered migrations`,
|
||||
);
|
||||
|
||||
// Run each migration that hasn't been applied yet
|
||||
for (const migration of migrations) {
|
||||
if (appliedMigrations.has(migration.name)) {
|
||||
logger.info(
|
||||
`[MigrationService] Skipping already applied migration: ${migration.name}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info(`[MigrationService] Applying migration: ${migration.name}`);
|
||||
|
||||
try {
|
||||
// Execute the migration SQL
|
||||
await sqlExec(migration.sql);
|
||||
@@ -141,8 +126,6 @@ export async function runMigrations<T>(
|
||||
throw new Error(`Migration ${migration.name} failed: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("[MigrationService] All migrations completed successfully");
|
||||
} catch (error) {
|
||||
logger.error("[MigrationService] Migration process failed:", error);
|
||||
throw error;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { logToDb } from "../db/databaseUtil";
|
||||
|
||||
function safeStringify(obj: unknown) {
|
||||
export function safeStringify(obj: unknown) {
|
||||
const seen = new WeakSet();
|
||||
|
||||
return JSON.stringify(obj, (_key, value) => {
|
||||
@@ -52,23 +52,18 @@ export const logger = {
|
||||
}
|
||||
},
|
||||
warn: (message: string, ...args: unknown[]) => {
|
||||
if (
|
||||
process.env.NODE_ENV !== "production" ||
|
||||
process.env.VITE_PLATFORM === "capacitor" ||
|
||||
process.env.VITE_PLATFORM === "electron"
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(message, ...args);
|
||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||
logToDb(message + argsString);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(message, ...args);
|
||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||
logToDb(message + argsString);
|
||||
},
|
||||
error: (message: string, ...args: unknown[]) => {
|
||||
// Errors will always be logged
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message, ...args);
|
||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||
logToDb(message + argsString);
|
||||
const messageString = safeStringify(message);
|
||||
const argsString = args.length > 0 ? safeStringify(args) : "";
|
||||
logToDb(messageString + argsString);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -349,8 +349,9 @@
|
||||
</div>
|
||||
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
|
||||
<p class="text-sm mb-2 text-slate-500">
|
||||
For your security, choose a location nearby but not exactly at your
|
||||
place.
|
||||
The location you choose will be shared with the world until you remove
|
||||
this checkbox. For your security, choose a location nearby but not
|
||||
exactly at your true location, like at your town center.
|
||||
</p>
|
||||
|
||||
<l-map
|
||||
@@ -435,11 +436,11 @@
|
||||
<p class="text-sm">
|
||||
You have done
|
||||
<b
|
||||
>{{ endorserLimits?.doneClaimsThisWeek || "?" }} claim{{
|
||||
>{{ endorserLimits?.doneClaimsThisWeek ?? "?" }} claim{{
|
||||
endorserLimits?.doneClaimsThisWeek === 1 ? "" : "s"
|
||||
}}</b
|
||||
>
|
||||
out of <b>{{ endorserLimits?.maxClaimsPerWeek || "?" }}</b> for this
|
||||
out of <b>{{ endorserLimits?.maxClaimsPerWeek ?? "?" }}</b> for this
|
||||
week. Your claims counter resets at
|
||||
<b class="whitespace-nowrap">{{
|
||||
readableDate(endorserLimits?.nextWeekBeginDateTime)
|
||||
@@ -449,14 +450,14 @@
|
||||
You have done
|
||||
<b
|
||||
>{{
|
||||
endorserLimits?.doneRegistrationsThisMonth || "?"
|
||||
endorserLimits?.doneRegistrationsThisMonth ?? "?"
|
||||
}}
|
||||
registration{{
|
||||
endorserLimits?.doneRegistrationsThisMonth === 1 ? "" : "s"
|
||||
}}</b
|
||||
>
|
||||
out of
|
||||
<b>{{ endorserLimits?.maxRegistrationsPerMonth || "?" }}</b> for this
|
||||
<b>{{ endorserLimits?.maxRegistrationsPerMonth ?? "?" }}</b> for this
|
||||
this month.
|
||||
<i>(You cannot register anyone on your first day.)</i>
|
||||
Your registration counter resets at
|
||||
@@ -467,11 +468,11 @@
|
||||
<p class="mt-3 text-sm">
|
||||
You have uploaded
|
||||
<b
|
||||
>{{ imageLimits?.doneImagesThisWeek || "?" }} image{{
|
||||
>{{ imageLimits?.doneImagesThisWeek ?? "?" }} image{{
|
||||
imageLimits?.doneImagesThisWeek === 1 ? "" : "s"
|
||||
}}</b
|
||||
>
|
||||
out of <b>{{ imageLimits?.maxImagesPerWeek || "?" }}</b> for this
|
||||
out of <b>{{ imageLimits?.maxImagesPerWeek ?? "?" }}</b> for this
|
||||
week. Your image counter resets at
|
||||
<b class="whitespace-nowrap">{{
|
||||
readableDate(imageLimits?.nextWeekBeginDateTime)
|
||||
|
||||
@@ -49,21 +49,32 @@
|
||||
v-if="veriClaim.id"
|
||||
:to="'/claim-cert/' + encodeURIComponent(veriClaim.id)"
|
||||
class="text-blue-500 mt-2"
|
||||
title="Printable Certificate"
|
||||
title="View Printable Certificate"
|
||||
>
|
||||
<font-awesome
|
||||
icon="square"
|
||||
class="text-white bg-yellow-500 p-1"
|
||||
/>
|
||||
</router-link>
|
||||
<button
|
||||
v-if="veriClaim.id"
|
||||
class="text-blue-500 ml-2 mt-2"
|
||||
title="Copy Printable Certificate Link"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
'A link to the certificate page',
|
||||
`${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`,
|
||||
)
|
||||
"
|
||||
>
|
||||
<font-awesome icon="link" class="text-yellow-500 p-1" />
|
||||
</button>
|
||||
</div>
|
||||
<!-- show link icon to copy this URL to the clipboard -->
|
||||
<div class="flex justify-end w-full">
|
||||
<button
|
||||
title="Copy Link"
|
||||
@click="
|
||||
copyToClipboard('A link to this page', window.location.href)
|
||||
"
|
||||
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||
>
|
||||
<font-awesome icon="link" class="text-slate-500" />
|
||||
</button>
|
||||
@@ -405,7 +416,7 @@
|
||||
contacts can see more details:
|
||||
<a
|
||||
class="text-blue-500"
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||
>click to copy this page info</a
|
||||
>
|
||||
and see if they can make an introduction. Someone is connected to
|
||||
@@ -428,7 +439,7 @@
|
||||
If you'd like an introduction,
|
||||
<a
|
||||
class="text-blue-500"
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||
>share this page with them and ask if they'll tell you more about
|
||||
about the participants.</a
|
||||
>
|
||||
@@ -546,7 +557,7 @@ import { useClipboard } from "@vueuse/core";
|
||||
import { GenericVerifiableCredential } from "../interfaces";
|
||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { db } from "../db/index";
|
||||
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||
@@ -593,8 +604,9 @@ export default class ClaimView extends Vue {
|
||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||
veriClaimDump = "";
|
||||
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
||||
windowLocation = window.location.href;
|
||||
windowDeepLink = window.location.href; // changed in the setup for deep linking
|
||||
|
||||
APP_SERVER = APP_SERVER;
|
||||
R = R;
|
||||
yaml = yaml;
|
||||
libsUtil = libsUtil;
|
||||
@@ -671,6 +683,7 @@ export default class ClaimView extends Vue {
|
||||
5000,
|
||||
);
|
||||
}
|
||||
this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
|
||||
|
||||
this.canShare = !!navigator.share;
|
||||
}
|
||||
@@ -1006,11 +1019,11 @@ export default class ClaimView extends Vue {
|
||||
}
|
||||
|
||||
onClickShareClaim() {
|
||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
||||
this.copyToClipboard("A link to this page", this.windowDeepLink);
|
||||
window.navigator.share({
|
||||
title: "Help Connect Me",
|
||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||
url: this.windowLocation,
|
||||
url: this.windowDeepLink,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -436,7 +436,7 @@ import { Component, Vue } from "vue-facing-decorator";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
@@ -494,7 +494,7 @@ export default class ConfirmGiftView extends Vue {
|
||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||
veriClaimDump = "";
|
||||
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
||||
windowLocation = window.location.href;
|
||||
windowLocation = window.location.href; // this is changed to a deep link in the setup
|
||||
|
||||
R = R;
|
||||
yaml = yaml;
|
||||
@@ -566,6 +566,9 @@ export default class ConfirmGiftView extends Vue {
|
||||
}
|
||||
|
||||
const claimId = decodeURIComponent(pathParam);
|
||||
|
||||
this.windowLocation = APP_SERVER + "/deep-link/confirm-gift/" + claimId;
|
||||
|
||||
await this.loadClaim(claimId, this.activeDid);
|
||||
}
|
||||
|
||||
@@ -676,12 +679,12 @@ export default class ConfirmGiftView extends Vue {
|
||||
/**
|
||||
* Add participant (giver/recipient) name & URL info
|
||||
*/
|
||||
this.giverName = this.didInfo(this.giveDetails?.agentDid);
|
||||
if (this.giveDetails?.agentDid) {
|
||||
this.giverName = this.didInfo(this.giveDetails.agentDid);
|
||||
this.urlForNewGive += `&giverDid=${encodeURIComponent(this.giveDetails.agentDid)}&giverName=${encodeURIComponent(this.giverName)}`;
|
||||
}
|
||||
this.recipientName = this.didInfo(this.giveDetails?.recipientDid);
|
||||
if (this.giveDetails?.recipientDid) {
|
||||
this.recipientName = this.didInfo(this.giveDetails.recipientDid);
|
||||
this.urlForNewGive += `&recipientDid=${encodeURIComponent(this.giveDetails.recipientDid)}&recipientName=${encodeURIComponent(this.recipientName)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<!-- Breadcrumb -->
|
||||
<div id="ViewBreadcrumb" class="mb-8">
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<h1 class="text-2xl text-center font-semibold relative px-7">
|
||||
<!-- Back -->
|
||||
<router-link
|
||||
:to="{ name: 'home' }"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</router-link>
|
||||
Given by...
|
||||
{{ stepType === "giver" ? "Given by..." : "Given to..." }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -19,19 +19,18 @@
|
||||
<ul class="border-t border-slate-300">
|
||||
<li class="border-b border-slate-300 py-3">
|
||||
<h2 class="text-base flex gap-4 items-center">
|
||||
<span class="grow">
|
||||
<img
|
||||
src="../assets/blank-square.svg"
|
||||
width="32"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
<span class="grow flex gap-2 items-center font-medium">
|
||||
<font-awesome
|
||||
icon="circle-question"
|
||||
class="text-slate-400 text-4xl"
|
||||
/>
|
||||
Unnamed/Unknown
|
||||
<span class="italic text-slate-400">(Unnamed/Unknown)</span>
|
||||
</span>
|
||||
<span class="text-right">
|
||||
<button
|
||||
type="button"
|
||||
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
@click="openDialog()"
|
||||
@click="openDialog('Unnamed')"
|
||||
>
|
||||
<font-awesome icon="gift" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
@@ -44,13 +43,14 @@
|
||||
class="border-b border-slate-300 py-3"
|
||||
>
|
||||
<h2 class="text-base flex gap-4 items-center">
|
||||
<span class="grow font-semibold">
|
||||
<span class="grow flex gap-2 items-center font-medium">
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:icon-size="32"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
:icon-size="34"
|
||||
class="inline-block align-middle border border-slate-300 rounded-full overflow-hidden"
|
||||
/>
|
||||
{{ contact.name || "(no name)" }}
|
||||
<span v-if="contact.name">{{ contact.name }}</span>
|
||||
<span v-else class="italic text-slate-400">(No name)</span>
|
||||
</span>
|
||||
<span class="text-right">
|
||||
<button
|
||||
@@ -65,7 +65,13 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<GiftedDialog ref="customDialog" :to-project-id="projectId" />
|
||||
<GiftedDialog
|
||||
ref="customDialog"
|
||||
:from-project-id="fromProjectId"
|
||||
:to-project-id="toProjectId"
|
||||
:show-projects="showProjects"
|
||||
:is-from-project-view="isFromProjectView"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -97,6 +103,24 @@ export default class ContactGiftingView extends Vue {
|
||||
description = "";
|
||||
projectId = "";
|
||||
prompt = "";
|
||||
recipientProjectName = "";
|
||||
recipientProjectImage = "";
|
||||
recipientProjectHandleId = "";
|
||||
|
||||
// New context parameters
|
||||
stepType = "giver";
|
||||
giverEntityType = "person" as "person" | "project";
|
||||
recipientEntityType = "person" as "person" | "project";
|
||||
giverProjectId = "";
|
||||
giverProjectName = "";
|
||||
giverProjectImage = "";
|
||||
giverProjectHandleId = "";
|
||||
giverDid = "";
|
||||
recipientDid = "";
|
||||
fromProjectId = "";
|
||||
toProjectId = "";
|
||||
showProjects = false;
|
||||
isFromProjectView = false;
|
||||
|
||||
async created() {
|
||||
try {
|
||||
@@ -124,9 +148,41 @@ export default class ContactGiftingView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
this.projectId = (this.$route.query["projectId"] as string) || "";
|
||||
this.projectId =
|
||||
(this.$route.query["recipientProjectId"] as string) || "";
|
||||
this.recipientProjectName =
|
||||
(this.$route.query["recipientProjectName"] as string) || "";
|
||||
this.recipientProjectImage =
|
||||
(this.$route.query["recipientProjectImage"] as string) || "";
|
||||
this.recipientProjectHandleId =
|
||||
(this.$route.query["recipientProjectHandleId"] as string) || "";
|
||||
this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt;
|
||||
|
||||
// Read new context parameters
|
||||
this.stepType = (this.$route.query["stepType"] as string) || "giver";
|
||||
this.giverEntityType =
|
||||
(this.$route.query["giverEntityType"] as "person" | "project") ||
|
||||
"person";
|
||||
this.recipientEntityType =
|
||||
(this.$route.query["recipientEntityType"] as "person" | "project") ||
|
||||
"person";
|
||||
this.giverProjectId =
|
||||
(this.$route.query["giverProjectId"] as string) || "";
|
||||
this.giverProjectName =
|
||||
(this.$route.query["giverProjectName"] as string) || "";
|
||||
this.giverProjectImage =
|
||||
(this.$route.query["giverProjectImage"] as string) || "";
|
||||
this.giverProjectHandleId =
|
||||
(this.$route.query["giverProjectHandleId"] as string) || "";
|
||||
this.giverDid = (this.$route.query["giverDid"] as string) || "";
|
||||
this.recipientDid = (this.$route.query["recipientDid"] as string) || "";
|
||||
this.fromProjectId = (this.$route.query["fromProjectId"] as string) || "";
|
||||
this.toProjectId = (this.$route.query["toProjectId"] as string) || "";
|
||||
this.showProjects =
|
||||
(this.$route.query["showProjects"] as string) === "true";
|
||||
this.isFromProjectView =
|
||||
(this.$route.query["isFromProjectView"] as string) === "true";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
logger.error("Error retrieving settings & contacts:", err);
|
||||
@@ -144,17 +200,108 @@ export default class ContactGiftingView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
openDialog(giver?: GiverReceiverInputInfo) {
|
||||
const recipient = this.projectId
|
||||
? undefined
|
||||
: { did: this.activeDid, name: "you" };
|
||||
(this.$refs.customDialog as GiftedDialog).open(
|
||||
giver,
|
||||
recipient,
|
||||
undefined,
|
||||
"Given by " + (giver?.name || "someone not named"),
|
||||
this.prompt,
|
||||
);
|
||||
openDialog(contact?: GiverReceiverInputInfo | "Unnamed") {
|
||||
if (contact === "Unnamed") {
|
||||
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
|
||||
let recipient: GiverReceiverInputInfo;
|
||||
let giver: GiverReceiverInputInfo | undefined;
|
||||
|
||||
if (this.stepType === "giver") {
|
||||
// We're selecting a giver, so recipient is either a project or the current user
|
||||
if (this.recipientEntityType === "project") {
|
||||
recipient = {
|
||||
did: this.recipientProjectHandleId,
|
||||
name: this.recipientProjectName,
|
||||
image: this.recipientProjectImage,
|
||||
handleId: this.recipientProjectHandleId,
|
||||
};
|
||||
} else {
|
||||
recipient = { did: this.activeDid, name: "You" };
|
||||
}
|
||||
giver = undefined; // Will be set to "Unnamed" in GiftedDialog
|
||||
} else {
|
||||
// We're selecting a recipient, so recipient is "Unnamed" and giver is preserved from context
|
||||
recipient = { did: "", name: "Unnamed" };
|
||||
|
||||
// Preserve the existing giver from the context
|
||||
if (this.giverEntityType === "project") {
|
||||
giver = {
|
||||
// no did, because it's a project
|
||||
name: this.giverProjectName,
|
||||
image: this.giverProjectImage,
|
||||
handleId: this.giverProjectHandleId,
|
||||
};
|
||||
} else if (this.giverDid) {
|
||||
giver = {
|
||||
did: this.giverDid,
|
||||
name: this.giverProjectName || "Someone",
|
||||
};
|
||||
} else {
|
||||
giver = { did: this.activeDid, name: "You" };
|
||||
}
|
||||
}
|
||||
|
||||
(this.$refs.customDialog as GiftedDialog).open(
|
||||
giver,
|
||||
recipient,
|
||||
undefined,
|
||||
this.stepType === "giver" ? "Given by Unnamed" : "Given to Unnamed",
|
||||
this.prompt,
|
||||
);
|
||||
// Immediately select "Unnamed" and move to Step 2
|
||||
(this.$refs.customDialog as GiftedDialog).selectGiver();
|
||||
} else {
|
||||
// Regular case: contact is a GiverReceiverInputInfo
|
||||
let giver: GiverReceiverInputInfo;
|
||||
let recipient: GiverReceiverInputInfo;
|
||||
|
||||
if (this.stepType === "giver") {
|
||||
// We're selecting a giver, so the contact becomes the giver
|
||||
giver = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
|
||||
|
||||
// Recipient is either a project or the current user
|
||||
if (this.recipientEntityType === "project") {
|
||||
recipient = {
|
||||
did: this.recipientProjectHandleId,
|
||||
name: this.recipientProjectName,
|
||||
image: this.recipientProjectImage,
|
||||
handleId: this.recipientProjectHandleId,
|
||||
};
|
||||
} else {
|
||||
recipient = { did: this.activeDid, name: "You" };
|
||||
}
|
||||
} else {
|
||||
// We're selecting a recipient, so the contact becomes the recipient
|
||||
recipient = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
|
||||
|
||||
// Preserve the existing giver from the context
|
||||
if (this.giverEntityType === "project") {
|
||||
giver = {
|
||||
did: this.giverProjectHandleId,
|
||||
name: this.giverProjectName,
|
||||
image: this.giverProjectImage,
|
||||
handleId: this.giverProjectHandleId,
|
||||
};
|
||||
} else if (this.giverDid) {
|
||||
giver = {
|
||||
did: this.giverDid,
|
||||
name: this.giverProjectName || "Someone",
|
||||
};
|
||||
} else {
|
||||
giver = { did: this.activeDid, name: "You" };
|
||||
}
|
||||
}
|
||||
|
||||
(this.$refs.customDialog as GiftedDialog).open(
|
||||
giver,
|
||||
recipient,
|
||||
undefined,
|
||||
this.stepType === "giver"
|
||||
? "Given by " + (contact?.name || "someone not named")
|
||||
: "Given to " + (contact?.name || "someone not named"),
|
||||
this.prompt,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -124,12 +124,14 @@ import * as databaseUtil from "../db/databaseUtil";
|
||||
import {
|
||||
CONTACT_CSV_HEADER,
|
||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||
generateEndorserJwtUrlForAccount,
|
||||
setVisibilityUtil,
|
||||
} from "../libs/endorserServer";
|
||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||
import { retrieveAccountMetadata } from "../libs/util";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { parseJsonField } from "../db/databaseUtil";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
interface QRScanResult {
|
||||
rawValue?: string;
|
||||
@@ -157,6 +159,7 @@ export default class ContactQRScanFull extends Vue {
|
||||
apiServer = "";
|
||||
givenName = "";
|
||||
isRegistered = false;
|
||||
profileImageUrl = "";
|
||||
qrValue = "";
|
||||
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
||||
|
||||
@@ -179,6 +182,7 @@ export default class ContactQRScanFull extends Vue {
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.givenName = settings.firstName || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
this.profileImageUrl = settings.profileImageUrl || "";
|
||||
|
||||
const account = await retrieveAccountMetadata(this.activeDid);
|
||||
if (account) {
|
||||
@@ -588,9 +592,19 @@ export default class ContactQRScanFull extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
onCopyUrlToClipboard() {
|
||||
async onCopyUrlToClipboard() {
|
||||
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
||||
this.activeDid,
|
||||
)) as Account;
|
||||
const jwtUrl = await generateEndorserJwtUrlForAccount(
|
||||
account,
|
||||
this.isRegistered,
|
||||
this.givenName,
|
||||
this.profileImageUrl,
|
||||
true,
|
||||
);
|
||||
useClipboard()
|
||||
.copy(this.qrValue)
|
||||
.copy(jwtUrl)
|
||||
.then(() => {
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -177,6 +177,7 @@ import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
||||
import {
|
||||
CONTACT_CSV_HEADER,
|
||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||
generateEndorserJwtUrlForAccount,
|
||||
register,
|
||||
setVisibilityUtil,
|
||||
} from "../libs/endorserServer";
|
||||
@@ -187,6 +188,7 @@ import { logger } from "../utils/logger";
|
||||
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
||||
import { CameraState } from "@/services/QRScanner/types";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
interface QRScanResult {
|
||||
rawValue?: string;
|
||||
@@ -216,6 +218,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
isRegistered = false;
|
||||
qrValue = "";
|
||||
isScanning = false;
|
||||
profileImageUrl = "";
|
||||
error: string | null = null;
|
||||
|
||||
// QR Scanner properties
|
||||
@@ -253,6 +256,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
this.hideRegisterPromptOnNewContact =
|
||||
!!settings.hideRegisterPromptOnNewContact;
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
this.profileImageUrl = settings.profileImageUrl || "";
|
||||
|
||||
const account = await libsUtil.retrieveAccountMetadata(this.activeDid);
|
||||
if (account) {
|
||||
@@ -667,10 +671,19 @@ export default class ContactQRScanShow extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
onCopyUrlToClipboard() {
|
||||
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
|
||||
async onCopyUrlToClipboard() {
|
||||
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
||||
this.activeDid,
|
||||
)) as Account;
|
||||
const jwtUrl = await generateEndorserJwtUrlForAccount(
|
||||
account,
|
||||
this.isRegistered,
|
||||
this.givenName,
|
||||
this.profileImageUrl,
|
||||
true,
|
||||
);
|
||||
useClipboard()
|
||||
.copy(this.qrValue)
|
||||
.copy(jwtUrl)
|
||||
.then(() => {
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -126,7 +126,6 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
v-if="showGiveNumbers"
|
||||
href=""
|
||||
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
:class="showGiveAmountsClassNames()"
|
||||
@click="toggleShowGiveTotals()"
|
||||
@@ -142,7 +141,6 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
href=""
|
||||
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
@click="toggleShowContactAmounts()"
|
||||
>
|
||||
@@ -493,7 +491,7 @@ export default class ContactsView extends Vue {
|
||||
private async processContactJwt() {
|
||||
// handle a contact sent via URL
|
||||
//
|
||||
// For external links, use /contact-import/:jwt with a JWT that has an array of contacts
|
||||
// For external links, use /deep-link/contact-import/:jwt with a JWT that has an array of contacts
|
||||
// because that will do better error checking for things like missing data on iOS platforms.
|
||||
const importedContactJwt = this.$route.query["contactJwt"] as string;
|
||||
if (importedContactJwt) {
|
||||
@@ -619,7 +617,7 @@ export default class ContactsView extends Vue {
|
||||
title: "Error with Invite",
|
||||
text: message,
|
||||
},
|
||||
5000,
|
||||
-1,
|
||||
);
|
||||
}
|
||||
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
||||
@@ -1124,7 +1122,7 @@ export default class ContactsView extends Vue {
|
||||
(regResult.error as string) ||
|
||||
"Something went wrong during registration.",
|
||||
},
|
||||
5000,
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1158,7 +1156,7 @@ export default class ContactsView extends Vue {
|
||||
title: "Registration Error",
|
||||
text: userMessage,
|
||||
},
|
||||
5000,
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1397,7 +1395,8 @@ export default class ContactsView extends Vue {
|
||||
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
||||
contacts: selectedContacts,
|
||||
});
|
||||
const contactsJwtUrl = APP_SERVER + "/contact-import/" + contactsJwt;
|
||||
const contactsJwtUrl =
|
||||
APP_SERVER + "/deep-link/contact-import/" + contactsJwt;
|
||||
useClipboard()
|
||||
.copy(contactsJwtUrl)
|
||||
.then(() => {
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
@click="confirmSetVisibility(contactFromDid, false)"
|
||||
>
|
||||
<font-awesome icon="eye" class="fa-fw" />
|
||||
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="
|
||||
@@ -87,6 +88,32 @@
|
||||
@click="confirmSetVisibility(contactFromDid, true)"
|
||||
>
|
||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="
|
||||
contactFromDid?.iViewContent &&
|
||||
contactFromDid.did !== activeDid
|
||||
"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="I view their content"
|
||||
@click="confirmViewContent(contactFromDid, false)"
|
||||
>
|
||||
<font-awesome icon="eye" class="fa-fw" />
|
||||
<font-awesome icon="arrow-down" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="
|
||||
!contactFromDid?.iViewContent &&
|
||||
contactFromDid?.did !== activeDid
|
||||
"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="I do not view their content"
|
||||
@click="confirmViewContent(contactFromDid, true)"
|
||||
>
|
||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||
<font-awesome icon="arrow-down" class="fa-fw" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -825,9 +852,9 @@ export default class DIDView extends Vue {
|
||||
title: "Visibility Refreshed",
|
||||
text:
|
||||
libsUtil.nameForContact(contact, true) +
|
||||
" can " +
|
||||
(visibility ? "" : "not ") +
|
||||
"see your activity.",
|
||||
" can" +
|
||||
(visibility ? "" : " not") +
|
||||
" see your activity.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
@@ -857,6 +884,64 @@ export default class DIDView extends Vue {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm whether the user want to see/hide the other's content, then execute it
|
||||
*
|
||||
* @param contact Contact content to show/hide from user
|
||||
* @param view whether user wants to view this contact
|
||||
*/
|
||||
async confirmViewContent(contact: Contact, view: boolean) {
|
||||
const contentVisibilityPrompt = view
|
||||
? "Are you sure you want to see their content?"
|
||||
: "Are you sure you want to hide their content from you?";
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Set Content Visibility",
|
||||
text: contentVisibilityPrompt,
|
||||
onYes: async () => {
|
||||
const success = await this.setViewContent(contact, view);
|
||||
if (success) {
|
||||
contact.iViewContent = view; // see visibility note about not working inside setVisibility
|
||||
}
|
||||
},
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates contact content visibility for this device
|
||||
*
|
||||
* @param contact - Contact to update content visibility for
|
||||
* @param visibility - New content visibility state
|
||||
* @returns Boolean indicating success
|
||||
*/
|
||||
async setViewContent(contact: Contact, visibility: boolean) {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
"UPDATE contacts SET iViewContent = ? WHERE did = ?",
|
||||
[visibility, contact.did],
|
||||
);
|
||||
if (USE_DEXIE_DB) {
|
||||
db.contacts.update(contact.did, { iViewContent: visibility });
|
||||
}
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Visibility Set",
|
||||
text:
|
||||
"You will" +
|
||||
(visibility ? "" : " not") +
|
||||
` see ${contact.name}'s activity.`,
|
||||
},
|
||||
3000,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
icon-name="chart"
|
||||
svg-class="-ml-1 mr-3 h-5 w-5"
|
||||
/>
|
||||
Download Account
|
||||
Show Account Seed
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -1122,6 +1122,7 @@ export default class DatabaseMigration extends Vue {
|
||||
private loadingMessage = "";
|
||||
private error = "";
|
||||
private warning = "";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private exportedData: Record<string, any> | null = null;
|
||||
private successMessage = "";
|
||||
|
||||
@@ -1134,6 +1135,7 @@ export default class DatabaseMigration extends Vue {
|
||||
* @param {any} setting - The setting object
|
||||
* @returns {string} The display name for the setting
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getSettingDisplayName(setting: any): string {
|
||||
// Handle exported JSON format (has 'type' and 'did' fields)
|
||||
if (setting.type && setting.did) {
|
||||
@@ -1153,6 +1155,7 @@ export default class DatabaseMigration extends Vue {
|
||||
* @param {any} account - The account object
|
||||
* @returns {boolean} True if account has identity
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getAccountHasIdentity(account: any): boolean {
|
||||
// Handle exported JSON format (has 'hasIdentity' field)
|
||||
if (account.hasIdentity !== undefined) {
|
||||
@@ -1170,6 +1173,7 @@ export default class DatabaseMigration extends Vue {
|
||||
* @param {any} account - The account object
|
||||
* @returns {boolean} True if account has mnemonic
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getAccountHasMnemonic(account: any): boolean {
|
||||
// Handle exported JSON format (has 'hasMnemonic' field)
|
||||
if (account.hasMnemonic !== undefined) {
|
||||
|
||||
@@ -66,9 +66,14 @@ const formattedPath = computed(() => {
|
||||
const path = originalPath.value.replace(/^\/+/, "");
|
||||
|
||||
// Log for debugging
|
||||
logger.log("Original Path:", originalPath.value);
|
||||
logger.log("Route Params:", route.params);
|
||||
logger.log("Route Query:", route.query);
|
||||
logger.log(
|
||||
"[DeepLinkError] Original Path:",
|
||||
originalPath.value,
|
||||
"Route Params:",
|
||||
route.params,
|
||||
"Route Query:",
|
||||
route.query,
|
||||
);
|
||||
|
||||
return path;
|
||||
});
|
||||
|
||||
227
src/views/DeepLinkRedirectView.vue
Normal file
227
src/views/DeepLinkRedirectView.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="relative w-[100vw] h-[100vh]">
|
||||
<div
|
||||
class="p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<h1 class="text-xl text-center font-semibold relative mb-4">
|
||||
Redirecting to Time Safari
|
||||
</h1>
|
||||
|
||||
<div v-if="destinationUrl" class="space-y-4">
|
||||
<!-- Platform-specific messaging -->
|
||||
<div class="text-center text-gray-600 mb-4">
|
||||
<p v-if="isMobile">
|
||||
{{
|
||||
isIOS
|
||||
? "Opening Time Safari app on your iPhone..."
|
||||
: "Opening Time Safari app on your Android device..."
|
||||
}}
|
||||
</p>
|
||||
<p v-else>Opening Time Safari app...</p>
|
||||
<p class="text-sm mt-2">
|
||||
<span v-if="isMobile"
|
||||
>If the app doesn't open automatically, use one of these
|
||||
options:</span
|
||||
>
|
||||
<span v-else>Choose how you'd like to open this link:</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Deep Link Button -->
|
||||
<div class="text-center">
|
||||
<a
|
||||
:href="deepLinkUrl || '#'"
|
||||
class="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||
@click="handleDeepLinkClick"
|
||||
>
|
||||
<span v-if="isMobile">Open in Time Safari App</span>
|
||||
<span v-else>Try Opening in Time Safari App</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Web Fallback Link -->
|
||||
<div class="text-center">
|
||||
<a
|
||||
:href="webUrl || '#'"
|
||||
target="_blank"
|
||||
class="inline-block bg-gray-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-gray-700 transition-colors"
|
||||
@click="handleWebFallbackClick"
|
||||
>
|
||||
<span v-if="isMobile">Open in Web Browser Instead</span>
|
||||
<span v-else>Open in Web Browser</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Manual Instructions -->
|
||||
<div class="text-center text-sm text-gray-500 mt-4">
|
||||
<p v-if="isMobile">
|
||||
Or manually open:
|
||||
<code class="bg-gray-100 px-2 py-1 rounded">{{
|
||||
deepLinkUrl
|
||||
}}</code>
|
||||
</p>
|
||||
<p v-else>
|
||||
If you have the Time Safari app installed, you can also copy this
|
||||
link:
|
||||
<code class="bg-gray-100 px-2 py-1 rounded">{{
|
||||
deepLinkUrl
|
||||
}}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Platform info for debugging -->
|
||||
<div
|
||||
v-if="isDevelopment"
|
||||
class="text-center text-xs text-gray-400 mt-4"
|
||||
>
|
||||
<p>
|
||||
Platform: {{ isMobile ? (isIOS ? "iOS" : "Android") : "Desktop" }}
|
||||
</p>
|
||||
<p>User Agent: {{ userAgent.substring(0, 50) }}...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="pageError" class="text-center text-red-500 mb-4">
|
||||
{{ pageError }}
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center text-gray-600">
|
||||
<p>Processing redirect...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import { APP_SERVER } from "@/constants/app";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { errorStringForLog } from "@/libs/endorserServer";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
@Component({})
|
||||
export default class DeepLinkRedirectView extends Vue {
|
||||
$router!: Router;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
pageError: string | null = null;
|
||||
destinationUrl: string | null = null; // full path after "/deep-link/"
|
||||
deepLinkUrl: string | null = null; // mobile link starting "timesafari://"
|
||||
webUrl: string | null = null; // web link, eg "https://timesafari.app/..."
|
||||
isDevelopment: boolean = false;
|
||||
userAgent: string = "";
|
||||
private platformService = PlatformServiceFactory.getInstance();
|
||||
|
||||
mounted() {
|
||||
// Get the path from the route parameter (catch-all parameter)
|
||||
const pathParam = this.$route.params.path;
|
||||
|
||||
// If pathParam is an array (catch-all parameter), join it
|
||||
const fullPath = Array.isArray(pathParam) ? pathParam.join("/") : pathParam;
|
||||
|
||||
// Get query parameters from the route
|
||||
const queryParams = this.$route.query;
|
||||
|
||||
// Build query string if there are query parameters
|
||||
let queryString = "";
|
||||
if (Object.keys(queryParams).length > 0) {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
const stringValue = Array.isArray(value) ? value[0] : value;
|
||||
if (stringValue !== null && stringValue !== undefined) {
|
||||
searchParams.append(key, stringValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
queryString = "?" + searchParams.toString();
|
||||
}
|
||||
|
||||
// Combine path with query parameters
|
||||
const fullPathWithQuery = fullPath + queryString;
|
||||
|
||||
this.destinationUrl = fullPathWithQuery;
|
||||
this.deepLinkUrl = `timesafari://${fullPathWithQuery}`;
|
||||
this.webUrl = `${APP_SERVER}/${fullPathWithQuery}`;
|
||||
|
||||
this.isDevelopment = process.env.NODE_ENV !== "production";
|
||||
this.userAgent = navigator.userAgent;
|
||||
|
||||
this.openDeepLink();
|
||||
}
|
||||
|
||||
private openDeepLink() {
|
||||
if (!this.deepLinkUrl || !this.webUrl) {
|
||||
this.pageError =
|
||||
"No deep link was provided. Check the URL and try again.";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// For mobile, try the deep link URL; for desktop, use the web URL
|
||||
const redirectUrl = this.isMobile ? this.deepLinkUrl : this.webUrl;
|
||||
|
||||
// Method 1: Try window.location.href (works on most browsers)
|
||||
window.location.href = redirectUrl;
|
||||
|
||||
// Method 2: Fallback - create and click a link element
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const link = document.createElement("a");
|
||||
link.href = redirectUrl;
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Fallback deep link failed: " + errorStringForLog(error),
|
||||
);
|
||||
this.pageError =
|
||||
"Redirecting to the Time Safari app failed. Please use a manual option below.";
|
||||
}
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
logger.error("Deep link redirect failed: " + errorStringForLog(error));
|
||||
this.pageError =
|
||||
"Unable to open the Time Safari app. Please use a manual option below.";
|
||||
}
|
||||
}
|
||||
|
||||
private handleDeepLinkClick(event: Event) {
|
||||
if (!this.deepLinkUrl) return;
|
||||
|
||||
// Prevent default to handle the click manually
|
||||
event.preventDefault();
|
||||
|
||||
this.openDeepLink();
|
||||
}
|
||||
|
||||
private handleWebFallbackClick(event: Event) {
|
||||
if (!this.webUrl) return;
|
||||
|
||||
// Get platform capabilities
|
||||
const capabilities = this.platformService.getCapabilities();
|
||||
|
||||
// For mobile, try to open in a new tab/window
|
||||
if (capabilities.isMobile) {
|
||||
event.preventDefault();
|
||||
window.open(this.webUrl, "_blank");
|
||||
}
|
||||
// For desktop, let the default behavior happen (opens in same tab)
|
||||
}
|
||||
|
||||
// Computed properties for template
|
||||
get isMobile(): boolean {
|
||||
return this.platformService.getCapabilities().isMobile;
|
||||
}
|
||||
|
||||
get isIOS(): boolean {
|
||||
return this.platformService.getCapabilities().isIOS;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -4,84 +4,91 @@
|
||||
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<!-- Back -->
|
||||
<div
|
||||
v-if="!hideBackButton"
|
||||
class="text-lg text-center font-light relative px-7"
|
||||
>
|
||||
<h1
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="cancelBack()"
|
||||
>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
<!-- Breadcrumb -->
|
||||
<div id="ViewBreadcrumb" class="mb-8">
|
||||
<h1 class="text-2xl text-center font-semibold relative px-7 mb-2">
|
||||
<!-- Back -->
|
||||
<div
|
||||
v-if="!hideBackButton"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="cancelBack()"
|
||||
>
|
||||
<font-awesome icon="chevron-left" class="fa-fw" />
|
||||
</div>
|
||||
What Was Given
|
||||
</h1>
|
||||
|
||||
<h2 class="text-lg font-normal text-center overflow-hidden">
|
||||
<div class="truncate">
|
||||
From
|
||||
{{
|
||||
providedByProject
|
||||
? providerProjectName
|
||||
: providedByGiver
|
||||
? giverName
|
||||
: "someone not named"
|
||||
}}
|
||||
</div>
|
||||
<div class="truncate">
|
||||
to
|
||||
{{
|
||||
givenToProject
|
||||
? fulfillsProjectName
|
||||
: givenToRecipient
|
||||
? recipientName
|
||||
: "someone not named"
|
||||
}}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Heading -->
|
||||
<h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1>
|
||||
|
||||
<h1 class="text-xl font-bold text-center mb-4">
|
||||
<span>
|
||||
From
|
||||
{{
|
||||
providedByProject
|
||||
? providerProjectName
|
||||
: providedByGiver
|
||||
? giverName
|
||||
: "someone not named"
|
||||
}}
|
||||
</span>
|
||||
<br />
|
||||
<span>
|
||||
to
|
||||
{{
|
||||
givenToProject
|
||||
? fulfillsProjectName
|
||||
: givenToRecipient
|
||||
? recipientName
|
||||
: "someone not named"
|
||||
}}</span
|
||||
>
|
||||
</h1>
|
||||
<textarea
|
||||
v-model="description"
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
placeholder="What was received"
|
||||
/>
|
||||
<div class="flex flex-row justify-center">
|
||||
<span
|
||||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
|
||||
@click="changeUnitCode()"
|
||||
>
|
||||
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
|
||||
</span>
|
||||
<div
|
||||
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||
<div class="flex mb-4">
|
||||
<button
|
||||
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="amountInput === '0' ? null : decrement()"
|
||||
>
|
||||
<font-awesome icon="chevron-left" />
|
||||
</div>
|
||||
</button>
|
||||
<input
|
||||
id="inputGivenAmount"
|
||||
v-model="amountInput"
|
||||
type="number"
|
||||
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
|
||||
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
|
||||
/>
|
||||
<div
|
||||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||
<button
|
||||
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="increment()"
|
||||
>
|
||||
<font-awesome icon="chevron-right" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<select
|
||||
v-model="unitCode"
|
||||
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
|
||||
>
|
||||
<option
|
||||
v-for="(displayName, code) in unitOptions"
|
||||
:key="code"
|
||||
:value="code"
|
||||
>
|
||||
{{ displayName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mt-4" data-testId="imagery">
|
||||
<span v-if="imageUrl" class="flex justify-between">
|
||||
<span v-if="imageUrl" class="flex items-end gap-3">
|
||||
<a :href="imageUrl" target="_blank">
|
||||
<img :src="imageUrl" class="h-24 rounded-xl" />
|
||||
<img :src="imageUrl" class="h-36 rounded-lg" />
|
||||
</a>
|
||||
<font-awesome
|
||||
icon="trash-can"
|
||||
class="text-red-500 fa-fw ml-8 mt-10"
|
||||
class="text-red-500 fa-fw cursor-pointer"
|
||||
@click="confirmDeleteImage"
|
||||
/>
|
||||
</span>
|
||||
@@ -95,22 +102,22 @@
|
||||
</div>
|
||||
<ImageMethodDialog ref="imageDialog" default-camera-mode="environment" />
|
||||
|
||||
<div class="mt-4 flex justify-between gap-2">
|
||||
<div class="mt-4 sm:flex justify-between gap-2">
|
||||
<!-- First Column for Giver -->
|
||||
<div class="flex-grow border border-slate-400 p-2 rounded-md">
|
||||
<div class="flex">
|
||||
<div class="sm:flex-grow sm:w-1/2 border border-slate-400 p-2 rounded-md overflow-hidden">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
v-if="giverDid && !providedByProject"
|
||||
v-model="providedByGiver"
|
||||
type="checkbox"
|
||||
class="h-6 w-6 mr-2"
|
||||
class="flex-shrink-0 h-6 w-6 mr-2"
|
||||
/>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="square"
|
||||
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
/>
|
||||
<label class="text-sm mt-1">
|
||||
<label class="text-sm truncate">
|
||||
{{
|
||||
giverDid
|
||||
? "This was provided by " + giverName + "."
|
||||
@@ -120,24 +127,24 @@
|
||||
<font-awesome
|
||||
v-if="!giverDid || providedByProject"
|
||||
icon="info-circle"
|
||||
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
class="text-base cursor-pointer bg-white text-slate-500 ms-1"
|
||||
@click="notifyUserOfGiver()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
v-if="providerProjectId && !providedByGiver"
|
||||
v-model="providedByProject"
|
||||
type="checkbox"
|
||||
class="h-6 w-6 mr-2"
|
||||
class="flex-shrink-0 h-6 w-6 mr-2"
|
||||
/>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="square"
|
||||
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
/>
|
||||
<label class="text-sm mt-1">
|
||||
<label class="text-sm truncate">
|
||||
{{
|
||||
providerProjectId
|
||||
? "This was provided by " + providerProjectName + "."
|
||||
@@ -147,31 +154,31 @@
|
||||
<font-awesome
|
||||
v-if="!providerProjectId || providedByGiver"
|
||||
icon="info-circle"
|
||||
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
class="text-base cursor-pointer bg-white text-slate-500 ms-1"
|
||||
@click="notifyUserOfProvidingProject()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink flex justify-center items-center">
|
||||
<font-awesome icon="arrow-right" class="fa-fw h-7" />
|
||||
<div class="sm:flex-shrink flex justify-center items-center my-1 sm:my-0">
|
||||
<font-awesome icon="arrow-right" class="fa-fw h-7 rotate-90 sm:rotate-0" />
|
||||
</div>
|
||||
|
||||
<!-- Third Column for Recipient -->
|
||||
<div class="flex-grow border border-slate-400 p-2 rounded-md">
|
||||
<div class="flex">
|
||||
<div class="sm:flex-grow sm:w-1/2 border border-slate-400 p-2 rounded-md overflow-hidden">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
v-if="recipientDid && !givenToProject"
|
||||
v-model="givenToRecipient"
|
||||
type="checkbox"
|
||||
class="h-6 w-6 mr-2"
|
||||
class="flex-shrink-0 h-6 w-6 mr-2"
|
||||
/>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="square"
|
||||
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
/>
|
||||
<label class="text-sm mt-1">
|
||||
<label class="text-sm truncate">
|
||||
{{
|
||||
recipientDid
|
||||
? "This was given to " + recipientName + "."
|
||||
@@ -181,24 +188,24 @@
|
||||
<font-awesome
|
||||
v-if="!recipientDid || givenToProject"
|
||||
icon="info-circle"
|
||||
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
class="text-base cursor-pointer bg-white text-slate-500 ms-1"
|
||||
@click="notifyUserOfRecipient()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
v-if="fulfillsProjectId && !givenToRecipient"
|
||||
v-model="givenToProject"
|
||||
type="checkbox"
|
||||
class="h-6 w-6 mr-2"
|
||||
class="flex-shrink-0 h-6 w-6 mr-2"
|
||||
/>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="square"
|
||||
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
/>
|
||||
<label class="text-sm mt-1">
|
||||
<label class="text-sm truncate">
|
||||
{{
|
||||
fulfillsProjectId
|
||||
? "This was given to " + fulfillsProjectName + ". "
|
||||
@@ -208,7 +215,7 @@
|
||||
<font-awesome
|
||||
v-if="!fulfillsProjectId || givenToRecipient"
|
||||
icon="info-circle"
|
||||
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
class="text-base cursor-pointer bg-white text-slate-500 ms-1"
|
||||
@click="notifyUserFulfillsProject()"
|
||||
/>
|
||||
</div>
|
||||
@@ -229,11 +236,11 @@
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<p class="text-center mb-2 mt-6 italic">
|
||||
Sign & Send to publish to the world
|
||||
<p class="text-center text-sm my-4">
|
||||
<b class="font-medium">Sign & Send</b> to publish to the world
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="pl-2 text-blue-500 cursor-pointer"
|
||||
class="fa-fw text-blue-500 text-base cursor-pointer"
|
||||
@click="explainData()"
|
||||
/>
|
||||
</p>
|
||||
@@ -910,5 +917,10 @@ export default class GiftedDetails extends Vue {
|
||||
7000,
|
||||
);
|
||||
}
|
||||
|
||||
// Computed property to get unit options
|
||||
get unitOptions() {
|
||||
return this.libsUtil.UNIT_SHORT;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,6 +12,7 @@ Raymer * @version 1.0.0 */
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
|
||||
{{ AppString.APP_NAME }}
|
||||
<span class="text-xs text-gray-500">{{ package.version }}</span>
|
||||
</h1>
|
||||
|
||||
<OnboardingDialog ref="onboardingDialog" />
|
||||
@@ -117,101 +118,73 @@ Raymer * @version 1.0.0 */
|
||||
</div>
|
||||
|
||||
<div v-else id="sectionRecordSomethingGiven">
|
||||
<!-- !isCreatingIdentifier && isRegistered -->
|
||||
<!-- Record Quick-Action -->
|
||||
<div class="mb-6">
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<h2 class="text-xl font-bold">Record something given by:</h2>
|
||||
<button
|
||||
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
|
||||
@click="openGiftedPrompts()"
|
||||
>
|
||||
<font-awesome
|
||||
icon="lightbulb"
|
||||
class="block text-center w-[1em]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- show the actions for recognizing a give -->
|
||||
<div class="flex">
|
||||
<h2 class="text-xl font-bold">What have you seen someone do?</h2>
|
||||
<button
|
||||
class="ml-2 block text-xs text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
|
||||
@click="openGiftedPrompts()"
|
||||
>
|
||||
<font-awesome icon="lightbulb" class="fa-fw" />
|
||||
</button>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
|
||||
@click="openDialogPerson()"
|
||||
>
|
||||
<font-awesome icon="user" />
|
||||
Person
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
|
||||
@click="openProjectDialog()"
|
||||
>
|
||||
<font-awesome icon="folder-open" />
|
||||
Project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mt-4"
|
||||
>
|
||||
<li @click="openDialog()">
|
||||
<img
|
||||
src="../assets/blank-square.svg"
|
||||
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
|
||||
/>
|
||||
<h3
|
||||
class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
|
||||
>
|
||||
Unnamed/Unknown
|
||||
</h3>
|
||||
</li>
|
||||
<li v-if="allContacts.length === 0" class="text-sm">
|
||||
(Add friends to see more people worthy of recognition.)
|
||||
</li>
|
||||
<li
|
||||
v-for="contact in allContacts.slice(0, 6)"
|
||||
:key="contact.did"
|
||||
@click="openDialog(contact)"
|
||||
>
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:icon-size="64"
|
||||
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
|
||||
/>
|
||||
<h3
|
||||
class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
|
||||
>
|
||||
{{ contact.name || contact.did }}
|
||||
</h3>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
v-if="allContacts.length >= 6"
|
||||
:to="{ name: 'contact-gift' }"
|
||||
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
|
||||
>
|
||||
... or someone else...
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GiftedDialog ref="customDialog" />
|
||||
<GiftedDialog ref="customDialog" :show-projects="showProjectsDialog" />
|
||||
<GiftedPrompts ref="giftedPrompts" />
|
||||
<FeedFilters ref="feedFilters" />
|
||||
|
||||
<div class="relative">
|
||||
<button
|
||||
v-if="isRegistered"
|
||||
class="absolute right-6 bottom-0 transform translate-y-1/2 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
|
||||
@click="openDialog()"
|
||||
>
|
||||
<font-awesome icon="plus" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Results List -->
|
||||
<div class="mt-4 mb-4">
|
||||
<div class="flex items-center mb-4">
|
||||
<h2 class="text-xl font-bold flex items-center gap-4">
|
||||
Latest Activity
|
||||
<button
|
||||
v-if="resultsAreFiltered()"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
|
||||
@click="openFeedFilters()"
|
||||
>
|
||||
<font-awesome icon="filter" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
|
||||
@click="openFeedFilters()"
|
||||
>
|
||||
<font-awesome icon="filter" class="fa-fw" />
|
||||
</button>
|
||||
</h2>
|
||||
<div class="flex gap-2 items-center mb-3">
|
||||
<h2 class="text-xl font-bold">Latest Activity</h2>
|
||||
<button
|
||||
v-if="resultsAreFiltered()"
|
||||
class="block ms-auto text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
|
||||
@click="openFeedFilters()"
|
||||
>
|
||||
<font-awesome
|
||||
icon="filter"
|
||||
class="block text-center w-[1em] translate-y-[0.05em]"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
|
||||
@click="openFeedFilters()"
|
||||
>
|
||||
<font-awesome
|
||||
icon="filter"
|
||||
class="block text-center w-[1em] translate-y-[0.05em]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -353,6 +326,7 @@ import * as serverUtil from "../libs/endorserServer";
|
||||
import { logger } from "../utils/logger";
|
||||
import { GiveRecordWithContactInfo } from "../interfaces/give";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import * as Package from "../../package.json";
|
||||
|
||||
interface Claim {
|
||||
claim?: Claim; // For nested claims in Verifiable Credentials
|
||||
@@ -443,11 +417,13 @@ export default class HomeView extends Vue {
|
||||
|
||||
AppString = AppString;
|
||||
PASSKEYS_ENABLED = PASSKEYS_ENABLED;
|
||||
package = Package;
|
||||
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: Array<string> = [];
|
||||
apiServer = "";
|
||||
blockedContactDids: Array<string> = [];
|
||||
feedData: GiveRecordWithContactInfo[] = [];
|
||||
feedPreviousOldestId?: string;
|
||||
feedLastViewedClaimId?: string;
|
||||
@@ -474,6 +450,7 @@ export default class HomeView extends Vue {
|
||||
selectedImageData: Blob | null = null;
|
||||
isImageViewerOpen = false;
|
||||
imageCache: Map<string, Blob | null> = new Map();
|
||||
showProjectsDialog = false;
|
||||
|
||||
/**
|
||||
* Initializes the component on mount
|
||||
@@ -519,7 +496,6 @@ export default class HomeView extends Vue {
|
||||
// Retrieve DIDs with better error handling
|
||||
try {
|
||||
this.allMyDids = await retrieveAccountDids();
|
||||
logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`);
|
||||
} catch (error) {
|
||||
logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
|
||||
throw new Error(
|
||||
@@ -552,9 +528,6 @@ export default class HomeView extends Vue {
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
logConsoleAndDb(
|
||||
`[HomeView] Retrieved settings for ${settings.activeDid || "no active DID"}`,
|
||||
);
|
||||
} catch (error) {
|
||||
logConsoleAndDb(
|
||||
`[HomeView] Failed to retrieve settings: ${error}`,
|
||||
@@ -571,25 +544,14 @@ export default class HomeView extends Vue {
|
||||
|
||||
// Load contacts with graceful fallback
|
||||
try {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbContacts = await platformService.dbQuery(
|
||||
"SELECT * FROM contacts",
|
||||
);
|
||||
this.allContacts = databaseUtil.mapQueryResultToValues(
|
||||
dbContacts,
|
||||
) as Contact[];
|
||||
if (USE_DEXIE_DB) {
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
}
|
||||
logConsoleAndDb(
|
||||
`[HomeView] Retrieved ${this.allContacts.length} contacts`,
|
||||
);
|
||||
this.loadContacts();
|
||||
} catch (error) {
|
||||
logConsoleAndDb(
|
||||
`[HomeView] Failed to retrieve contacts: ${error}`,
|
||||
true,
|
||||
);
|
||||
this.allContacts = []; // Ensure we have a valid empty array
|
||||
this.blockedContactDids = [];
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -641,9 +603,6 @@ export default class HomeView extends Vue {
|
||||
});
|
||||
}
|
||||
this.isRegistered = true;
|
||||
logConsoleAndDb(
|
||||
`[HomeView] User ${this.activeDid} is now registered`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logConsoleAndDb(
|
||||
@@ -685,11 +644,6 @@ export default class HomeView extends Vue {
|
||||
this.newOffersToUserHitLimit = offersToUser.hitLimit;
|
||||
this.numNewOffersToUserProjects = offersToProjects.data.length;
|
||||
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
|
||||
|
||||
logConsoleAndDb(
|
||||
`[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` +
|
||||
`${this.numNewOffersToUserProjects} project offers`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logConsoleAndDb(
|
||||
@@ -761,6 +715,9 @@ export default class HomeView extends Vue {
|
||||
if (USE_DEXIE_DB) {
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
}
|
||||
this.blockedContactDids = this.allContacts
|
||||
.filter((c) => !c.iViewContent)
|
||||
.map((c) => c.did);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1028,6 +985,7 @@ export default class HomeView extends Vue {
|
||||
);
|
||||
if (results.data.length > 0) {
|
||||
endOfResults = false;
|
||||
// gather any contacts that user has blocked from view
|
||||
await this.processFeedResults(results.data);
|
||||
await this.updateFeedLastViewedId(results.data);
|
||||
}
|
||||
@@ -1215,7 +1173,7 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if record should be included based on filters
|
||||
* Checks if record should be included based on filters & preferences
|
||||
*
|
||||
* @internal
|
||||
* @callGraph
|
||||
@@ -1241,6 +1199,10 @@ export default class HomeView extends Vue {
|
||||
record: GiveSummaryRecord,
|
||||
fulfillsPlan?: FulfillsPlan,
|
||||
): boolean {
|
||||
if (this.blockedContactDids.includes(record.issuerDid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.isAnyFeedFilterOn) {
|
||||
return true;
|
||||
}
|
||||
@@ -1637,17 +1599,33 @@ export default class HomeView extends Vue {
|
||||
* @param giver Optional contact info for giver
|
||||
* @param description Optional gift description
|
||||
*/
|
||||
openDialog(giver?: GiverReceiverInputInfo, description?: string) {
|
||||
(this.$refs.customDialog as GiftedDialog).open(
|
||||
giver,
|
||||
{
|
||||
did: this.activeDid,
|
||||
name: "you",
|
||||
} as GiverReceiverInputInfo,
|
||||
undefined,
|
||||
"Given by " + (giver?.name || "someone not named"),
|
||||
description,
|
||||
);
|
||||
openDialog(giver?: GiverReceiverInputInfo | "Unnamed", description?: string) {
|
||||
if (giver === "Unnamed") {
|
||||
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
|
||||
(this.$refs.customDialog as GiftedDialog).open(
|
||||
undefined,
|
||||
{
|
||||
did: this.activeDid,
|
||||
name: "You",
|
||||
} as GiverReceiverInputInfo,
|
||||
undefined,
|
||||
"Given by Unnamed",
|
||||
description,
|
||||
);
|
||||
// Immediately select "Unnamed" and move to Step 2
|
||||
(this.$refs.customDialog as GiftedDialog).selectGiver();
|
||||
} else {
|
||||
(this.$refs.customDialog as GiftedDialog).open(
|
||||
giver,
|
||||
{
|
||||
did: this.activeDid,
|
||||
name: "You",
|
||||
} as GiverReceiverInputInfo,
|
||||
undefined,
|
||||
"Given by " + (giver?.name || "someone not named"),
|
||||
description,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1881,5 +1859,18 @@ export default class HomeView extends Vue {
|
||||
this.$router.push({ name: "contact-qr" });
|
||||
}
|
||||
}
|
||||
|
||||
openDialogPerson(
|
||||
giver?: GiverReceiverInputInfo | "Unnamed",
|
||||
description?: string,
|
||||
) {
|
||||
this.showProjectsDialog = false;
|
||||
this.openDialog(giver, description);
|
||||
}
|
||||
|
||||
openProjectDialog() {
|
||||
this.showProjectsDialog = true;
|
||||
(this.$refs.customDialog as any).open();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -241,7 +241,7 @@ export default class InviteOneView extends Vue {
|
||||
}
|
||||
|
||||
inviteLink(jwt: string): string {
|
||||
return APP_SERVER + "/invite-one-accept/" + jwt;
|
||||
return APP_SERVER + "/deep-link/invite-one-accept/" + jwt;
|
||||
}
|
||||
|
||||
copyInviteAndNotify(inviteId: string, jwt: string) {
|
||||
|
||||
@@ -720,7 +720,7 @@ export default class OnboardMeetingView extends Vue {
|
||||
|
||||
onboardMeetingMembersLink(): string {
|
||||
if (this.currentMeeting) {
|
||||
return `${APP_SERVER}/onboard-meeting-members/${this.currentMeeting?.groupId}?password=${encodeURIComponent(
|
||||
return `${APP_SERVER}/deep-link/onboard-meeting-members/${this.currentMeeting?.groupId}?password=${encodeURIComponent(
|
||||
this.currentMeeting?.password || "",
|
||||
)}`;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
>
|
||||
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||
</button>
|
||||
<button title="Copy Link to Project" @click="onCopyLinkClick()">
|
||||
<font-awesome
|
||||
icon="link"
|
||||
class="text-sm text-slate-500 ml-2 mb-1"
|
||||
/>
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,7 +61,11 @@
|
||||
<span class="truncate inline-block max-w-[calc(100%-2rem)]">
|
||||
{{ issuerInfoObject?.displayName }}
|
||||
</span>
|
||||
<span class="inline-flex items-center">
|
||||
|
||||
<span
|
||||
v-if="!serverUtil.isHiddenDid(issuer)"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<router-link
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(issuer),
|
||||
@@ -113,7 +123,7 @@
|
||||
class="fa-fw text-slate-400"
|
||||
></font-awesome>
|
||||
<a
|
||||
:href="addScheme(url)"
|
||||
:href="ensureScheme(url)"
|
||||
target="_blank"
|
||||
class="underline text-blue-500"
|
||||
>
|
||||
@@ -204,63 +214,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeDid && isRegistered">
|
||||
<div class="text-center">
|
||||
<p class="mt-2 mt-4 text-center">Record a contribution from:</p>
|
||||
</div>
|
||||
<ul
|
||||
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5 mt-2"
|
||||
>
|
||||
<li @click="openGiftDialogToProject({ name: 'you', did: activeDid })">
|
||||
<font-awesome
|
||||
icon="hand"
|
||||
class="fa-fw text-blue-500 text-5xl cursor-pointer"
|
||||
/>
|
||||
<h3
|
||||
class="mt-5 text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
|
||||
>
|
||||
You
|
||||
</h3>
|
||||
</li>
|
||||
<li @click="openGiftDialogToProject()">
|
||||
<img
|
||||
src="../assets/blank-square.svg"
|
||||
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
|
||||
/>
|
||||
<h3
|
||||
class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
|
||||
>
|
||||
Unnamed/Unknown
|
||||
</h3>
|
||||
</li>
|
||||
<li
|
||||
v-for="contact in allContacts.slice(0, 5)"
|
||||
:key="contact.did"
|
||||
@click="openGiftDialogToProject(contact)"
|
||||
>
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:icon-size="64"
|
||||
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
|
||||
/>
|
||||
<h3
|
||||
class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
|
||||
>
|
||||
{{ contact.name || "(no name)" }}
|
||||
</h3>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
v-if="allContacts.length >= 5"
|
||||
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
|
||||
@click="onClickAllContactsGifting()"
|
||||
>
|
||||
... or someone else...
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<GiftedDialog ref="giveDialogToThis" :to-project-id="projectId" />
|
||||
<GiftedDialog
|
||||
ref="giveDialogToThis"
|
||||
:to-project-id="projectId"
|
||||
:is-from-project-view="true"
|
||||
/>
|
||||
|
||||
<!-- Offers & Gifts to & from this -->
|
||||
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||
@@ -526,7 +484,12 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<GiftedDialog ref="giveDialogFromThis" :from-project-id="projectId" />
|
||||
<GiftedDialog
|
||||
ref="giveDialogFromThis"
|
||||
:from-project-id="projectId"
|
||||
:show-projects="true"
|
||||
:is-from-project-view="true"
|
||||
/>
|
||||
|
||||
<h3 class="text-lg font-bold mb-3 mt-4">
|
||||
Benefitted From This Project
|
||||
@@ -632,7 +595,7 @@ import TopMessage from "../components/TopMessage.vue";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
import ProjectIcon from "../components/ProjectIcon.vue";
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import {
|
||||
db,
|
||||
@@ -646,6 +609,7 @@ import { retrieveAccountDids } from "../libs/util";
|
||||
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
/**
|
||||
* Project View Component
|
||||
* @author Matthew Raymer
|
||||
@@ -842,6 +806,28 @@ export default class ProjectViewView extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
onCopyLinkClick() {
|
||||
const shortestProjectId = this.projectId.startsWith(
|
||||
serverUtil.ENDORSER_CH_HANDLE_PREFIX,
|
||||
)
|
||||
? this.projectId.substring(serverUtil.ENDORSER_CH_HANDLE_PREFIX.length)
|
||||
: this.projectId;
|
||||
const deepLink = `${APP_SERVER}/deep-link/project/${shortestProjectId}`;
|
||||
useClipboard()
|
||||
.copy(deepLink)
|
||||
.then(() => {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
title: "Copied",
|
||||
text: "A link to this project was copied to the clipboard.",
|
||||
},
|
||||
2000,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Isn't there a better way to make this available to the template?
|
||||
expandText() {
|
||||
this.expanded = true;
|
||||
@@ -1237,21 +1223,52 @@ export default class ProjectViewView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
openGiftDialogToProject(contact?: libsUtil.GiverReceiverInputInfo) {
|
||||
(this.$refs.giveDialogToThis as GiftedDialog).open(
|
||||
contact,
|
||||
undefined,
|
||||
undefined,
|
||||
(contact?.name || "Someone not named") + ` gave to this project`,
|
||||
);
|
||||
openGiftDialogToProject(
|
||||
contact?: libsUtil.GiverReceiverInputInfo | "Unnamed",
|
||||
) {
|
||||
if (contact === "Unnamed") {
|
||||
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
|
||||
(this.$refs.giveDialogToThis as GiftedDialog).open(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
"Given by Unnamed to this project",
|
||||
);
|
||||
// Immediately select "Unnamed" and move to Step 2
|
||||
(this.$refs.giveDialogToThis as GiftedDialog).selectGiver();
|
||||
} else {
|
||||
// Open straight to Step 2 with current user as giver and current project as recipient
|
||||
(this.$refs.giveDialogToThis as GiftedDialog).open(
|
||||
{
|
||||
did: this.activeDid,
|
||||
name: "You",
|
||||
},
|
||||
{
|
||||
did: this.issuer,
|
||||
name: this.name,
|
||||
handleId: this.projectId,
|
||||
image: this.imageUrl,
|
||||
},
|
||||
undefined,
|
||||
`Given to ${this.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
openGiftDialogFromProject() {
|
||||
// Set the project as giver and the current user as recipient
|
||||
(this.$refs.giveDialogFromThis as GiftedDialog).open(
|
||||
undefined,
|
||||
{
|
||||
did: undefined,
|
||||
name: this.name,
|
||||
handleId: this.projectId,
|
||||
image: this.imageUrl,
|
||||
},
|
||||
{ did: this.activeDid, name: "You" },
|
||||
undefined,
|
||||
`This project gave to you`,
|
||||
`${this.name} gave to you`,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1304,7 +1321,7 @@ export default class ProjectViewView extends Vue {
|
||||
}
|
||||
|
||||
// return an HTTPS URL if it's not a global URL
|
||||
addScheme(url: string) {
|
||||
ensureScheme(url: string) {
|
||||
if (!libsUtil.isGlobalUri(url)) {
|
||||
return "https://" + url;
|
||||
}
|
||||
@@ -1465,7 +1482,13 @@ export default class ProjectViewView extends Vue {
|
||||
}
|
||||
|
||||
openHiddenDidDialog() {
|
||||
const shortestProjectId = this.projectId.startsWith(
|
||||
serverUtil.ENDORSER_CH_HANDLE_PREFIX,
|
||||
)
|
||||
? this.projectId.substring(serverUtil.ENDORSER_CH_HANDLE_PREFIX.length)
|
||||
: this.projectId;
|
||||
(this.$refs.hiddenDidDialog as HiddenDidDialog).open(
|
||||
"project/" + shortestProjectId,
|
||||
"creator",
|
||||
this.issuerVisibleToDids,
|
||||
this.allContacts,
|
||||
|
||||
@@ -105,7 +105,7 @@ export default class ShareMyContactInfoView extends Vue {
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Copied",
|
||||
text: "Your contact info was copied to the clipboard. Have them paste it in the box on their 'Contacts' screen.",
|
||||
text: "Your contact info was copied to the clipboard. Have them click on it, or paste it in the box on their 'Contacts' screen.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
</button>
|
||||
Individual Profile
|
||||
</h1>
|
||||
<div class="text-sm text-center text-slate-500"></div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Animation -->
|
||||
@@ -32,6 +33,12 @@
|
||||
<div class="text-sm">
|
||||
<font-awesome icon="user" class="fa-fw text-slate-400"></font-awesome>
|
||||
{{ didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) }}
|
||||
<button title="Copy Link to Profile" @click="onCopyLinkClick()">
|
||||
<font-awesome
|
||||
icon="link"
|
||||
class="text-sm text-slate-500 ml-2 mb-1"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="profile.description" class="mt-4 text-slate-600">
|
||||
{{ profile.description }}
|
||||
@@ -100,6 +107,7 @@ import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import {
|
||||
APP_SERVER,
|
||||
DEFAULT_PARTNER_API_SERVER,
|
||||
NotificationIface,
|
||||
USE_DEXIE_DB,
|
||||
@@ -113,6 +121,7 @@ import { retrieveAccountDids } from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { Settings } from "@/db/tables/settings";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
@Component({
|
||||
components: {
|
||||
LMap,
|
||||
@@ -186,6 +195,10 @@ export default class UserProfileView extends Vue {
|
||||
if (response.status === 200) {
|
||||
const result = await response.json();
|
||||
this.profile = result.data;
|
||||
if (this.profile && this.profile.rowId !== profileId) {
|
||||
// currently the server returns "rowid" with lowercase "i"; remove when that's fixed
|
||||
this.profile.rowId = profileId;
|
||||
}
|
||||
} else {
|
||||
throw new Error("Failed to load profile");
|
||||
}
|
||||
@@ -204,5 +217,22 @@ export default class UserProfileView extends Vue {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onCopyLinkClick() {
|
||||
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
||||
useClipboard()
|
||||
.copy(deepLink)
|
||||
.then(() => {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
title: "Copied",
|
||||
text: "A link to this profile was copied to the clipboard.",
|
||||
},
|
||||
2000,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user