Compare commits

...

40 Commits

Author SHA1 Message Date
30e9b65ff0 update context with better focus for this project 2025-04-12 19:04:09 -06:00
f14e9dbb84 add file for context when working with LLMs 2025-04-12 19:03:02 -06:00
5cc42be58a fix some test scripts 2025-04-08 20:31:47 -06:00
3d1a2eeb8d adjust to app.timesafari.app in more places 2025-04-08 20:29:08 -06:00
7b0ee2e44e more ios folders to ignore (until we figure out the right way to dance with capacitor-assets) 2025-04-06 19:52:44 -06:00
ac018997e8 adjust instructions for capacitor-assets and more files 2025-04-06 19:48:45 -06:00
6f449e9c1f restore important file from previous cleanup 2025-04-06 19:15:00 -06:00
543599a6a1 remove icon files that are generated by capacitor-assets 2025-04-06 19:02:01 -06:00
634395ff38 fix instructions & app name 2025-04-05 17:23:29 -06:00
da1f08ebaa move the android secrets files in proximity to where they're used 2025-04-05 16:25:17 -06:00
4ee3ce0061 make changes that must have been done by XCode 2025-04-05 16:08:12 -06:00
654c67af72 add important ios files that aren't regenerated 2025-04-05 16:03:44 -06:00
b244f609b3 fix linting 2025-04-05 15:16:01 -06:00
9c84302c2e consolidate build & test instructions 2025-04-05 15:04:28 -06:00
ca37c30180 add instructions for the release build 2025-04-04 20:10:09 -06:00
130139e2af fix the build config to allow signing, either with a secrets file or with env vars 2025-04-04 20:01:23 -06:00
9802deb17c Merge pull request 'Adjustments to source-destination graphic' (#129) from homeview-card-design-2025-04 into master
Reviewed-on: #129
2025-04-03 21:50:30 -04:00
76c983ea3e replace with real designed icon 2025-04-03 17:55:59 -06:00
114ef440b8 add more build instructions for iOS 2025-04-03 17:52:03 -06:00
b58d510f24 make Advanced links explicit and use "project" instead of "idea" in project page 2025-04-03 17:50:49 -06:00
Matthew Raymer
da6a5ee83e fix(ui): resolve duplicate attributes and improve code style
- Remove duplicate class attributes in ProjectsView and ClaimView
- Fix attribute ordering for better readability
- Replace this references with direct variable names in templates
- Update icon-size prop to use kebab-case
- Remove unnecessary comments and improve formatting
- Fix import organization in ProjectsView

This commit resolves Vue template errors and improves code consistency.
2025-04-02 00:39:38 -07:00
Matthew Raymer
7af39d322f Merge branch 'ui-fixes-2025-03' 2025-04-02 06:48:07 +00:00
Matthew Raymer
bab802160f docs: add call graph and chain documentation to remaining methods
Add comprehensive JSDoc documentation to methods in HomeView.vue:

- latLongInAnySearchBox: Add call chain from shouldIncludeRecord
- giveDescription: Document template usage and displayAmount calls
- displayAmount: Add currency formatting chain
- currencyShortWordForCode: Document amount formatting flow
- openDialog: Document template and openGiftedPrompts usage
- openGiftedPrompts: Add dialog opening chain
- showNameThenIdDialog: Document template usage and prompt flow
- promptForShareMethod: Add sharing flow documentation

Each method now includes:
- @callGraph showing caller/callee relationships
- @chain showing complete execution paths
- @requires listing dependencies
- Enhanced parameter documentation

This completes the standardized documentation pattern across all methods,
making method relationships and dependencies explicit.
2025-04-01 12:30:46 +00:00
Matthew Raymer
01d7bc9e27 docs: enhance method documentation with standardized patterns
Add comprehensive JSDoc documentation to methods in HomeView.vue using standardized patterns:

- Add @callGraph sections to document method relationships and dependencies
- Add @chain sections to show complete call chains
- Add @requires sections to list state and parameter dependencies
- Add @modifies sections to document state changes
- Enhance parameter and return type documentation
- Standardize documentation format across all methods

Key methods enhanced:
- processRecord()
- extractClaim()
- extractGiverDid()
- getFulfillsPlan()
- shouldIncludeRecord()
- createFeedRecord()

This improves code maintainability by:
- Making method relationships explicit
- Documenting state dependencies
- Clarifying call chains
- Standardizing documentation format
2025-04-01 11:04:19 +00:00
Matthew Raymer
fa20360d87 docs: enhance component documentation with usage and reference tracking
- Add comprehensive JSDoc comments to HomeView and InfiniteScroll components
- Document method visibility (@public/@internal) and usage contexts
- Add clear references to where methods are called from (template, components, lifecycle)
- Include file-level documentation with component descriptions
- Document component dependencies and template usage
- Add parameter and return type documentation
- Clarify method call chains and dependencies
- Document event emissions and component interactions

This commit improves code maintainability by making method usage and
component relationships more explicit in the documentation.
2025-04-01 10:57:41 +00:00
Jose Olarte III
770c0fa77c Adjustments to source-destination graphic 2025-03-31 21:46:50 +08:00
Matthew Raymer
0709d0c726 fix: resolve strict mode violation in gift recording test
- Update selector to target specific gift text link instead of generic anchor
- Add filter to ensure unique element selection
- Fix test reliability by being explicit about element selection
2025-03-31 09:32:58 +00:00
Matthew Raymer
d943983bf8 refactor: improve feed loading and infinite scroll reliability
- Add debouncing and loading state to InfiniteScroll component to prevent duplicate entries
- Refactor updateAllFeed into smaller, focused functions for better maintainability
- Add proper error handling and type assertions
- Optimize test performance with networkidle waits and better selectors
- Fix strict mode violations in Playwright tests
- Clean up test configuration by commenting out unused browser targets
2025-03-31 09:17:15 +00:00
be9465e9f8 fix spelling & empty message, rename middle button to one word "yours" 2025-03-30 19:32:29 -06:00
5606f2a18a bump test timeout to 45 seconds, mostly for #33 with many records 2025-03-29 17:56:33 -06:00
Jose Olarte III
06e9950e53 Homeview activity card design tweaks 2025-03-27 00:02:43 +08:00
Jose Olarte III
5143c65337 Reinforce entity icon sizes 2025-03-25 20:05:17 +08:00
Jose Olarte III
09ee94d5a3 Linting 2025-03-25 19:49:24 +08:00
Jose Olarte III
5dbd66e51b Nav tweaks 2025-03-12 23:12:04 +08:00
Jose Olarte III
312b4aaaa3 Padding adjustments 2025-03-12 17:54:18 +08:00
Jose Olarte III
3a6a24d923 Contact list tweaks 2025-03-12 16:50:13 +08:00
Jose Olarte III
d7afb80a07 Pointer cursor 2025-03-12 15:51:39 +08:00
Jose Olarte III
751df09fe5 Button style tweaks + consistency 2025-03-12 15:51:15 +08:00
Jose Olarte III
8858495f73 Larger contact image
ClickUp task 86b3dgv2f
2025-03-10 19:55:57 +08:00
Jose Olarte III
ecb088bee2 Recolored confirm button to gray
ClickUp task 86b3y8f95
2025-03-10 19:08:49 +08:00
68 changed files with 2890 additions and 1230 deletions

16
.gitignore vendored
View File

@@ -38,30 +38,18 @@ pnpm-debug.log*
/dist-capacitor/ /dist-capacitor/
/test-playwright-results/ /test-playwright-results/
playwright-tests playwright-tests
test-playwright
dist-electron-packages dist-electron-packages
ios
.ruby-version .ruby-version
+.env +.env
# Generated test files # Generated test files
.generated/ .generated/
# Fastlane
ios/fastlane/report.xml
ios/fastlane/Preview.html
ios/fastlane/screenshots
ios/fastlane/test_output
android/fastlane/report.xml
android/fastlane/Preview.html
android/fastlane/screenshots
android/fastlane/test_output
.env.default .env.default
vendor/ vendor/
# Build logs # Build logs
build_logs/ build_logs/
# Android generated assets # PWA icon files generated by capacitor-assets
android/app/src/main/assets/public/assets/ icons

View File

@@ -9,8 +9,19 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
- Node.js (LTS version recommended) - Node.js (LTS version recommended)
- npm (comes with Node.js) - npm (comes with Node.js)
- Git - Git
- For iOS builds: macOS with Xcode installed
- For Android builds: Android Studio with SDK installed - For Android builds: Android Studio with SDK installed
- For iOS builds: macOS with Xcode and ruby gems & bundle
- pkgx +rubygems.org sh
- ... and you may have to fix these, especially with pkgx
```bash
gem_path=$(which gem)
shortened_path="${gem_path:h:h}"
export GEM_HOME=$shortened_path
export GEM_PATH=$shortened_path
```
- For desktop builds: Additional build tools based on your OS - For desktop builds: Additional build tools based on your OS
## Forks ## Forks
@@ -22,26 +33,23 @@ If you have forked this to make your own app, you'll want to customize the iOS &
npx cap add ios npx cap add ios
``` ```
You'll also want to edit the deep link configuration. You'll also want to edit the deep link configuration (see below).
## Initial Setup ## Initial Setup
1. Clone the repository: Install dependencies:
```bash
git clone [repository-url]
cd TimeSafari
```
2. Install dependencies:
```bash ```bash
npm install npm install
``` ```
## Web Build ## Web Dev Locally
To build for web deployment: ```bash
npm run dev
```
## Web Build for Server
1. Run the production build: 1. Run the production build:
@@ -49,17 +57,66 @@ To build for web deployment:
npm run build npm run build
``` ```
2. The built files will be in the `dist` directory. The built files will be in the `dist` directory.
3. To test the production build locally: 2. To test the production build locally:
You'll likely want to use test locations for the Endorser & image & partner servers; see "DEFAULT_ENDORSER_API_SERVER" & "DEFAULT_IMAGE_API_SERVER" & "DEFAULT_PARTNER_API_SERVER" below.
```bash ```bash
npm run serve npm run serve
``` ```
### Compile and minify for test & production
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
* `npx prettier --write ./sw_scripts/`
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
* 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 later).
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`.
* For test, build the app (because test server is not yet set up to build):
```bash
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
```
... and transfer to the test server:
```bash
rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari
```
(Let's replace that with a .env.development or .env.staging file.)
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
* For prod, get on the server and run the correct build:
... and log onto the server:
* `pkgx +npm sh`
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.55 && npm install && npm run build && cd -`
(The plain `npm run build` 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/`
* 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.
## Desktop Build (Electron) ## Desktop Build (Electron)
### Building for Linux ### Linux Build
1. Build the electron app in production mode: 1. Build the electron app in production mode:
@@ -130,7 +187,11 @@ Prerequisites: macOS with Xcode installed
3. Copy the assets: 3. Copy the assets:
```bash ```bash
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents.
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
echo '{"images":[]}' > ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
mkdir -p ios/App/App/Assets.xcassets/Splash.imageset
echo '{"images":[]}' > ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
npx capacitor-assets generate --ios npx capacitor-assets generate --ios
``` ```
@@ -142,6 +203,12 @@ Prerequisites: macOS with Xcode installed
4. Use Xcode to build and run on simulator or device. 4. Use Xcode to build and run on simulator or device.
#### First-time iOS Configuration
- Generate certificates inside XCode.
- Right-click on App and under Signing & Capabilities set the Team.
### Android Build ### Android Build
Prerequisites: Android Studio with SDK installed Prerequisites: Android Studio with SDK installed
@@ -174,7 +241,7 @@ Prerequisites: Android Studio with SDK installed
5. Use Android Studio to build and run on emulator or device. 5. Use Android Studio to build and run on emulator or device.
## Building Android from the console ## Android Build from the console
```bash ```bash
cd android cd android
@@ -187,11 +254,18 @@ Prerequisites: Android Studio with SDK installed
... or, to create the `aab` file, `bundle` instead of `build`: ... or, to create the `aab` file, `bundle` instead of `build`:
```bash ```bash
./gradlew bundle -Dlint.baselines.continue=true ./gradlew bundleDebug -Dlint.baselines.continue=true
```
... or, to create a signed release, add the app/gradle.properties.secrets file (see properties at top of app/build.gradle) and the app/time-safari-upload-key-pkcs12.jks file, then `bundleRelease`:
```bash
./gradlew bundleRelease -Dlint.baselines.continue=true
``` ```
## Configuring Android for deep links
## First-time Android Configuration for deep links
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file: You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:

96
README-context.md Normal file
View File

@@ -0,0 +1,96 @@
# Time Safari Context
## Project Overview
Time Safari is an application designed to foster community building through gifts, gratitude, and collaborative projects. The app should make it extremely easy and intuitive for users of any age and capability to recognize contributions, build trust networks, and organize collective action. It is built on services that preserve privacy and data sovereignty.
The ultimate goals of Time Safari are two-fold:
1. **Connect** Make it easy, rewarding, and non-threatening for people to connect with others who have similar interests, and to initiate activities together. This helps people accomplish and learn from other individuals in less-structured environments; moreover, it helps them discover who they want to continue to support and with whom they want to maintain relationships.
2. **Reveal** Widely advertise the great support and rewards that are being given and accepted freely, especially non-monetary ones. Using visuals and text, display the kind of impact that gifts are making in the lives of others. Also show useful and engaging reports of project statistics and personal accomplishments.
## Core Approaches
Time Safari should help everyday users build meaningful connections and organize collective efforts by:
1. **Recognizing Contributions**: Creating permanent, verifiable records of gifts and contributions people give to each other and their communities.
2. **Facilitating Collaboration**: Making it ridiculously easy for people to ask for or propose help on projects and interests that matter to them.
3. **Building Trust Networks**: Enabling users to maintain their network and activity visibility. Developing reputation through verified contributions and references, which can be selectively shown to others outside the network.
4. **Preserving Privacy**: Ensuring personal identifiers are only shared with explicitly authorized contacts, allowing private individuals including children to participate safely.
5. **Engaging Content**: Displaying people's records in compelling stories, and highlighting those projects that are lifting people's lives long-term, both in physical support and in emotional-spiritual-creative thriving.
## Technical Foundation
This application is built on a privacy-preserving claims architecture (via endorser.ch) with these key characteristics:
- **Decentralized Identifiers (DIDs)**: User identities are based on public/private key pairs stored on their devices
- **Cryptographic Verification**: All claims and confirmations are cryptographically signed
- **User-Controlled Visibility**: Users explicitly control who can see their identifiers and data
- **Merkle-Chained Claims**: Claims are cryptographically chained for verification and integrity
- **Native and Web App**: Works on iOS, Android, and web browsers
## User Journey
The typical progression of usage follows these stages:
1. **Gratitude & Recognition**: Users begin by expressing and recording gratitude for gifts received, building a foundation of acknowledgment.
2. **Project Proposals**: Users propose projects and ideas, reaching out to connect with others who share similar interests.
3. **Action Triggers**: Offers of help serve as triggers and motivations to execute proposed projects, moving from ideas to action.
## Context for LLM Development
When developing new functionality for Time Safari, consider these design principles:
1. **Accessibility First**: Features should be usable by non-technical users with minimal learning curve.
2. **Privacy by Design**: All features must respect user privacy and data sovereignty.
3. **Progressive Enhancement**: Core functionality should work across all devices, with richer experiences where supported.
4. **Voluntary Collaboration**: The system should enable but never coerce participation.
5. **Trust Building**: Features should help build verifiable trust between users.
6. **Network Effects**: Consider how features scale as more users join the platform.
7. **Low Resource Requirements**: The system should be lightweight enough to run on inexpensive devices users already own.
## Use Cases to Support
LLM development should focus on enhancing these key use cases:
1. **Community Building**: Tools that help people find others with shared interests and values.
2. **Project Coordination**: Features that make it easy to propose collaborative projects and to submit suggestions and offers to existing ones.
3. **Reputation Building**: Methods for users to showcase their contributions and reliability, in contexts where they explicitly reveal that information.
4. **Governance Experimentation**: Features that facilitate decision-making and collective governance.
## Constraints
When developing new features, be mindful of these constraints:
1. **Privacy Preservation**: User identifiers must remain private except when explicitly shared.
2. **Platform Limitations**: Features must work within the constraints of the target app platforms, while aiming to leverage the best platform technology available.
3. **Endorser API Limitations**: Backend features are constrained by the endorser.ch API capabilities.
4. **Performance on Low-End Devices**: The application should remain performant on older/simpler devices.
5. **Offline-First When Possible**: Key functionality should work offline when feasible.

View File

@@ -19,59 +19,6 @@ npm run dev
See [BUILDING.md](BUILDING.md) for more details. See [BUILDING.md](BUILDING.md) for more details.
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use http://localhost:3000 for local endorser.ch
### Run all UI tests
Look at [BUILDING.md](BUILDING.md) for the "test-all" instructions and [TESTING.md](test-playwright/TESTING.md) for more details.
### Compile and minify for test & production
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
* `npx prettier --write ./sw_scripts/`
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
* 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 later).
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`.
* For test, build the app (because test server is not yet set up to build):
```bash
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
```
... and transfer to the test server:
```bash
rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari
```
(Let's replace that with a .env.development or .env.staging file.)
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
* For prod, get on the server and run the correct build:
... and log onto the server:
* `pkgx +npm sh`
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.55 && npm install && npm run build && cd -`
(The plain `npm run build` 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/`
* 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.

8
android/.gitignore vendored
View File

@@ -1,5 +1,8 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore # Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
app/gradle.properties.secrets
app/time-safari-upload-key-pkcs12.jks
# Built application files # Built application files
*.apk *.apk
*.aar *.aar
@@ -99,3 +102,8 @@ app/src/main/assets/public
app/src/main/assets/capacitor.config.json app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml app/src/main/res/xml/config.xml
# Generated Icons from capacitor-assets
app/src/main/res/drawable/*.png
app/src/main/res/drawable-*/*.png
app/src/main/res/mipmap-*/*.png

View File

@@ -1,14 +1,38 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
// These are sample values to set in gradle.properties.secrets
// MY_KEYSTORE_FILE=time-safari-upload-key-pkcs12.jks
// MY_KEYSTORE_PASSWORD=...
// MY_KEY_ALIAS=time-safari-key-alias
// MY_KEY_PASSWORD=...
// Try to load from environment variables first
project.ext.MY_KEYSTORE_FILE = System.getenv('ANDROID_KEYSTORE_FILE') ?: ""
project.ext.MY_KEYSTORE_PASSWORD = System.getenv('ANDROID_KEYSTORE_PASSWORD') ?: ""
project.ext.MY_KEY_ALIAS = System.getenv('ANDROID_KEY_ALIAS') ?: ""
project.ext.MY_KEY_PASSWORD = System.getenv('ANDROID_KEY_PASSWORD') ?: ""
// If no environment variables, try to load from secrets file
if (!project.ext.MY_KEYSTORE_FILE) {
def secretsPropertiesFile = rootProject.file("gradle.properties.secrets")
if (secretsPropertiesFile.exists()) {
Properties secretsProperties = new Properties()
secretsProperties.load(new FileInputStream(secretsPropertiesFile))
secretsProperties.each { name, value ->
project.ext[name] = value
}
}
}
android { android {
namespace 'app.timesafari' namespace 'app.timesafari'
compileSdk rootProject.ext.compileSdkVersion compileSdk rootProject.ext.compileSdkVersion
defaultConfig { defaultConfig {
applicationId "app.timesafari" applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 9
versionName "1.0" versionName "0.4.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -16,10 +40,41 @@ android {
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
} }
} }
signingConfigs {
release {
if (project.ext.MY_KEYSTORE_FILE &&
project.ext.MY_KEYSTORE_PASSWORD &&
project.ext.MY_KEY_ALIAS &&
project.ext.MY_KEY_PASSWORD) {
storeFile file(project.ext.MY_KEYSTORE_FILE)
storePassword project.ext.MY_KEYSTORE_PASSWORD
keyAlias project.ext.MY_KEY_ALIAS
keyPassword project.ext.MY_KEY_PASSWORD
}
}
}
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// Only sign if we have the signing config
if (signingConfigs.release.storeFile != null) {
signingConfig signingConfigs.release
}
}
}
// Enable bundle builds (without which it doesn't work right for bundleDebug vs bundleRelease)
bundle {
language {
enableSplit = true
}
density {
enableSplit = true
}
abi {
enableSplit = true
} }
} }
} }

View File

@@ -21,6 +21,6 @@ public class ExampleInstrumentedTest {
// Context of the app under test. // Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("app.timesafari", appContext.getPackageName()); assertEquals("app.timesafari.app", appContext.getPackageName());
} }
} }

View File

@@ -1,5 +1,5 @@
{ {
"appId": "app.timesafari", "appId": "app.timesafari.app",
"appName": "TimeSafari", "appName": "TimeSafari",
"webDir": "dist", "webDir": "dist",
"bundledWebRuntime": false, "bundledWebRuntime": false,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
</background>
<foreground>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</foreground>
</adaptive-icon> </adaptive-icon>

View File

@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
</background>
<foreground>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</foreground>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

27
ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
App/build
App/Pods
App/output
App/App/public
DerivedData
xcuserdata
*.xcuserstate
# Cordova plugins for Capacitor
capacitor-cordova-ios-plugins
# Generated Config files
App/App/capacitor.config.json
App/App/config.xml
# User-specific Xcode files
App/App.xcodeproj/xcuserdata/*.xcuserdatad/
App/App.xcodeproj/*.xcuserstate
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
# Generated Icons from capacitor-assets (also Contents.json which is confusing; see BUILDING.md)
App/App/Assets.xcassets/AppIcon.appiconset
App/App/Assets.xcassets/Splash.imageset

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Time Safari.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,49 @@
import UIKit
import Capacitor
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Called when the app was launched with an activity, including Universal Links.
// Feel free to add additional processing here, but if you want the App API to support
// tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</imageView>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="Splash" width="1366" height="1366"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
</dependencies>
<scenes>
<!--Bridge View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/App/App/Info.plist Normal file
View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>TimeSafari</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
</dict>
</plist>

24
ios/App/Podfile Normal file
View File

@@ -0,0 +1,24 @@
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '13.0'
use_frameworks!
# workaround to avoid Xcode caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
end
target 'App' do
capacitor_pods
# Add your Pods here
end
post_install do |installer|
assertDeploymentTarget(installer)
end

28
ios/App/Podfile.lock Normal file
View File

@@ -0,0 +1,28 @@
PODS:
- Capacitor (6.2.0):
- CapacitorCordova
- CapacitorApp (6.0.2):
- Capacitor
- CapacitorCordova (6.2.0)
DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
EXTERNAL SOURCES:
Capacitor:
:path: "../../node_modules/@capacitor/ios"
CapacitorApp:
:path: "../../node_modules/@capacitor/app"
CapacitorCordova:
:path: "../../node_modules/@capacitor/ios"
SPEC CHECKSUMS:
Capacitor: 05d35014f4425b0740fc8776481f6a369ad071bf
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
CapacitorCordova: b33e7f4aa4ed105dd43283acdd940964374a87d9
PODFILE CHECKSUM: 4233f5c5f414604460ff96d372542c311b0fb7a8
COCOAPODS: 1.16.2

View File

@@ -0,0 +1,414 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 48;
objects = {
/* Begin PBXBuildFile section */
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* Time Safari.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Time Safari.app"; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
504EC3011FED79650016851F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
isa = PBXGroup;
children = (
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
504EC2FB1FED79650016851F = {
isa = PBXGroup;
children = (
504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
);
sourceTree = "<group>";
};
504EC3051FED79650016851F /* Products */ = {
isa = PBXGroup;
children = (
504EC3041FED79650016851F /* Time Safari.app */,
);
name = Products;
sourceTree = "<group>";
};
504EC3061FED79650016851F /* App */ = {
isa = PBXGroup;
children = (
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
504EC30E1FED79650016851F /* Assets.xcassets */,
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
504EC3131FED79650016851F /* Info.plist */,
2FAD9762203C412B000D30F8 /* config.xml */,
50B271D01FEDC1A000F3C39B /* public */,
);
path = App;
sourceTree = "<group>";
};
7F8756D8B27F46E3366F6CEA /* Pods */ = {
isa = PBXGroup;
children = (
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
504EC3031FED79650016851F /* Time Safari */ = {
isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Time Safari" */;
buildPhases = (
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "Time Safari";
productName = App;
productReference = 504EC3041FED79650016851F /* Time Safari.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0920;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "Time Safari" */;
compatibilityVersion = "Xcode 8.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 504EC2FB1FED79650016851F;
packageReferences = (
);
productRefGroup = 504EC3051FED79650016851F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
504EC3031FED79650016851F /* Time Safari */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
504EC3021FED79650016851F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
50B271D11FEDC1A000F3C39B /* public in Resources */,
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
504EC3001FED79650016851F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
504EC30B1FED79650016851F /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC30C1FED79650016851F /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC3111FED79650016851F /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
504EC3141FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
504EC3151FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
504EC3171FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GM3FS5JQPH;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Time Safari";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
504EC3181FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GM3FS5JQPH;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Time Safari";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "Time Safari" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3141FED79650016851F /* Debug */,
504EC3151FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Time Safari" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3171FED79650016851F /* Debug */,
504EC3181FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;
}

1801
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "0.4.4", "version": "0.4.4",
"description": "TimeSafari Desktop Application", "description": "Time Safari Application",
"author": { "author": {
"name": "TimeSafari Team" "name": "Time Safari Team"
}, },
"scripts": { "scripts": {
"dev": "vite --config vite.config.dev.mts", "dev": "vite --config vite.config.dev.mts",
@@ -155,7 +155,7 @@
}, },
"main": "./dist-electron/main.js", "main": "./dist-electron/main.js",
"build": { "build": {
"appId": "app.timesafari", "appId": "app.timesafari.app",
"productName": "TimeSafari", "productName": "TimeSafari",
"directories": { "directories": {
"output": "dist-electron-packages" "output": "dist-electron-packages"

View File

@@ -46,21 +46,21 @@ export default defineConfig({
/* Configure projects for major browsers */ /* Configure projects for major browsers */
projects: [ projects: [
{ // {
name: 'chromium-serial', // name: 'chromium-serial',
testMatch: /.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts/, // testMatch: /.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts/,
use: { // use: {
...devices['Desktop Chrome'], // ...devices['Desktop Chrome'],
permissions: ["clipboard-read"], // permissions: ["clipboard-read"],
}, // },
workers: 1, // Force serial execution for problematic tests // workers: 1, // Force serial execution for problematic tests
}, // },
{ // {
name: 'firefox-serial', // name: 'firefox-serial',
testMatch: /.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts/, // testMatch: /.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts/,
use: { ...devices['Desktop Firefox'] }, // use: { ...devices['Desktop Firefox'] },
workers: 1, // workers: 1,
}, // },
{ {
name: 'chromium', name: 'chromium',
testMatch: /^(?!.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts).+\.spec\.ts$/, testMatch: /^(?!.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts).+\.spec\.ts$/,
@@ -76,32 +76,26 @@ export default defineConfig({
}, },
/* Test against mobile viewports. */ /* Test against mobile viewports. */
// {
{ // name: "Mobile Chrome",
name: "Mobile Chrome", // use: { ...devices["Pixel 5"] },
use: { ...devices["Pixel 5"] }, // },
}, // {
{ // name: "Mobile Safari",
name: "Mobile Safari", // use: { ...devices["iPhone 12"] },
use: { ...devices["iPhone 12"] }, // },
},
/* Test against branded browsers. */ /* Test against branded browsers. */
// { // {
// name: 'Microsoft Edge', // name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' }, // use: { ...devices['Desktop Edge'], channel: 'msedge' },
// }, // },
{
name: "Google Chrome",
use: { ...devices["Desktop Chrome"], channel: "chrome" },
},
], ],
/* Configure global timeout; default is 30000 milliseconds */ /* Configure global timeout; default is 30000 milliseconds */
// the image upload will often not succeed in 5 seconds // the image upload will often not succeed in 5 seconds
// 33-record-gift-x10.spec.ts:90:5 > Record 9 new gifts will often not succeed in 30 seconds // 33-record-gift-x10.spec.ts:90:5 > Record 9 new gifts will often not succeed in 30 seconds
timeout: 35000, // various tests fail at various times with 25000 timeout: 45000, // various tests fail at various times with 25000
/* Run your local dev server before starting the tests */ /* Run your local dev server before starting the tests */
/** /**

View File

@@ -51,7 +51,7 @@ const { existsSync } = require('fs');
*/ */
function checkCommand(command, errorMessage) { function checkCommand(command, errorMessage) {
try { try {
execSync(command + ' --version', { stdio: 'ignore' }); execSync(command, { stdio: 'ignore' });
return true; return true;
} catch (e) { } catch (e) {
console.error(`${errorMessage}`); console.error(`${errorMessage}`);
@@ -164,10 +164,10 @@ function main() {
// Check required command line tools // Check required command line tools
// These are essential for building and testing the application // These are essential for building and testing the application
success &= checkCommand('node', 'Node.js is required'); success &= checkCommand('node --version', 'Node.js is required');
success &= checkCommand('npm', 'npm is required'); success &= checkCommand('npm --version', 'npm is required');
success &= checkCommand('gradle', 'Gradle is required for Android builds'); success &= checkCommand('gradle --version', 'Gradle is required for Android builds');
success &= checkCommand('xcodebuild', 'Xcode is required for iOS builds'); success &= checkCommand('xcodebuild --help', 'Xcode is required for iOS builds');
// Check platform-specific development environments // Check platform-specific development environments
success &= checkAndroidSetup(); success &= checkAndroidSetup();

View File

@@ -170,7 +170,7 @@ const executeDeeplink = async (url, description, log) => {
try { try {
// Stop the app before executing the deep link // Stop the app before executing the deep link
execSync('adb shell am force-stop app.timesafari'); execSync('adb shell am force-stop app.timesafari.app');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`); execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);

View File

@@ -10,24 +10,26 @@
</span> </span>
</div> </div>
<div class="bg-slate-100 rounded-t-md border border-slate-300 p-3 sm:p-4"> <div
<div class="flex items-center gap-2 mb-6"> class="flex items-center justify-between gap-2 text-lg bg-slate-200 border border-slate-300 border-b-0 rounded-t-md px-3 sm:px-4 py-1 sm:py-2"
>
<div class="flex items-center gap-2">
<div v-if="record.issuerDid"> <div v-if="record.issuerDid">
<EntityIcon <EntityIcon
:entity-id="record.issuerDid" :entity-id="record.issuerDid"
class="rounded size-[3rem] sm:size-[4rem] object-cover" class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/> />
</div> </div>
<div v-else> <div v-else>
<font-awesome icon="person-circle-question" class="text-slate-300 text-[3rem] sm:text-[4rem]" /> <font-awesome
icon="person-circle-question"
class="text-slate-300 text-[2rem]"
/>
</div> </div>
<div> <div>
<h3 class="font-semibold"> <h3 class="font-semibold">
{{ {{ record.issuer.known ? record.issuer.displayName : "" }}
record.issuer.known ? record.issuer.displayName : ""
}}
</h3> </h3>
<p class="ms-auto text-xs text-slate-500 italic"> <p class="ms-auto text-xs text-slate-500 italic">
{{ friendlyDate }} {{ friendlyDate }}
@@ -35,10 +37,16 @@
</div> </div>
</div> </div>
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
</a>
</div>
<div class="bg-slate-100 rounded-b-md border border-slate-300 p-3 sm:p-4">
<!-- Record Image --> <!-- Record Image -->
<div <div
v-if="record.image" v-if="record.image"
class="bg-cover mb-6 -mx-3 sm:-mx-4" class="bg-cover mb-6 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
:style="`background-image: url(${record.image});`" :style="`background-image: url(${record.image});`"
> >
<a <a
@@ -54,10 +62,12 @@
</a> </a>
</div> </div>
<div class="relative flex justify-between gap-4 max-w-lg mx-auto mb-5"> <div
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mb-5"
>
<!-- Source --> <!-- Source -->
<div <div
class="w-28 sm:w-40 text-center bg-white border border-slate-200 rounded p-2 sm:p-3" class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
> >
<div class="relative w-fit mx-auto"> <div class="relative w-fit mx-auto">
<div> <div>
@@ -74,7 +84,7 @@
<EntityIcon <EntityIcon
:entity-id="record.agentDid" :entity-id="record.agentDid"
:profile-image-url="record.issuer.profileImageUrl" :profile-image-url="record.issuer.profileImageUrl"
class="rounded size-[3rem] sm:size-[4rem]" class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
/> />
</div> </div>
<!-- Unknown Person --> <!-- Unknown Person -->
@@ -86,8 +96,10 @@
</div> </div>
</div> </div>
</div> </div>
<div class="text-xs mt-2 line-clamp-3 sm:line-clamp-2"> <div
<div v-if="record.providerPlanName || record.giver.known"> v-if="record.providerPlanName || record.giver.known"
class="text-xs mt-2 truncate"
>
<font-awesome <font-awesome
:icon="record.providerPlanName ? 'users' : 'user'" :icon="record.providerPlanName ? 'users' : 'user'"
class="fa-fw text-slate-400" class="fa-fw text-slate-400"
@@ -95,13 +107,12 @@
{{ record.providerPlanName || record.giver.displayName }} {{ record.providerPlanName || record.giver.displayName }}
</div> </div>
</div> </div>
</div>
<!-- Arrow --> <!-- Arrow -->
<div <div
class="absolute inset-x-28 sm:inset-x-40 mx-2 top-1/2 -translate-y-1/2" class="absolute inset-x-[8rem] sm:inset-x-[12rem] mx-2 top-1/2 -translate-y-1/2"
> >
<div class="text-sm text-center leading-none font-semibold"> <div class="text-sm text-center leading-none font-semibold pe-[15px]">
{{ fetchAmount }} {{ fetchAmount }}
</div> </div>
@@ -118,7 +129,7 @@
<!-- Destination --> <!-- Destination -->
<div <div
class="w-28 sm:w-40 text-center bg-white border border-slate-200 rounded p-2 sm:p-3" class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
> >
<div class="relative w-fit mx-auto"> <div class="relative w-fit mx-auto">
<div> <div>
@@ -135,7 +146,7 @@
<EntityIcon <EntityIcon
:entity-id="record.recipientDid" :entity-id="record.recipientDid"
:profile-image-url="record.receiver.profileImageUrl" :profile-image-url="record.receiver.profileImageUrl"
class="rounded size-[3rem] sm:size-[4rem]" class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
/> />
</div> </div>
<!-- Unknown Person --> <!-- Unknown Person -->
@@ -147,8 +158,10 @@
</div> </div>
</div> </div>
</div> </div>
<div class="text-xs mt-2 line-clamp-3 sm:line-clamp-2"> <div
<div v-if="record.recipientProjectName || record.receiver.known"> v-if="record.recipientProjectName || record.receiver.known"
class="text-xs mt-2 truncate"
>
<font-awesome <font-awesome
:icon="record.recipientProjectName ? 'users' : 'user'" :icon="record.recipientProjectName ? 'users' : 'user'"
class="fa-fw text-slate-400" class="fa-fw text-slate-400"
@@ -157,7 +170,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Description --> <!-- Description -->
<p class="font-medium"> <p class="font-medium">
@@ -166,14 +178,6 @@
</a> </a>
</p> </p>
</div> </div>
<div
class="flex items-center gap-2 text-lg bg-slate-300 rounded-b-md px-3 sm:px-4 py-1 sm:py-2"
>
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
</a>
</div>
</li> </li>
</template> </template>
@@ -213,10 +217,6 @@ export default class ActivityListItem extends Vue {
const claim = const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim; (this.record.fullClaim as unknown).claim || this.record.fullClaim;
if (!claim.description) {
return "something not described";
}
return `${claim.description}`; return `${claim.description}`;
} }

View File

@@ -1,3 +1,9 @@
/** * @file InfiniteScroll.vue * @description A Vue component that implements
infinite scrolling functionality using the Intersection Observer API. * This
component emits a 'reached-bottom' event when the user scrolls near the bottom
of the content. * It includes debouncing to prevent multiple rapid triggers and
loading state management. * * @author Matthew Raymer * @version 1.0.0 */
<template> <template>
<div ref="scrollContainer"> <div ref="scrollContainer">
<slot /> <slot />
@@ -8,13 +14,51 @@
<script lang="ts"> <script lang="ts">
import { Component, Emit, Prop, Vue } from "vue-facing-decorator"; import { Component, Emit, Prop, Vue } from "vue-facing-decorator";
/**
* InfiniteScroll Component
*
* This component implements infinite scrolling functionality by observing when a user
* scrolls near the bottom of the content. It uses the Intersection Observer API for
* efficient scroll detection and includes debouncing to prevent multiple rapid triggers.
*
* Usage in template:
* ```vue
* <InfiniteScroll @reached-bottom="loadMore">
* <div>Content goes here</div>
* </InfiniteScroll>
* ```
*
* Props:
* - distance: number (default: 200) - Distance in pixels from the bottom at which to trigger the event
*
* Events:
* - reached-bottom: Emitted when the user scrolls near the bottom of the content
*/
@Component @Component
export default class InfiniteScroll extends Vue { export default class InfiniteScroll extends Vue {
/** Distance in pixels from the bottom at which to trigger the reached-bottom event */
@Prop({ default: 200 }) @Prop({ default: 200 })
readonly distance!: number; readonly distance!: number;
/** Intersection Observer instance for detecting scroll position */
private observer!: IntersectionObserver; private observer!: IntersectionObserver;
/** Flag to track initial render state */
private isInitialRender = true; private isInitialRender = true;
/** Flag to prevent multiple simultaneous loading states */
private isLoading = false;
/** Timeout ID for debouncing scroll events */
private debounceTimeout: number | null = null;
/**
* Vue lifecycle hook that runs after component updates.
* Initializes the Intersection Observer if not already set up.
*
* @internal
* Used internally by Vue's lifecycle system
*/
updated() { updated() {
if (!this.observer) { if (!this.observer) {
const options = { const options = {
@@ -30,18 +74,50 @@ export default class InfiniteScroll extends Vue {
} }
} }
// 'beforeUnmount' hook runs before unmounting the component /**
* Vue lifecycle hook that runs before component unmounting.
* Cleans up the Intersection Observer and any pending timeouts.
*
* @internal
* Used internally by Vue's lifecycle system
*/
beforeUnmount() { beforeUnmount() {
if (this.observer) { if (this.observer) {
this.observer.disconnect(); this.observer.disconnect();
} }
if (this.debounceTimeout) {
window.clearTimeout(this.debounceTimeout);
}
} }
/**
* Handles intersection observer callbacks when the sentinel element becomes visible.
* Implements debouncing to prevent multiple rapid triggers and manages loading state.
*
* @param entries - Array of IntersectionObserverEntry objects
* @returns false (required by @Emit decorator)
*
* @internal
* Used internally by the Intersection Observer
* @emits reached-bottom - Emitted when the user scrolls near the bottom
*/
@Emit("reached-bottom") @Emit("reached-bottom")
handleIntersection(entries: IntersectionObserverEntry[]) { handleIntersection(entries: IntersectionObserverEntry[]) {
const entry = entries[0]; const entry = entries[0];
if (entry.isIntersecting) { if (entry.isIntersecting && !this.isLoading) {
return true; // Debounce the intersection event
if (this.debounceTimeout) {
window.clearTimeout(this.debounceTimeout);
}
this.debounceTimeout = window.setTimeout(() => {
this.isLoading = true;
this.$emit("reached-bottom", true);
// Reset loading state after a short delay
setTimeout(() => {
this.isLoading = false;
}, 1000);
}, 300);
} }
return false; return false;
} }

View File

@@ -95,7 +95,7 @@
</p> </p>
<p class="mt-4"> <p class="mt-4">
Search for a topic, or search around your neighborhod under "Nearby". Search for a topic, or search around your neighborhood under "Nearby".
</p> </p>
<p class="mt-4"> <p class="mt-4">

View File

@@ -1,7 +1,7 @@
<template> <template>
<!-- QUICK NAV --> <!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50"> <nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2 max-w-3xl mx-auto"> <ul class="flex text-2xl px-6 py-2 gap-1 max-w-3xl mx-auto">
<!-- Home Feed --> <!-- Home Feed -->
<li <li
:class="{ :class="{
@@ -52,7 +52,7 @@
> >
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<font-awesome icon="hand" class="fa-fw" /> <font-awesome icon="hand" class="fa-fw" />
<span class="text-xs mt-1">your work</span> <span class="text-xs mt-1">yours</span>
</div> </div>
</router-link> </router-link>
</li> </li>

View File

@@ -259,7 +259,7 @@
<span class="mb-2 font-bold">Location for Searches</span> <span class="mb-2 font-bold">Location for Searches</span>
<router-link <router-link
:to="{ name: 'search-area' }" :to="{ name: 'search-area' }"
class="text-m 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-md mb-2" class="text-m 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-4 py-2 rounded-md mb-2"
> >
{{ isSearchAreasSet ? "Change" : "Set" }} Search Area {{ isSearchAreasSet ? "Change" : "Set" }} Search Area
</router-link> </router-link>
@@ -471,7 +471,7 @@
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
<h3 <h3
id="advanced" id="advanced"
class="text-sm uppercase font-semibold mb-3" class="text-blue-500 text-sm font-semibold mb-3"
@click="showAdvanced = !showAdvanced" @click="showAdvanced = !showAdvanced"
> >
Advanced Advanced

View File

@@ -107,7 +107,7 @@
<button <button
v-if="!showGiveNumbers" v-if="!showGiveNumbers"
href="" href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md" class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
:style=" :style="
contactsSelected.length > 0 contactsSelected.length > 0
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);' ? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
@@ -130,7 +130,7 @@
<div class="w-full text-right"> <div class="w-full text-right">
<button <button
href="" 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-1 py-1 rounded-md" 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()" @click="toggleShowContactAmounts()"
> >
{{ {{
@@ -180,14 +180,7 @@
data-testId="contactListItem" data-testId="contactListItem"
> >
<div class="grow overflow-hidden"> <div class="grow overflow-hidden">
<div class="flex items-center"> <div class="flex items-center gap-3">
<EntityIcon
:contact="contact"
:icon-size="24"
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
@click="showLargeIdenticon = contact"
/>
<input <input
v-if="!showGiveNumbers" v-if="!showGiveNumbers"
type="checkbox" type="checkbox"
@@ -204,14 +197,19 @@
" "
/> />
<h2 <EntityIcon
class="text-base font-semibold ml-2 w-1/3 truncate flex-shrink-0" :contact="contact"
> :icon-size="48"
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact"
/>
<h2 class="text-base font-semibold w-1/3 truncate flex-shrink-0">
{{ contactNameNonBreakingSpace(contact.name) }} {{ contactNameNonBreakingSpace(contact.name) }}
</h2> </h2>
<span> <span>
<div class="flex items-center"> <div class="flex gap-2 items-center">
<router-link <router-link
:to="{ :to="{
path: '/did/' + encodeURIComponent(contact.did), path: '/did/' + encodeURIComponent(contact.did),
@@ -220,23 +218,22 @@
> >
<font-awesome <font-awesome
icon="circle-info" icon="circle-info"
class="text-xl text-blue-500 ml-4" class="text-xl text-blue-500"
/> />
</router-link> </router-link>
<span class="ml-4 text-sm overflow-hidden">{{ <span class="text-sm overflow-hidden">{{
libsUtil.shortDid(contact.did) libsUtil.shortDid(contact.did)
}}</span> }}</span>
</div> </div>
<div class="ml-4 text-sm"> <div class="text-sm">
{{ contact.notes }} {{ contact.notes }}
</div> </div>
</span> </span>
</div> </div>
<div id="ContactActions" class="flex gap-1.5 mt-2">
<div <div
v-if="showGiveNumbers && contact.did != activeDid" v-if="showGiveNumbers && contact.did != activeDid"
class="ml-auto flex gap-1.5" class="ml-auto flex gap-1.5 mt-2"
> >
<button <button
class="text-sm 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-1.5 rounded-l-md" class="text-sm 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-1.5 rounded-l-md"
@@ -292,11 +289,10 @@
class="text-sm 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-2 py-1.5 rounded-md border border-slate-400" class="text-sm 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-2 py-1.5 rounded-md border border-slate-400"
title="See more given activity" title="See more given activity"
> >
<font-awesome icon="file-lines" class="fa-fw" /> <fa icon="file-lines" class="fa-fw" />
</router-link> </router-link>
</div> </div>
</div> </div>
</div>
</li> </li>
</ul> </ul>
<p v-else>There are no contacts.</p> <p v-else>There are no contacts.</p>
@@ -317,7 +313,7 @@
<button <button
v-if="!showGiveNumbers" v-if="!showGiveNumbers"
href="" 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 ml-2 px-1 py-1 rounded-md" 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 ml-3 px-3 py-1.5 rounded-md"
:style=" :style="
contactsSelected.length > 0 contactsSelected.length > 0
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);' ? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'

View File

@@ -1,9 +1,15 @@
/** * @file HomeView.vue * @description Main view component for the
application's home page. Handles user identity, feed management, * and
interaction with various dialogs and components. Implements infinite scrolling
for activity feed * and manages user registration status. * * @author Matthew
Raymer * @version 1.0.0 */
<template> <template>
<QuickNav selected="Home" /> <QuickNav selected="Home" />
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto"> <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"> <h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
{{ AppString.APP_NAME }} {{ AppString.APP_NAME }}
</h1> </h1>
@@ -344,15 +350,31 @@ import * as serverUtil from "../libs/endorserServer";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { GiveRecordWithContactInfo } from "types"; import { GiveRecordWithContactInfo } from "types";
/** /**
* HomeView - Main view component for the application's home page * HomeView Component
* *
* Workflow: * Main view component that handles:
* 1. On mount, initializes user identity, settings, and data * 1. User identity and registration management
* 2. Handles user registration status * 2. Activity feed with infinite scrolling
* 3. Manages feed of activities and offers * 3. Contact management and display
* 4. Provides interface for creating and viewing claims * 4. Gift/claim creation and viewing
* 5. Feed filtering and settings
*
* Template Usage:
* ```vue
* <HomeView>
* <!-- Content is managed internally -->
* </HomeView>
* ```
*
* Component Dependencies:
* - QuickNav: Navigation component
* - TopMessage: Message display component
* - OnboardingDialog: User onboarding flow
* - GiftedDialog: Gift creation interface
* - FeedFilters: Feed filtering options
* - InfiniteScroll: Infinite scrolling functionality
* - ActivityListItem: Individual activity display
*/ */
@Component({ @Component({
components: { components: {
@@ -418,6 +440,9 @@ export default class HomeView extends Vue {
* 5. Load feed data * 5. Load feed data
* 6. Load new offers * 6. Load new offers
* 7. Check onboarding status * 7. Check onboarding status
*
* @internal
* Called automatically by Vue lifecycle system
*/ */
async mounted() { async mounted() {
try { try {
@@ -437,6 +462,11 @@ export default class HomeView extends Vue {
* Initializes user identity * Initializes user identity
* - Retrieves existing DIDs * - Retrieves existing DIDs
* - Creates new DID if none exists * - Creates new DID if none exists
* - Loads user settings and contacts
* - Checks registration status
*
* @internal
* Called by mounted()
* @throws Logs error if DID retrieval fails * @throws Logs error if DID retrieval fails
*/ */
private async initializeIdentity() { private async initializeIdentity() {
@@ -526,7 +556,7 @@ export default class HomeView extends Vue {
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text:
err.userMessage || (err as { userMessage?: string })?.userMessage ||
"There was an error retrieving your settings or the latest activity.", "There was an error retrieving your settings or the latest activity.",
}, },
5000, 5000,
@@ -542,6 +572,9 @@ export default class HomeView extends Vue {
* - Feed filters and view settings * - Feed filters and view settings
* - Registration status * - Registration status
* - Notification acknowledgments * - Notification acknowledgments
*
* @internal
* Called by mounted() and reloadFeedOnChange()
*/ */
private async loadSettings() { private async loadSettings() {
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
@@ -563,6 +596,9 @@ export default class HomeView extends Vue {
/** /**
* Loads user contacts from database * Loads user contacts from database
* Used for displaying contact info in feed and actions * Used for displaying contact info in feed and actions
*
* @internal
* Called by mounted() and initializeIdentity()
*/ */
private async loadContacts() { private async loadContacts() {
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
@@ -573,6 +609,9 @@ export default class HomeView extends Vue {
* - Checks if unregistered user can access API * - Checks if unregistered user can access API
* - Updates registration status if successful * - Updates registration status if successful
* - Preserves unregistered state on failure * - Preserves unregistered state on failure
*
* @internal
* Called by mounted() and initializeIdentity()
*/ */
private async checkRegistrationStatus() { private async checkRegistrationStatus() {
if (!this.isRegistered && this.activeDid) { if (!this.isRegistered && this.activeDid) {
@@ -599,6 +638,9 @@ export default class HomeView extends Vue {
/** /**
* Initializes feed data * Initializes feed data
* Triggers updateAllFeed() to populate activity feed * Triggers updateAllFeed() to populate activity feed
*
* @internal
* Called by mounted()
*/ */
private async loadFeedData() { private async loadFeedData() {
await this.updateAllFeed(); await this.updateAllFeed();
@@ -610,6 +652,9 @@ export default class HomeView extends Vue {
* - Number of new direct offers * - Number of new direct offers
* - Number of new project offers * - Number of new project offers
* - Rate limit status for both * - Rate limit status for both
*
* @internal
* Called by mounted() and initializeIdentity()
* @requires Active DID * @requires Active DID
*/ */
private async loadNewOffers() { private async loadNewOffers() {
@@ -637,6 +682,9 @@ export default class HomeView extends Vue {
/** /**
* Checks if user needs onboarding * Checks if user needs onboarding
* Opens onboarding dialog if not completed * Opens onboarding dialog if not completed
*
* @internal
* Called by mounted()
*/ */
private async checkOnboarding() { private async checkOnboarding() {
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
@@ -649,6 +697,9 @@ export default class HomeView extends Vue {
* Handles errors during initialization * Handles errors during initialization
* - Logs error to console and database * - Logs error to console and database
* - Displays user notification * - Displays user notification
*
* @internal
* Called by mounted() and handleFeedError()
* @param err Error object with optional userMessage * @param err Error object with optional userMessage
*/ */
private handleError(err: unknown) { private handleError(err: unknown) {
@@ -659,7 +710,7 @@ export default class HomeView extends Vue {
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text:
err.userMessage || (err as { userMessage?: string })?.userMessage ||
"There was an error retrieving your settings or the latest activity.", "There was an error retrieving your settings or the latest activity.",
}, },
5000, 5000,
@@ -668,6 +719,9 @@ export default class HomeView extends Vue {
/** /**
* Checks if feed results are being filtered * Checks if feed results are being filtered
*
* @public
* Used in template for filter button display
* @returns true if visible or nearby filters are active * @returns true if visible or nearby filters are active
*/ */
resultsAreFiltered() { resultsAreFiltered() {
@@ -676,6 +730,9 @@ export default class HomeView extends Vue {
/** /**
* Checks if browser notifications are supported * Checks if browser notifications are supported
*
* @public
* Used in template for notification feature detection
* @returns true if Notification API is available * @returns true if Notification API is available
*/ */
notificationsSupported() { notificationsSupported() {
@@ -687,6 +744,9 @@ export default class HomeView extends Vue {
* - Updates filter states * - Updates filter states
* - Clears existing feed data * - Clears existing feed data
* - Triggers new feed load * - Triggers new feed load
*
* @public
* Called by FeedFilters component when filters change
*/ */
async reloadFeedOnChange() { async reloadFeedOnChange() {
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
@@ -701,6 +761,9 @@ export default class HomeView extends Vue {
/** /**
* Loads more feed items for infinite scroll * Loads more feed items for infinite scroll
*
* @public
* Called by InfiniteScroll component when bottom is reached
* @param payload Boolean indicating if more items should be loaded * @param payload Boolean indicating if more items should be loaded
*/ */
async loadMoreGives(payload: boolean) { async loadMoreGives(payload: boolean) {
@@ -712,6 +775,24 @@ export default class HomeView extends Vue {
} }
} }
/**
* Checks if coordinates fall within any search box
*
* @internal
* @callGraph
* Called by: shouldIncludeRecord()
* Calls: None
*
* @chain
* shouldIncludeRecord() -> latLongInAnySearchBox()
*
* @requires
* - this.searchBoxes
*
* @param lat Latitude to check
* @param long Longitude to check
* @returns true if coordinates are within any search box
*/
latLongInAnySearchBox(lat: number, long: number) { latLongInAnySearchBox(lat: number, long: number) {
for (const boxInfo of this.searchBoxes) { for (const boxInfo of this.searchBoxes) {
if ( if (
@@ -727,78 +808,351 @@ export default class HomeView extends Vue {
/** /**
* Updates feed with latest activity * Updates feed with latest activity
* - Handles filtering of results *
* - Updates last viewed claim ID * @internal
* - Manages loading state * @callGraph
* Called by:
* - loadMoreGives()
* - initializeIdentity()
* Calls:
* - retrieveGives()
* - processFeedResults()
* - updateFeedLastViewedId()
* - handleFeedError()
*
* @chain
* loadMoreGives() -> updateAllFeed()
* initializeIdentity() -> updateAllFeed()
*
* @requires
* - this.apiServer
* - this.activeDid
* - this.feedPreviousOldestId
*
* @modifies
* - this.isFeedLoading
* - this.feedData (via processFeedResults)
* - this.feedLastViewedClaimId (via updateFeedLastViewedId)
*/ */
async updateAllFeed() { async updateAllFeed() {
this.isFeedLoading = true; this.isFeedLoading = true;
let endOfResults = true; let endOfResults = true;
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
.then(async (results) => { try {
const results = await this.retrieveGives(
this.apiServer,
this.feedPreviousOldestId,
);
if (results.data.length > 0) { if (results.data.length > 0) {
endOfResults = false; endOfResults = false;
// include the descriptions of the giver and receiver await this.processFeedResults(results.data);
for (const record of results.data as GiveSummaryRecord[]) { await this.updateFeedLastViewedId(results.data);
// similar code is in endorser-mobile utility.ts }
// claim.claim happen for some claims wrapped in a Verifiable Credential } catch (e) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any this.handleFeedError(e);
const claim = (record.fullClaim as any).claim || record.fullClaim; }
// agent.did is for legacy data, before March 2023
const giverDid =
claim.agent?.identifier || (claim.agent as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
// recipient.did is for legacy data, before March 2023
const recipientDid =
claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
// This has indeed proven problematic. See loadMoreGives if (this.feedData.length === 0 && !endOfResults) {
// We should display it immediately and then get the plan later. await this.updateAllFeed();
const fulfillsPlan = await getPlanFromCache( }
this.isFeedLoading = false;
}
/**
* Processes feed results and adds them to feedData
*
* @internal
* @callGraph
* Called by: updateAllFeed()
* Calls: processRecord()
*
* @chain
* updateAllFeed() -> processFeedResults()
*
* @requires
* - this.feedData
* - this.feedPreviousOldestId
*
* @modifies
* - this.feedData
* - this.feedPreviousOldestId
*
* @param records Array of feed records to process
*/
private async processFeedResults(records: GiveSummaryRecord[]) {
for (const record of records) {
const processedRecord = await this.processRecord(record);
if (processedRecord) {
this.feedData.push(processedRecord);
}
}
this.feedPreviousOldestId = records[records.length - 1].jwtId;
}
/**
* Processes a single record and returns it if it passes filters
*
* @internal
* @callGraph
* Called by: processFeedResults()
* Calls:
* - extractClaim()
* - extractGiverDid()
* - extractRecipientDid()
* - getFulfillsPlan()
* - shouldIncludeRecord()
* - extractProvider()
* - getProvidedByPlan()
* - createFeedRecord()
*
* @chain
* updateAllFeed() -> processFeedResults() -> processRecord()
*
* @requires
* - this.isAnyFeedFilterOn
* - this.isFeedFilteredByVisible
* - this.isFeedFilteredByNearby
* - this.activeDid
* - this.allContacts
*
* @modifies
* - this.feedData (via createFeedRecord)
*
* @param record The record to process
* @returns Processed record with contact info if it passes filters, null otherwise
*/
private async processRecord(
record: GiveSummaryRecord,
): Promise<GiveRecordWithContactInfo | null> {
const claim = this.extractClaim(record);
const giverDid = this.extractGiverDid(claim);
const recipientDid = this.extractRecipientDid(claim);
const fulfillsPlan = await this.getFulfillsPlan(record);
if (!this.shouldIncludeRecord(record, fulfillsPlan)) {
return null;
}
const provider = this.extractProvider(claim);
const providedByPlan = await this.getProvidedByPlan(provider);
return this.createFeedRecord(
record,
claim,
giverDid,
recipientDid,
provider,
fulfillsPlan,
providedByPlan,
);
}
/**
* Extracts claim from record, handling both direct and wrapped claims
*
* @internal
* @callGraph
* Called by: processRecord()
* Calls: None
*
* @chain
* processRecord() -> extractClaim()
*
* @requires
* - record.fullClaim
*
* @param record The record containing the claim
* @returns The extracted claim object
*/
private extractClaim(record: GiveSummaryRecord) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (record.fullClaim as any).claim || record.fullClaim;
}
/**
* Extracts giver DID from claim
*
* @internal
* @callGraph
* Called by: processRecord()
* Calls: None
*
* @chain
* processRecord() -> extractGiverDid()
*
* @requires
* - claim.agent
*
* @param claim The claim object containing giver information
* @returns The giver's DID
*/
private extractGiverDid(claim: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return claim.agent?.identifier || (claim.agent as any)?.did;
}
/**
* Extracts recipient DID from claim
*
* @internal
* Called by processRecord()
*/
private extractRecipientDid(claim: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return claim.recipient?.identifier || (claim.recipient as any)?.did;
}
/**
* Gets fulfills plan from cache
*
* @internal
* @callGraph
* Called by: processRecord()
* Calls: getPlanFromCache()
*
* @chain
* processRecord() -> getFulfillsPlan()
*
* @requires
* - this.axios
* - this.apiServer
* - this.activeDid
*
* @param record The record containing the plan handle ID
* @returns The fulfills plan object
*/
private async getFulfillsPlan(record: GiveSummaryRecord) {
return await getPlanFromCache(
record.fulfillsPlanHandleId, record.fulfillsPlanHandleId,
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid, this.activeDid,
); );
}
/**
* Checks if record should be included based on filters
*
* @internal
* @callGraph
* Called by: processRecord()
* Calls:
* - containsNonHiddenDid()
* - latLongInAnySearchBox()
*
* @chain
* processRecord() -> shouldIncludeRecord()
*
* @requires
* - this.isAnyFeedFilterOn
* - this.isFeedFilteredByVisible
* - this.isFeedFilteredByNearby
* - this.searchBoxes
*
* @param record The record to check
* @param fulfillsPlan The fulfills plan object
* @returns true if record should be included based on filters
*/
private shouldIncludeRecord(
record: GiveSummaryRecord,
fulfillsPlan: any,
): boolean {
if (!this.isAnyFeedFilterOn) {
return true;
}
// check if the record should be filtered out
let anyMatch = false; let anyMatch = false;
if (this.isFeedFilteredByVisible && containsNonHiddenDid(record)) { if (this.isFeedFilteredByVisible && containsNonHiddenDid(record)) {
// has a visible DID so it's a keeper
anyMatch = true; anyMatch = true;
} }
if (!anyMatch && this.isFeedFilteredByNearby) {
// check if the associated project has a location inside user's search box
if (record.fulfillsPlanHandleId) {
if (fulfillsPlan?.locLat && fulfillsPlan?.locLon) {
if ( if (
!anyMatch &&
this.isFeedFilteredByNearby &&
record.fulfillsPlanHandleId
) {
if (fulfillsPlan?.locLat && fulfillsPlan?.locLon) {
anyMatch =
this.latLongInAnySearchBox( this.latLongInAnySearchBox(
fulfillsPlan.locLat, fulfillsPlan.locLat,
fulfillsPlan.locLon, fulfillsPlan.locLon,
) ) ?? false;
) {
anyMatch = true;
} }
} }
}
}
if (this.isAnyFeedFilterOn && !anyMatch) {
continue;
}
// checking for arrays due to legacy data return anyMatch;
const provider = Array.isArray(claim.provider) }
? claim.provider[0]
: claim.provider; /**
const providedByPlan = await getPlanFromCache( * Extracts provider from claim
*
* @internal
* Called by processRecord()
*/
private extractProvider(claim: any) {
return Array.isArray(claim.provider) ? claim.provider[0] : claim.provider;
}
/**
* Gets provided by plan from cache
*
* @internal
* Called by processRecord()
*/
private async getProvidedByPlan(provider: any) {
return await getPlanFromCache(
provider?.identifier as string, provider?.identifier as string,
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid, this.activeDid,
); );
}
const newRecord: GiveRecordWithContactInfo = { /**
* Creates a feed record with contact info
*
* @internal
* @callGraph
* Called by: processRecord()
* Calls:
* - didInfoForContact()
* - contactForDid()
*
* @chain
* processRecord() -> createFeedRecord()
*
* @requires
* - this.activeDid
* - this.allContacts
* - this.allMyDids
*
* @param record The base record
* @param claim The claim object
* @param giverDid The giver's DID
* @param recipientDid The recipient's DID
* @param provider The provider object
* @param fulfillsPlan The fulfills plan object
* @param providedByPlan The provided by plan object
* @returns A feed record with contact information
*/
private createFeedRecord(
record: GiveSummaryRecord,
claim: any,
giverDid: string,
recipientDid: string,
provider: any,
fulfillsPlan: any,
providedByPlan: any,
): GiveRecordWithContactInfo {
return {
...record, ...record,
jwtId: record.jwtId, jwtId: record.jwtId,
fullClaim: record.fullClaim,
description: record.description || "",
handleId: record.handleId,
issuerDid: record.issuerDid,
fulfillsPlanHandleId: record.fulfillsPlanHandleId,
giver: didInfoForContact( giver: didInfoForContact(
giverDid, giverDid,
this.activeDid, this.activeDid,
@@ -821,24 +1175,34 @@ export default class HomeView extends Vue {
contactForDid(recipientDid, this.allContacts), contactForDid(recipientDid, this.allContacts),
this.allMyDids, this.allMyDids,
), ),
}; } as GiveRecordWithContactInfo;
this.feedData.push(newRecord);
} }
this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId; /**
// The following update is only done on the first load. * Updates the last viewed claim ID in settings
*
* @internal
* Called by updateAllFeed()
*/
private async updateFeedLastViewedId(records: GiveSummaryRecord[]) {
if ( if (
this.feedLastViewedClaimId == null || this.feedLastViewedClaimId == null ||
this.feedLastViewedClaimId < results.data[0].jwtId this.feedLastViewedClaimId < records[0].jwtId
) { ) {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: results.data[0].jwtId, lastViewedClaimId: records[0].jwtId,
}); });
} }
} }
})
.catch((e) => { /**
* Handles feed error and shows notification
*
* @internal
* Called by updateAllFeed()
*/
private handleFeedError(e: any) {
logger.error("Error with feed load:", e); logger.error("Error with feed load:", e);
this.$notify( this.$notify(
{ {
@@ -849,19 +1213,16 @@ export default class HomeView extends Vue {
}, },
-1, -1,
); );
});
if (this.feedData.length === 0 && !endOfResults) {
// repeat until there's at least some data
await this.updateAllFeed();
}
this.isFeedLoading = false;
} }
/** /**
* Retrieve claims in reverse chronological order * Retrieve claims in reverse chronological order
* *
* @param beforeId the earliest ID (of previous searches) to search earlier * @internal
* @return claims in reverse chronological order * Called by updateAllFeed()
* @param endorserApiServer API server URL
* @param beforeId OptioCalled by updateAllFeed()nal ID to fetch earlier results
* @returns claims in reverse chronological order
*/ */
async retrieveGives(endorserApiServer: string, beforeId?: string) { async retrieveGives(endorserApiServer: string, beforeId?: string) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId; const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
@@ -894,6 +1255,27 @@ export default class HomeView extends Vue {
} }
} }
/**
* Formats gift description with giver and recipient info
*
* @public
* @callGraph
* Called by: Template
* Calls: displayAmount()
*
* @chain
* Template -> giveDescription() -> displayAmount() -> currencyShortWordForCode()
*
* @requires
* - giveRecord.fullClaim
* - giveRecord.giver
* - giveRecord.receiver
* - giveRecord.recipientProjectName
* - giveRecord.providerPlanName
*
* @param giveRecord Record containing gift information
* @returns formatted description string
*/
giveDescription(giveRecord: GiveRecordWithContactInfo) { giveDescription(giveRecord: GiveRecordWithContactInfo) {
// similar code is in endorser-mobile utility.ts // similar code is in endorser-mobile utility.ts
// claim.claim happen for some claims wrapped in a Verifiable Credential // claim.claim happen for some claims wrapped in a Verifiable Credential
@@ -974,10 +1356,23 @@ export default class HomeView extends Vue {
} }
} }
/**
* Navigates to activity page
*
* @public
* Called by template click handler
*/
goToActivityToUserPage() { goToActivityToUserPage() {
this.$router.push({ name: "new-activity" }); this.$router.push({ name: "new-activity" });
} }
/**
* Navigates to claim details page
*
* @public
* Called by ActivityListItem component
* @param jwtId ID of the claim to view
*/
onClickLoadClaim(jwtId: string) { onClickLoadClaim(jwtId: string) {
const route = { const route = {
path: "/claim/" + encodeURIComponent(jwtId), path: "/claim/" + encodeURIComponent(jwtId),
@@ -985,16 +1380,70 @@ export default class HomeView extends Vue {
this.$router.push(route); this.$router.push(route);
} }
/**
* Formats amount with currency code
*
* @internal
* @callGraph
* Called by: giveDescription()
* Calls: currencyShortWordForCode()
*
* @chain
* giveDescription() -> displayAmount() -> currencyShortWordForCode()
*
* @requires
* - code: string (currency code)
* - amt: number (amount to format)
*
* @param code Currency code
* @param amt Amount to format
* @returns formatted amount string
*/
displayAmount(code: string, amt: number) { displayAmount(code: string, amt: number) {
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1); return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
} }
/**
* Gets currency word based on code and plurality
*
* @internal
* @callGraph
* Called by: displayAmount()
* Calls: None
*
* @chain
* giveDescription() -> displayAmount() -> currencyShortWordForCode()
*
* @requires
* - unitCode: string (currency code)
* - single: boolean (whether to use singular form)
*
* @param unitCode Currency code
* @param single Whether to use singular form
* @returns formatted currency word
*/
currencyShortWordForCode(unitCode: string, single: boolean) { currencyShortWordForCode(unitCode: string, single: boolean) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
} }
/** /**
* Opens dialog for creating new gift/claim * Opens dialog for creating new gift/claim
*
* @public
* @callGraph
* Called by:
* - Template
* - openGiftedPrompts()
* Calls: GiftedDialog.open()
*
* @chain
* Template -> openDialog()
* openGiftedPrompts() -> openDialog()
*
* @requires
* - this.$refs.customDialog
* - this.activeDid
*
* @param giver Optional contact info for giver * @param giver Optional contact info for giver
* @param description Optional gift description * @param description Optional gift description
*/ */
@@ -1013,7 +1462,19 @@ export default class HomeView extends Vue {
/** /**
* Opens prompts for gift ideas * Opens prompts for gift ideas
* Links to openDialog for selected prompt *
* @public
* @callGraph
* Called by: Template
* Calls: openDialog()
*
* @chain
* Template -> openGiftedPrompts() -> openDialog()
*
* @requires
* - this.$refs.giftedPrompts
*
* @param callback Function to handle selected gift info
*/ */
openGiftedPrompts() { openGiftedPrompts() {
(this.$refs.giftedPrompts as GiftedPrompts).open((giver, description) => (this.$refs.giftedPrompts as GiftedPrompts).open((giver, description) =>
@@ -1023,12 +1484,21 @@ export default class HomeView extends Vue {
/** /**
* Opens feed filter configuration * Opens feed filter configuration
* @param reloadFeedOnChange Callback for when filters are updated *
* @public
* Called by template click handler
*/ */
openFeedFilters() { openFeedFilters() {
(this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange); (this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
} }
/**
* Shows toast notification to user
*
* @internal
* Used for various user notifications
* @param message Message to display
*/
toastUser(message: string) { toastUser(message: string) {
this.$notify( this.$notify(
{ {
@@ -1041,10 +1511,35 @@ export default class HomeView extends Vue {
); );
} }
/**
* Computes CSS classes for known person icons
*
* @public
* Used in template for icon styling
* @param known Whether the person is known
* @returns CSS class string
*/
computeKnownPersonIconStyleClassNames(known: boolean) { computeKnownPersonIconStyleClassNames(known: boolean) {
return known ? "text-slate-500" : "text-slate-100"; return known ? "text-slate-500" : "text-slate-100";
} }
/**
* Shows name input dialog if needed
*
* @public
* @callGraph
* Called by: Template
* Calls:
* - UserNameDialog.open()
* - promptForShareMethod()
*
* @chain
* Template -> showNameThenIdDialog() -> promptForShareMethod()
*
* @requires
* - this.$refs.userNameDialog
* - this.givenName
*/
showNameThenIdDialog() { showNameThenIdDialog() {
if (!this.givenName) { if (!this.givenName) {
(this.$refs.userNameDialog as UserNameDialog).open(() => { (this.$refs.userNameDialog as UserNameDialog).open(() => {
@@ -1055,6 +1550,21 @@ export default class HomeView extends Vue {
} }
} }
/**
* Shows dialog for sharing method selection
*
* @internal
* @callGraph
* Called by: showNameThenIdDialog()
* Calls: ChoiceButtonDialog.open()
*
* @chain
* Template -> showNameThenIdDialog() -> promptForShareMethod()
*
* @requires
* - this.$refs.choiceButtonDialog
* - this.$router
*/
promptForShareMethod() { promptForShareMethod() {
(this.$refs.choiceButtonDialog as ChoiceButtonDialog).open({ (this.$refs.choiceButtonDialog as ChoiceButtonDialog).open({
title: "How can you share your info?", title: "How can you share your info?",
@@ -1074,6 +1584,14 @@ export default class HomeView extends Vue {
}); });
} }
/**
* Caches image data for sharing
*
* @public
* Called by ActivityListItem component
* @param event Event object
* @param imageUrl URL of image to cache
*/
async cacheImageData(event: Event, imageUrl: string) { async cacheImageData(event: Event, imageUrl: string) {
try { try {
// For images that might fail CORS, just store the URL // For images that might fail CORS, just store the URL
@@ -1084,12 +1602,26 @@ export default class HomeView extends Vue {
} }
} }
/**
* Opens image viewer dialog
*
* @public
* Called by ActivityListItem component
* @param imageUrl URL of image to display
*/
async openImageViewer(imageUrl: string) { async openImageViewer(imageUrl: string) {
this.selectedImageData = this.imageCache.get(imageUrl) ?? null; this.selectedImageData = this.imageCache.get(imageUrl) ?? null;
this.selectedImage = imageUrl; this.selectedImage = imageUrl;
this.isImageViewerOpen = true; this.isImageViewerOpen = true;
} }
/**
* Handles claim confirmation
*
* @public
* Called by ActivityListItem component
* @param record Record to confirm
*/
async confirmClaim(record: GiveRecordWithContactInfo) { async confirmClaim(record: GiveRecordWithContactInfo) {
this.$notify( this.$notify(
{ {

View File

@@ -27,7 +27,7 @@
/> />
<h3 <h3
class="text-sm uppercase font-semibold mb-3" class="text-blue-500 text-sm font-semibold mb-3"
@click="showAdvanced = !showAdvanced" @click="showAdvanced = !showAdvanced"
> >
Advanced Advanced

View File

@@ -266,7 +266,7 @@
<div class="text-center"> <div class="text-center">
<button <button
data-testId="offerButton" data-testId="offerButton"
class="block w-full 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 rounded-md" class="block w-full 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 text-sm leading-tight rounded-md"
@click="openOfferDialog()" @click="openOfferDialog()"
> >
Offer to this (maybe with conditions)... Offer to this (maybe with conditions)...
@@ -353,7 +353,7 @@
<div v-if="activeDid && isRegistered"> <div v-if="activeDid && isRegistered">
<div class="text-center"> <div class="text-center">
<button <button
class="block w-full 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-1rounded-md" class="block w-full 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 text-sm leading-tight rounded-md"
@click="openGiftDialogToProject()" @click="openGiftDialogToProject()"
> >
Given To This... Given To This...
@@ -361,7 +361,7 @@
</div> </div>
</div> </div>
<h3 class="text-lg font-bold mt-4">Given To This Idea</h3> <h3 class="text-lg font-bold mt-4">Given To This Project</h3>
<div v-if="givesToThis.length === 0" class="text-sm"> <div v-if="givesToThis.length === 0" class="text-sm">
(None yet. If you've seen something, say something by clicking a (None yet. If you've seen something, say something by clicking a
@@ -511,7 +511,7 @@
<div v-if="activeDid && isRegistered"> <div v-if="activeDid && isRegistered">
<div class="text-center"> <div class="text-center">
<button <button
class="block w-full 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 rounded-md" class="block w-full 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 text-sm leading-tight rounded-md"
@click="openGiftDialogFromProject()" @click="openGiftDialogFromProject()"
> >
Given By This... Given By This...

View File

@@ -239,7 +239,7 @@
class="border-b border-slate-300" class="border-b border-slate-300"
> >
<a <a
class="block py-4 flex gap-4" class="block py-4 flex gap-4 cursor-pointer"
@click="onClickLoadProject(project.handleId)" @click="onClickLoadProject(project.handleId)"
> >
<div class="flex-none"> <div class="flex-none">
@@ -279,13 +279,8 @@ import ProjectIcon from "../components/ProjectIcon.vue";
import TopMessage from "../components/TopMessage.vue"; import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue"; import UserNameDialog from "../components/UserNameDialog.vue";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { import { didInfo, getHeaders, getPlanFromCache } from "../libs/endorserServer";
didInfo, import { OfferSummaryRecord, PlanData } from "../interfaces/records";
getHeaders,
getPlanFromCache,
OfferSummaryRecord,
PlanData,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { OnboardPage } from "../libs/util"; import { OnboardPage } from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";

View File

@@ -109,7 +109,11 @@ test('Record something given', async ({ page }) => {
// Refresh home view and check gift // Refresh home view and check gift
await page.goto('./'); await page.goto('./');
await page.locator('li').filter({ hasText: finalTitle }).locator('a').click(); await page.locator('li')
.filter({ hasText: finalTitle })
.locator('a.cursor-pointer')
.filter({ hasText: finalTitle })
.click();
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible(); await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
const page1Promise = page.waitForEvent('popup'); const page1Promise = page.waitForEvent('popup');

View File

@@ -88,12 +88,8 @@ import { test, expect } from '@playwright/test';
import { importUser, createUniqueStringsArray, createRandomNumbersArray } from './testUtils'; import { importUser, createUniqueStringsArray, createRandomNumbersArray } from './testUtils';
test('Record 9 new gifts', async ({ page }) => { test('Record 9 new gifts', async ({ page }) => {
const giftCount = 9; // because 10 has taken us above 30 seconds const giftCount = 9;
// Standard text
const standardTitle = 'Gift '; const standardTitle = 'Gift ';
// Field value arrays
const finalTitles = []; const finalTitles = [];
const finalNumbers = []; const finalNumbers = [];
@@ -101,21 +97,19 @@ test('Record 9 new gifts', async ({ page }) => {
const uniqueStrings = await createUniqueStringsArray(giftCount); const uniqueStrings = await createUniqueStringsArray(giftCount);
const randomNumbers = await createRandomNumbersArray(giftCount); const randomNumbers = await createRandomNumbersArray(giftCount);
// Populate array with titles // Populate arrays
for (let i = 0; i < giftCount; i++) { for (let i = 0; i < giftCount; i++) {
let loopTitle = standardTitle + uniqueStrings[i]; finalTitles.push(standardTitle + uniqueStrings[i]);
finalTitles.push(loopTitle); finalNumbers.push(randomNumbers[i]);
let loopNumber = randomNumbers[i];
finalNumbers.push(loopNumber);
} }
// Import user 00 // Import user 00
await importUser(page, '00'); await importUser(page, '00');
// Record new gifts // Record new gifts with optimized waiting
for (let i = 0; i < giftCount; i++) { for (let i = 0; i < giftCount; i++) {
// Record something given // Record gift
await page.goto('./'); await page.goto('./', { waitUntil: 'networkidle' });
if (i === 0) { if (i === 0) {
await page.getByTestId('closeOnboardingAndFinish').click(); await page.getByTestId('closeOnboardingAndFinish').click();
} }
@@ -123,11 +117,16 @@ test('Record 9 new gifts', async ({ page }) => {
await page.getByPlaceholder('What was given').fill(finalTitles[i]); await page.getByPlaceholder('What was given').fill(finalTitles[i]);
await page.getByRole('spinbutton').fill(finalNumbers[i].toString()); await page.getByRole('spinbutton').fill(finalNumbers[i].toString());
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That gift was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
// Refresh home view and check gift // Wait for success and dismiss
await page.goto('./'); await expect(page.getByText('That gift was recorded.')).toBeVisible();
await expect(page.locator('li').filter({ hasText: finalTitles[i] })).toBeVisible(); await page.locator('div[role="alert"] button > svg.fa-xmark').click();
// Verify gift in list with network idle wait
await page.goto('./', { waitUntil: 'networkidle' });
await expect(page.locator('ul#listLatestActivity li')
.filter({ hasText: finalTitles[i] })
.first())
.toBeVisible({ timeout: 10000 });
} }
}); });

View File

@@ -0,0 +1,4 @@
{
"status": "failed",
"failedTests": []
}

View File

@@ -0,0 +1,29 @@
{
"status": "failed",
"failedTests": [
"a29eb57667e0fb28c7e9-7a80e551e7f16a766d0d",
"a29eb57667e0fb28c7e9-1a8c76601bb6ea4f735c",
"a29eb57667e0fb28c7e9-0a3670fa77fcd5ac9827",
"a29eb57667e0fb28c7e9-90c8866cf70c7f96647d",
"a29eb57667e0fb28c7e9-4abc584edcf7a6a12389",
"a29eb57667e0fb28c7e9-3b443656a23fd8e7eb76",
"a29eb57667e0fb28c7e9-1f63cf7a41b756ffe01f",
"a29eb57667e0fb28c7e9-4eb03633761e58eac0a4",
"db48a48c514e3e2940e5-cef25040a0b285eed2ba",
"1c818805c9b0ac973736-726f18ba6163d57238c8",
"c52ae54d86eda05904f3-adf7525a07e75f4e3cc2",
"2fac21b9c9c3eb062631-9d2d2e9a199603c11b9b",
"64242279fe0133650483-20fbacc4e45c5561df6c",
"a7ff64a290be94f9d82c-e26ceb13031dafad1133",
"868977083268005e6ec0-c27d226d34e20ba4863d",
"5e149db5da4a5e319bcc-3298c84d0ebfff5e6d7c",
"5e149db5da4a5e319bcc-1981ba81641b6000f80b",
"2b5f6d3352de2040032d-bf5ed3a9483d90c396dd",
"2b5f6d3352de2040032d-6f52c3699c55c19ccad8",
"2b5f6d3352de2040032d-0f478a3208f64651daa1",
"2b5f6d3352de2040032d-a05f542cad739ee3b5b9",
"955bdfdfe05b442c0f5d-a9ec2b8bc7bd90ea0439",
"955bdfdfe05b442c0f5d-2c38171f673436923a8b",
"1a1fd29d3f0573e705e6-a3a6805908fe9a29ab11"
]
}