Compare commits

..

152 Commits

Author SHA1 Message Date
Matthew Raymer
6b38b1a347 test: increase timeout for record offer test to 60s
The record offer test occasionally hits the 45s timeout limit.
Increasing to 60s provides more headroom, aligning with actual
test durations seen in Firefox (up to 43.1s for similar operations).
2025-04-21 06:14:34 +00:00
ca455e9593 modify files to make the ios build & distribution work 2025-04-18 20:40:41 -06:00
5ada70b05e fix the reference to the secrets file 2025-04-15 21:24:03 -06:00
Matthew Raymer
4f9b146a66 fix: improve file sharing on Android using app-private storage
- Replace direct file writing with app-private storage + share dialog
- Add Share plugin for cross-platform file sharing
- Update file paths configuration for Android
- Fix permission issues by using Directory.Data instead of Documents
- Simplify file export flow in DataExportSection
2025-04-11 07:13:07 +00:00
Matthew Raymer
2b638ce2a7 chore: add an android build script to simplify creation of versions 2025-04-10 10:55:40 +00:00
Matthew Raymer
0b528af2a6 WIP: Fix Android file writing permissions and path handling
- Refactor writeFile method to properly handle Android Storage Access Framework (SAF) URIs
- Update path construction for Android to use Download directory with correct permissions
- Enhance error handling and logging throughout file operations
- Add detailed logging for debugging file system operations
- Fix permission checking logic to handle "File does not exist" case correctly
- Improve error messages and stack traces in logs
- Add timestamp to all log entries for better debugging
- Use proper directory types (ExternalStorage for Android, Documents for iOS)
- Add UTF-8 encoding specification for file writes

This is a work in progress as we're still seeing permission issues with Android file writing.
2025-04-09 13:05:42 +00:00
Matthew Raymer
008211bc21 feat(android): implement file picker for data export
- Add @capawesome/capacitor-file-picker dependency
- Update DataExportSection UI text to reflect new file picker behavior
- Implement file picker in CapacitorPlatformService
- Add debug logging for path handling
- Fix logger to show messages in Capacitor environment

WIP: File path handling still needs refinement
2025-04-09 10:04:03 +00:00
Matthew Raymer
6955a36458 chore: clean up lock file 2025-04-09 08:25:20 +00:00
Matthew Raymer
ba079ea983 chore: remove generated index.html from git repo 2025-04-09 07:53:01 +00:00
Matthew Raymer
d7b3c5ec9d fix: remove last "any" lint messages 2025-04-09 07:37:54 +00:00
Matthew Raymer
d83a25f47e chore: linted with auto-fix 2025-04-09 07:17:21 +00:00
Matthew Raymer
fb40dc0ff7 feat(android): update Capacitor assets and fix Xcode project version
- Generate new launcher icons for all densities (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi)
- Update both regular and round icon variants
- Fix Xcode project version numbers (0920 -> 920)
- Add missing file provider paths meta-data
2025-04-09 07:10:30 +00:00
Matthew Raymer
d03fa55001 refactor(platform): replace platform checks with capability-based system
- Add PlatformCapabilities interface to define available features
- Remove isWeb(), isCapacitor(), isElectron(), isPyWebView() methods
- Update platform services to implement getCapabilities()
- Refactor DataExportSection to use capability checks instead of platform checks
- Improve platform abstraction and separation of concerns
- Make platform-specific logic more maintainable and extensible

This change decouples components from specific platform implementations,
making the codebase more maintainable and easier to extend with new platforms.
2025-04-08 11:29:39 +00:00
Matthew Raymer
c8eff4d39e chore(deps): update React Native and Metro dependencies
- Update react-native from 0.78.2 to 0.79.0
- Update metro and related packages from 0.81.4 to 0.82.1
- Update @react-native/virtualized-lists to 0.79.0
- Update @react-native/normalize-colors to 0.79.0
- Update @react-native/codegen to 0.79.0
- Remove unused dependencies and their related code
- Update debug configuration to use v4.4.0 instead of v2.6.9
- Update sudo-prompt to 9.1.1
- Update synckit to 0.11.3
- Gradle updated from 8.9.0 to 8.9.1

This update improves build stability and removes deprecated dependencies.
2025-04-08 11:14:23 +00:00
Matthew Raymer
b8a7771edf feat(export): adapt DataExportSection for platform-specific file handling
Integrate PlatformServiceFactory to provide platform-specific data export:
- Add platform-specific file saving for Capacitor and other platforms
- Use web download mechanism only in web platform
- Conditionally show platform-specific save instructions
- Add iOS/Android detection for targeted guidance
- Update success messages based on platform context
- Improve download link visibility logic for web platform

This change ensures proper file handling across web, mobile, and desktop
platforms while maintaining a consistent user experience.
2025-04-07 08:50:09 +00:00
Matthew Raymer
5d845fb112 docs: add comprehensive JSDoc documentation to service layer
Add detailed TypeScript JSDoc documentation to core service modules:
- api.ts: Document error handling utilities and platform-specific logging
- plan.ts: Document plan/claim loading with retry mechanism
- deepLinks.ts: Document URL parsing and routing functionality
- Platform services:
  - CapacitorPlatformService: Document mobile platform capabilities
  - ElectronPlatformService: Document desktop placeholder implementation
  - PyWebViewPlatformService: Document Python bridge placeholder
  - WebPlatformService: Document web platform limitations and features

Key improvements:
- Add detailed @remarks sections explaining implementation details
- Include usage examples with TypeScript code snippets
- Document error handling and platform-specific behaviors
- Add @todo tags for unimplemented features
- Fix PlanResponse interface to include headers property

This documentation enhances code maintainability and developer experience
by providing clear guidance on service layer functionality and usage.
2025-04-07 07:49:39 +00:00
Matthew Raymer
660f2170de fix: improve error handling in photo upload
- Add proper unknown type for error handling in PhotoDialog
- Remove any type in favor of unknown for better type safety
- Fix error message access with type guards
2025-04-07 07:29:52 +00:00
Matthew Raymer
94bd649003 refactor: improve camera controls and modularize data export
- Add detailed error logging for image upload failures in PhotoDialog and SharedPhotoView
- Extract DataExportSection into standalone component with proper prop handling
- Fix Backup Identifier Seed visibility by passing activeDid prop
2025-04-07 07:17:43 +00:00
Matthew Raymer
b2d628cfeb chore: commit gitignore 2025-04-07 06:43:56 +00:00
Matthew Raymer
00e52f8dca feat: enhance error logging and upgrade Android build tools
- Add detailed error logging for image upload failures
- Upgrade Gradle to 8.11.1 and Android build tools to 8.9.0
- Add Capacitor camera and filesystem modules to Android build
2025-04-07 06:37:14 +00:00
Matthew Raymer
073ce24f43 chore(deps): Add Capacitor camera and filesystem plugins
- Add @capacitor/camera@6.0.0 for cross-platform photo capture
- Add @capacitor/filesystem@6.0.0 for file system operations
- Maintain compatibility with existing Capacitor core v6.2.1

These plugins enable native camera access and file system operations
for the Capacitor platform implementation.
2025-04-06 13:28:50 +00:00
Matthew Raymer
2c84bb50b3 **refactor(PhotoDialog, PlatformService): Implement cross-platform photo capture and encapsulated image processing**
- Replace direct camera library with platform-agnostic `PlatformService`
- Move platform-specific image processing logic to respective platform implementations
- Introduce `ImageResult` interface for consistent image handling across platforms
- Add support for native camera and image picker across all platforms
- Simplify `PhotoDialog` by removing platform-specific logic
- Maintain existing cropping and upload functionality
- Improve error handling and logging throughout
- Clean up UI for better user experience
- Add comprehensive documentation for usage and architecture

**BREAKING CHANGE:** Removes direct camera library dependency in favor of `PlatformService`

This change improves separation of concerns, enhances maintainability, and standardizes cross-platform image handling.
2025-04-06 13:04:26 +00:00
Matthew Raymer
abf18835f6 feat: update TypeScript config for platform services
- Add useDefineForClassFields for class field initialization
- Remove test-playwright from includes
- Add tsconfig.node.json reference
- Remove redundant node_modules exclude
2025-04-06 06:58:25 +00:00
Matthew Raymer
f72562804d feat: update TypeScript config for platform services
- Add useDefineForClassFields for class field initialization
- Remove test-playwright from includes
- Add tsconfig.node.json reference
- Remove redundant node_modules exclude
2025-04-06 06:58:14 +00:00
Matthew Raymer
bdc5ffafc1 baseline for this branch 2025-04-06 05:41:12 +00: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
071792b97c on home page: fix images for all persons, remove excessive verbiage, fix project icon, allow click on image to close 2025-03-24 20:51:16 -06:00
bf2f23021f change 'fa' to 'font-awesome' 2025-03-24 19:29:01 -06:00
829870b16c add some logging to the DB (especially for iOS app feed debugging) 2025-03-24 19:26:47 -06:00
Matthew Raymer
44ffeebabe chore: chang applicationId in iOS for consistency 2025-03-24 09:01:56 +00:00
Matthew Raymer
bed3bfa387 Merge branch 'homeview-refresh-2025-02'
refactor: Extract ActivityListItem component and add claim confirmation

- Move activity list item from HomeView to dedicated component
- Add claim confirmation functionality with AgreeAction schema
- Update feed data handling for confirmation status
- Improve error handling with structured logging
- Add user confirmation dialog for claim verification

The changes improve code organization by:
1. Separating activity item UI into reusable component
2. Adding proper type definitions for activity records
3. Implementing structured claim confirmation flow
4. Adding user feedback for confirmation actions
5. Improving error handling with logger utility

Technical details:
- Added ActivityListItem.vue component
- Added confirmClaim method with schema.org AgreeAction
- Updated feed refresh after confirmation
- Added proper TypeScript interfaces
- Improved notification handling
2025-03-24 08:52:29 +00:00
b1056fc8dd add icon asset and new capacitor step, and change "test-all" to "test:all" 2025-03-22 16:34:41 -06:00
189bfabcf8 add LogView for those cases where the log download doesn't work 2025-03-22 15:15:17 -06:00
Matthew Raymer
aed1a9fea8 fix: Update Android package name and improve test reliability
- Change package name from app.timesafari.app to app.timesafari
- Fix ADB connection issues with server restart and retries
- Add interactive test flow with user prompts between tests
- Generate test data locally instead of external script
- Add proper cleanup of readline interface
- Fix URL scheme registration in Android manifest

Technical changes:
1. Remove duplicate SDK suppression entries
2. Update Gradle and build configurations
3. Add retry logic for flaky ADB connections
4. Add proper error handling for test data generation
5. Update all package references consistently

The changes improve Android testing by:
1. Making package naming consistent
2. Adding reliability to ADB connections
3. Adding user control over test flow
4. Providing better test progress visibility
5. Improving error handling and logging
2025-03-21 08:31:55 +00:00
Jose Olarte III
f71c76fcd3 Fix: removed links and elements
- Removed links from icons and giver name
- Removed confirm button
- Lint fixes
2025-03-21 15:52:45 +08:00
Matthew Raymer
d024db2258 feature: move amount and wire it up 2025-03-21 07:03:31 +00:00
Matthew Raymer
c760385dcf fix: improve DeepLinkErrorView code quality
- Fix TypeScript property error by using validRoutes constant
- Replace console.log with logger utility
- Fix string quote consistency
- Improve v-for loop variable naming
- Add proper type safety for route parameters

Technical Changes:
- Use VALID_DEEP_LINK_ROUTES constant for route list
- Add logger import and replace console.log calls
- Standardize string quotes to double quotes
- Rename v-for variable to avoid shadowing
- Add proper type assertions for route params

This improves code quality and type safety in the
DeepLinkErrorView component while maintaining consistent
coding standards.
2025-03-20 12:04:27 +00:00
Matthew Raymer
8be8de5f1f feat(ios-testing): Enhance deeplink testing and error handling
- Improve test data generation and validation
  - Add detailed logging of generated test data
  - Implement robust validation of required fields
  - Use ts-node script for test data generation
  - Add fallback data generation with validation

- Enhance deeplink testing UX
  - Add interactive prompts between tests
  - Display detailed test progress and next steps
  - Improve error handling and test skip logic
  - Add comprehensive logging throughout test execution

- Improve DeepLinkErrorView
  - Add detailed error information display
  - Show debug information for parameters and queries
  - Enhance UI with better styling and layout
  - Add safe area spacing for iOS

- Refactor deeplink handling
  - Standardize route definitions
  - Improve parameter validation
  - Add better error logging
2025-03-20 04:34:47 -07:00
Jose Olarte III
b40604f8a6 Amount above arrow
- Needs wiring up
2025-03-19 20:47:10 +08:00
Matthew Raymer
2660b91995 wip: Improve deep link validation and error handling
- Add comprehensive route validation with zod schema
- Create type-safe DeepLinkRoute enum for all valid routes
- Add structured error handling for invalid routes
- Redirect to error page with detailed feedback
- Add better timeout handling in deeplink tests

The changes improve robustness by:
1. Validating route paths before navigation
2. Providing detailed error messages for invalid links
3. Redirecting users to dedicated error pages
4. Adding parameter validation with specific feedback
5. Improving type safety across deeplink handling
2025-03-18 09:19:35 +00:00
Matthew Raymer
474999dc9c feat(test): Comprehensive iOS test script overhaul with context-aware deeplink testing
* Add complete iOS platform cleanup and reset on each test run
* Implement interactive deeplink testing with keyboard controls between tests
* Add context awareness to verify app is running and in foreground
* Improve error handling and diagnostic messaging throughout
* Auto-register URL schemes and verify app state for reliable testing
Include prerequisites check for Xcode, CocoaPods and simulator availability
* Include prerequisites check for Xcode, CocoaPods and simulator availability
2025-03-18 00:34:17 -07:00
e825950e6e remove old script 2025-03-17 20:23:04 -06:00
a73d0a85e2 change icons to font-awesome 2025-03-17 20:22:35 -06:00
fc01e81af7 remove file that should not be committed 2025-03-17 20:21:13 -06:00
Jose Olarte III
436f40813c Source-destination compacted
- Narrower max-width
- Element sizes adjusted
- Switched to a more controllable unit for widths and heights
2025-03-17 21:03:58 +08:00
Jose Olarte III
77b296b606 Identicon responsive size fix + lint-fix 2025-03-17 17:59:59 +08:00
Matthew Raymer
683e85f5be Merge branch 'deep_linking' 2025-03-17 03:01:53 +00:00
ac58804cb5 add a test for a non-existent deep link 2025-03-16 17:39:10 -06:00
49b82e6c44 rename app ID from app.timesafari.app to app.timesafari & adjust tests (Java 20 works) 2025-03-16 17:28:47 -06:00
6f4fbc697f fix 'give' query and restore confirmClaim method 2025-03-16 17:06:01 -06:00
42413045c5 add back Chrome & Mobile Safari for testing 2025-03-16 17:05:27 -06:00
245959d783 consolidate mobile build instructions, moving config afterward 2025-03-16 17:02:51 -06:00
ae376f4c81 fix app build name & add pkgx config 2025-03-16 16:48:01 -06:00
Server
a67094218d feat(ios): enhance iOS test automation and fix dependencies
- Improve iOS test script with automatic simulator selection and booting

- Add URL scheme registration for handling deeplink tests

- Enhance error handling for iOS security prompts during testing

- Improve test data generation with fallback mechanisms

- Fix Xcode build command to use standard 'build' instead of 'build-for-testing'

- Add @ethersproject/wallet dependency and update package dependencies
2025-03-14 01:41:24 -07:00
e3ac5fe9fe fix references to partner API server 2025-03-13 19:14:59 -06:00
Matthew Raymer
a215b1de72 feat: Add comprehensive iOS test runner with deeplink testing
- Add test-ios.js script for iOS build and test automation
- Add simulator detection and validation
- Add CocoaPods dependency handling
- Add iOS deeplink testing with xcrun simctl
- Add detailed build logging for iOS tests

The changes improve iOS testing by:
1. Automating iOS build and test process
2. Adding structured logging to build_logs directory
3. Supporting deeplink testing in iOS simulator
4. Validating iOS development environment
5. Matching Android test capabilities

Technical details:
- Uses xcodebuild for building and testing
- Handles simulator device detection
- Supports all deeplink test cases
- Provides detailed error reporting
- Maintains test logs for debugging
2025-03-13 08:48:47 +00:00
Matthew Raymer
d1acfb3c49 feat: Add comprehensive Android test logging and build tracking
- Add build_logs directory for tracking Android build output
- Add Android generated assets to gitignore
- Improve test logging with timestamps and build context
- Add detailed error reporting for build and test failures
- Add proper cleanup of Android build artifacts

The changes improve Android testing by:
1. Tracking build history in build_logs directory
2. Providing detailed build context for debugging
3. Cleaning up generated assets properly
4. Improving error reporting and traceability
5. Adding structured logging for test execution

Added:
- build_logs/ directory for build history
- Android assets cleanup in .gitignore
- Detailed build and test logging
2025-03-13 08:02:03 +00:00
Matthew Raymer
6773f512b9 Remove android/app/src/main/assets/public/assets/ from git tracking 2025-03-13 07:50:14 +00:00
Jose Olarte III
5dbd66e51b Nav tweaks 2025-03-12 23:12:04 +08:00
Matthew Raymer
df81bb6a95 chore: update dependencies to latest stable versions
- Update @babel packages to 7.26.10
- Update @expo packages including cli to 0.22.19
- Update @eslint-community/eslint-utils to 4.5.0
- Update axios to 1.8.3
- Update babel-plugin-polyfill-corejs3 to 0.11.1
- Update electron-to-chromium to 1.5.114
- Update expo core modules to 2.2.3
- Update expo-constants to 17.0.8

Technical Changes:
- Upgrade @babel/core, traverse, and types
- Update expo configuration plugins and metro config
- Update prebuild config and ws-tunnel
- Fix peer dependencies for babel plugins
- Update core expo modules for better stability

This updates all dependencies to their latest stable versions
to improve security and compatibility while maintaining
build stability.
2025-03-12 10:50:33 +00: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
Matthew Raymer
611d318a7a feat: Add comprehensive mobile testing infrastructure
- Add test scripts for Android and iOS platforms
- Create prerequisite checker for mobile development setup
- Add device/simulator availability checks
- Update test-all command to include mobile tests
- Add granular test commands for web and mobile

The changes improve testing by:
1. Adding structured mobile test runners
2. Validating development environment setup
3. Separating web and mobile test flows
4. Adding device availability checks
5. Providing detailed test documentation

Added scripts:
- check-prerequisites.js: Validates dev environment
- test-android.js: Runs Android tests
- test-ios.js: Runs iOS tests
- run-available-mobile-tests.js: Smart platform detection
2025-03-12 08:33:29 +00: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
Matthew Raymer
2fbd42def5 Cleanup 2025-03-12 07:24:25 +00:00
Matthew Raymer
9c8bf7997f feat: Add Fastlane configuration for mobile app deployment
- Add Fastlane scripts for iOS and Android beta/release
- Remove DID generator scripts in favor of mobile deployment
- Update .gitignore for Fastlane artifacts and reports
- Remove Android test results and build artifacts
- Increase deep link test timeout for better reliability

The changes improve mobile deployment by:
1. Adding automated deployment pipelines for iOS/Android
2. Cleaning up build artifacts from version control
3. Improving test reliability with longer timeouts
4. Removing unused DID generator scripts
5. Adding proper gitignore rules for mobile builds
2025-03-12 07:22:35 +00:00
Matthew Raymer
6d4428668a chore: sync android 2025-03-11 10:41:47 +00:00
Matthew Raymer
eda4a6b25e fix: consolidate deep link testing scripts
- Remove redundant test-deeplinks.sh script
- Update run-deeplink-tests.sh with improved timeout
- Remove unused secp256k1-sign script
- Remove deprecated new_flow scripts
- Clean up test script directory

Technical Changes:
- Increase default timeout from 5 to 10 seconds
- Remove Python/TypeScript DID generation scripts
- Remove redundant deep link test script
- Remove unused crypto signing script

This simplifies the test script directory by removing
deprecated scripts and consolidating deep link testing
into a single, more reliable script.
2025-03-11 10:39:41 +00:00
Matthew Raymer
87ef6f4186 chore: add android folder back 2025-03-11 10:01:39 +00:00
Matthew Raymer
e0aded04b4 refactor: Replace console logging with logger utility
- Add logger import across multiple view components
- Replace console.error/warn/log with logger methods
- Update error handling to use structured logging
- Improve type safety for error objects
- Add crypto-browserify polyfill for browser environment

The changes improve logging by:
1. Using consistent logging interface
2. Adding structured error logging
3. Improving error type safety
4. Centralizing logging configuration
5. Fixing browser compatibility issues

Affected files:
- Multiple view components
- vite.config.ts
- Build configuration
2025-03-11 09:35:55 +00:00
Matthew Raymer
8cae601148 Merge branch 'master' into deep_linking
chore: Clean up merge conflicts in documentation files

- Resolve merge conflicts in BUILDING.md and CHANGELOG.md
- Remove duplicate entries in documentation
- Fix formatting inconsistencies from merge
- Maintain consistent documentation style
- Clean up git diff markers

The cleanup improves documentation by:
1. Resolving all merge conflicts
2. Ensuring consistent formatting
3. Removing duplicate content
4. Maintaining documentation standards
5. Preserving change history
2025-03-11 04:05:24 +00:00
562e82f176 clean up build & test instructions 2025-03-10 20:06:00 -06:00
d53de5e79b fix linting 2025-03-10 09:10:30 -06:00
Matthew Raymer
b6213f5040 Merge branch 'deep_linking' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into deep_linking 2025-03-10 13:01:03 +00:00
Matthew Raymer
b590e41ec8 feat: add claim route deep linking support
- Add claim route to deep link schema
- Add claim view test to deeplink tests
- Update build docs for clean web builds
- Add rm -rf dist to build steps

Technical Changes:
- Add claim schema with id parameter
- Add claim route test using generated claim ID
- Add clean step before web/capacitor builds
- Update Android build instructions

This adds support for deep linking to claim views and
improves the build process reliability by ensuring clean
builds. The claim route allows direct navigation to claim
details via external links.
2025-03-10 13:00:54 +00: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
Matthew Raymer
93219219ba refactor: Improve ConfirmGiftView component organization and error handling
- Split loadClaim into focused methods for better separation of concerns
- Add proper error handling and error messages
- Add JSDoc comments for all methods
- Extract URL parameter handling into dedicated method
- Improve gift confirmation and sharing workflows

The changes improve code maintainability by:
1. Breaking down monolithic methods into smaller, focused functions
2. Adding clear error handling and user feedback
3. Improving method documentation with JSDoc
4. Separating data fetching from processing logic
5. Making component behavior more predictable
2025-03-10 10:50:50 +00:00
Matthew Raymer
8336b87bd0 docs: add Android deep linking setup instructions
- Add deep linking configuration section to BUILDING.md
- Document intent filter requirements for Android
- Add console build instructions
- Document gradlew commands for clean builds
- Add Capacitor setup steps

Technical Details:
- Add XML example for intent-filter configuration
- Document autoVerify flag requirement
- Add gradlew clean and build commands
- Add lint baseline configuration
- Document Capacitor CLI usage

This improves the developer documentation by adding clear
instructions for setting up Android deep linking support,
including both Android Studio and command line build
processes.
2025-03-10 09:40:48 +00:00
Matthew Raymer
a40420af16 fix: streamline deep link handling and testing
- Add force-stop before deep link execution
- Remove URL encoding for deep link paths
- Simplify deep link schema definitions
- Remove unused route mappings
- Add proper app package name for force-stop

Technical Changes:
- Remove redundant URL encoding for deep link paths
- Add force-stop for app.timesafari.app before deep links
- Add sleep delay after force-stop for reliability
- Remove unused route mappings from routeMap
- Simplify DID schema to only require id field

This improves deep link testing reliability by properly
resetting app state before each test and simplifies the
deep link handling code by removing unused routes and
redundant URL encoding.
2025-03-10 07:52:21 +00:00
Matthew Raymer
21244efa73 fix(deeplinks): improve DID route handling and URL encoding
- Add colon (:) to safe characters in URL encoding to preserve DID format
- Add test case for simple DID route with "test" identifier
- Improve test descriptions for DID-based deeplinks
- Fix typo in test sequence (/d)

The URL encoding now correctly handles DID format (did:ethr:0x...) while still
encoding other special characters. Added basic DID test case to verify route
handling before testing with actual DID values.
2025-03-10 07:15:29 +00:00
Matthew Raymer
02d6d220c7 docs: Add JSDoc comments to DIDView methods
- Add detailed JSDoc comments for all methods in DIDView
- Improve method descriptions and parameter documentation
- Group related methods with descriptive comments
- Add return type documentation where applicable
- Clarify method purposes and side effects

The changes improve code documentation by:
1. Adding clear method descriptions
2. Documenting parameters and return types
3. Explaining method behaviors and side effects
4. Grouping related functionality
5. Making code more maintainable
2025-03-10 07:01:05 +00:00
Matthew Raymer
5ed626b92f Merge branch 'deep_linking' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into deep_linking 2025-03-10 05:02:06 +00:00
Matthew Raymer
4562be3bac feat: add deep linking support for DID routes
- Add DID route schema to deepLinks.ts
- Enable Android app link verification
- Add autoVerify flag to Android manifest

Technical Changes:
- Add did schema with id parameter to deepLinkSchemas
- Add DeepLinkParams type for DID routes
- Set android:autoVerify="true" in intent-filter
- Update manifest to handle DID deep links

This enables proper deep linking for DID-based routes and
allows Android to verify app links automatically. The DID
schema allows direct navigation to DID-specific views via
external links.
2025-03-10 05:00:46 +00:00
Matthew Raymer
22de70a77d chore: added ios and android to ignore 2025-03-10 03:39:42 +00:00
Matthew Raymer
d6bf89ba57 chore: Remove folders 2025-03-10 03:38:24 +00:00
Matthew Raymer
b55b786738 documentation: update Android build 2025-03-10 03:21:29 +00:00
Matthew Raymer
879c00bd97 chore: add android/ios to gitignore 2025-03-10 03:18:31 +00:00
Matthew Raymer
6cb1482b5f chore: updated package-lock and added a README for the test scripts. 2025-03-09 11:10:48 +00:00
Matthew Raymer
ad9b4836cd feat: add JSON output files and improve test flow
- Add .generated directory for test artifacts
- Save key derivation data to key_derivation.json
- Save account initialization to account_init.json
- Save registration data to registration.json
- Save claim details to claim_details.json
- Save test environment to test-env.sh
- Save contacts data to contacts.json
- Add proper error handling for file operations
- Improve deeplink test flow with JSON-based data
- Add color output and better status messages
- Add ADB device detection and fallback to print mode

Technical Changes:
- Add file system operations with proper error handling
- Standardize JSON output format across Python/TypeScript
- Update test flow to use generated JSON files
- Add proper typing for registration response
- Improve error reporting and debug output

This improves the test workflow by saving all intermediate
data as JSON files that can be used by other test scripts.
The deeplink testing now uses this data instead of environment
variables for better reliability.
2025-03-08 13:01:15 +00:00
Matthew Raymer
32f1f182d7 fix: improve key derivation and logging
- Fix mnemonic validation in Python version
- Add consistent output logging across Python/TypeScript
- Show key derivation details in readable format
- Truncate sensitive values in output
- Match output format between implementations

This fixes the mnemonic error and improves debugging by
adding consistent logging of the key derivation process.
2025-03-08 11:04:17 +00:00
Matthew Raymer
8f7d794962 fix: improve image handling and icon support
- Fix image load event handler signature
- Add alt text for accessibility
- Add building icon to FontAwesome library

Technical Changes:
- Update cacheImage event to pass only image URL
- Add proper alt text for activity images
- Add faBuilding icon to FontAwesome library

This improves image handling and accessibility while adding
needed icon support for the activity feed interface.
2025-03-07 12:58:14 +00:00
Matthew Raymer
fa7d6317b9 feat: add claim confirmation functionality to activity feed
- Add confirm button functionality to ActivityListItem
- Implement confirmation logic in HomeView
- Add proper button state handling and validation

Technical Changes:
- Add canConfirm computed property to validate confirmation ability
- Add handleConfirmClick method with proper error handling
- Pass required props (isRegistered, activeDid, confirmerIdList)
- Add confirmation dialog with user verification
- Implement claim submission with proper cleanup
- Add visual feedback for button states
- Update feed after successful confirmation

UI/UX Improvements:
- Add disabled state styling for confirm button
- Show proper error messages for invalid confirmation attempts
- Add loading and success notifications
- Improve button accessibility with proper states

Bug Fixes:
- Make apiServer optional in settings type
- Fix settings update during registration
- Add proper type checking for claim confirmation

This adds the ability to confirm claims directly from the
activity feed with proper validation, error handling, and
user feedback. The confirmation flow matches the existing
claim view confirmation functionality.
2025-03-07 10:22:53 +00:00
Matthew Raymer
510f6a5faa fix: improve secp256k1 signing in shell script
- Use proper secp256k1 signing tools
- Simplify private key format
- Add fallback signing mechanism
- Match TypeScript/Python signature format
- Fix JWT verification error

This fixes the JWT verification by using proper
secp256k1 signing tools and matching the signature
format of the working implementations.
2025-03-05 14:20:04 +00:00
Matthew Raymer
1bb4e77714 refactor: replace Python crypto with native openssl operations
- Remove Python dependency for cryptographic operations
- Implement pure bash/openssl key generation
- Maintain ES256K signature compatibility
- Add detailed error handling and logging
2025-03-05 14:11:18 +00:00
Matthew Raymer
cc10dab3a4 feat: add shell implementation of DID registration flow
- Match Python/TypeScript implementations
- Use consistent JWT signing approach
- Maintain payload compatibility
- Add detailed documentation
2025-03-05 14:08:27 +00:00
Matthew Raymer
0a066dc99c feat: add claim fetching functionality
- Add fetch_claim method to match TypeScript version
- Implement JWT authentication for claim fetching
- Update main flow to fetch claim after registration
- Add error handling and logging
- Match TypeScript API structure

This adds the ability to fetch claim details after
successful DID registration, completing the full
registration and verification flow.
2025-03-05 13:49:54 +00:00
Matthew Raymer
eeddab506d feat: implement DID registration with JWT signing
- Add full DID registration flow matching TypeScript version
- Implement ES256K JWT signing with PEM key format
- Add async/await support for JWT operations
- Improve error handling and debug output
- Add rich documentation and type hints

Technical Changes:
- Convert private key to PEM format for JWT signing
- Match TypeScript's JWT payload structure
- Add proper JWT header with ES256K algorithm
- Implement async functions for JWT creation
- Add detailed debug output for JWT parts

Documentation:
- Add module-level docstring with flow description
- Add function-level docstrings with examples
- Document security considerations
- Add technical details and error handling info

Dependencies:
- Add cryptography for key format conversion
- Add jwcrypto for JWT operations
- Update requirements.txt with versions and comments

This commit implements the complete DID registration flow,
matching the TypeScript implementation's behavior and
adding comprehensive documentation and error handling.
2025-03-05 13:44:42 +00:00
Matthew Raymer
bfd1aee27c Experimental DID creation and registration 2025-03-05 06:30:32 +00:00
Matthew Raymer
2424d788d1 Merge branch 'deep_linking' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into deep_linking 2025-03-05 06:08:54 +00:00
Matthew Raymer
d14431161a refactor: Improve settings and feed handling in HomeView
- Split feed initialization into separate methods
- Add registration status verification
- Improve error handling and notifications
- Add JSDoc comments for better code documentation
- Make apiServer optional in settings type

The changes improve code organization by:
1. Breaking down monolithic initialization into focused methods
2. Adding proper type safety for optional settings
3. Improving error handling and user feedback
4. Adding clear documentation for methods
5. Separating concerns for feed, contacts and registration
2025-03-05 06:08:08 +00:00
8f993923a1 for test script: add requirements, fix endpoint, and add setup instructions 2025-03-04 21:07:55 -07:00
Matthew Raymer
9edb3a255c feat: enhance DID visibility management
- Add automatic visibility request when DID not found
- Add rich documentation for script usage and features
- Improve error handling and user feedback
- Add visual indicators for success/failure
- Add HTTPS for API endpoints
- Add proper exit codes for different scenarios
- Add detailed debug logging throughout
- Add file header documentation

This enhances the DID visibility script by adding automatic
visibility requests when a DID is not found, along with
better documentation, error handling and user feedback.
2025-03-04 13:30:10 +00:00
Matthew Raymer
b7b208407b feat(scripts): Add DID visibility check script
Adds dids_seen.sh script to check DID visibility permissions in endorser.ch system.
Key features:
- JWT creation and signing using ES256K-R
- DID visibility checking via API
- Environment variable and .env file support
- Debug logging with DEBUG=1 flag
- Command line argument parsing for specific DID checks
- Secure key handling with temporary files
- Pretty-printed JSON output

Security:
- Uses secure temporary files with cleanup
- Validates DER signature format
- Handles key material securely
- Supports environment variable configuration
2025-03-04 13:07:35 +00:00
Jose Olarte III
4a75cdf20e Homeview changes
- Moved activity image further up the frame
- Added placeholder icon for projects
- Minor fixes
2025-03-04 20:38:14 +08:00
Matthew Raymer
a974ab4f51 refactor: improve DID generation and error handling
- Convert did_generator.sh to output clean JSON
- Add structured error reporting with stages
- Improve debug logging with DEBUG flag
- Better error handling in run-deeplink-tests.sh
- Add detailed debug tracing
- Fix JSON parsing and validation
- Add visual feedback for generated DIDs
- Use printf instead of echo for consistent output
- Remove stderr mixing with stdout
- Add proper exit status handling

This refactors the DID generation process to be more reliable
and maintainable by using structured JSON output and proper
error handling throughout the pipeline.
2025-03-04 12:32:19 +00:00
Matthew Raymer
bc971056e1 fix: WIP: Update test scripts for DID verification and claim generation
- Add check-did.sh to verify DID registration using admin JWT auth
- Fix JWT signing in generate-test-claim.sh to match uport-credentials format
- Clean up DID extraction in run-deeplink-tests.sh
- Add proper error handling and response parsing

The changes improve test script reliability by:
1. Using consistent JWT signing across scripts
2. Adding ability to verify DID registration status
3. Simplifying DID info extraction
4. Adding better error messages and debug output
2025-03-04 10:20:14 +00:00
Matthew Raymer
69b4b899c9 fix: Implement proper JWT signing in test claim generator
- Add ES256K signature generation for test claims
- Fix signature format to match endorser.ch requirements
- Remove dependency on did_generator.sh for signing
- Improve JWT creation with proper header and payload structure

The changes fix JWT verification issues by:
1. Implementing proper DER to R+S signature conversion
2. Handling secp256k1 private key formatting correctly
3. Using correct base64url encoding for JWT components
4. Adding proper issuer field to JWT payload
2025-03-04 09:12:47 +00:00
Matthew Raymer
02747fb771 feat: Improve test scripts for deep link testing
- Add DID generation and management for testing
- Create .generated directory for test artifacts
- Add environment variable support for test configuration
- Improve deep link test script with better URL handling
- Add print/execute modes for testing with/without device

The changes improve the testing workflow by:
1. Generating and managing test DIDs automatically
2. Storing test artifacts in .generated directory (gitignored)
3. Adding proper URL encoding for deep links
4. Supporting both print mode for debugging and execute mode for device testing
5. Adding better error handling and validation
2025-03-04 09:01:05 +00:00
Matthew Raymer
3dae8f7f7f feat: Add environment variable support for DID registration
- Bash implementation of DID creation-registration
- Move admin credentials to .env file for better security
- Add .env.example with default values
- Add dotenv support to TypeScript, Python and Bash implementations
- Update dependencies to include dotenv packages
- Fix JWT signature format in Bash implementation
- Add DER signature parsing for ES256K in Bash script

The admin DID and private key can now be configured via environment
variables, with fallback to default values if not set. This allows
for easier testing and deployment across different environments.
2025-03-04 06:27:20 +00:00
Matthew Raymer
9e6f0ab468 feat: Improve DID registration with admin credentials
- Add admin keypair (DID + private key) for proper registration signing
- Remove SQLite database dependency for admin DID lookup
- Update both TypeScript and Python implementations to use admin credentials
- Enhance documentation with detailed usage, options, and security notes
- Fix JWT signing to use admin's key as issuer
- Standardize API URL handling with defaults
- Add command-line options for admin DID and API URL override

Both implementations now successfully register new DIDs using the admin's
credentials to sign the registration claims. The admin acts as both the
agent in the claim and the issuer of the JWT.
2025-03-04 05:59:10 +00:00
Matthew Raymer
6685421ee8 fix: WIP: update did_generator.ts to use registration table
Changes:
- Update SQL query to use registration table instead of accounts
- Add proper column names for registration table schema
- Add issuanceDate sorting for latest admin DID
- Improve error messages for database queries
- Add TypeScript types for database row results

This fixes DID generation by using the correct table schema
from the endorser database.
2025-03-03 13:21:51 +00:00
Jose Olarte III
79fdb9e570 Homeview design adjustments
- Added markup for poster info (needs wiring)
- Repositioned giver and receiver type icons to beside their labels
- Added "Confirm" button to bottom right of item card (timestamp repositioned to poster info as a result)
- Spacing tweaks
2025-03-03 19:34:09 +08:00
Matthew Raymer
4fcbb78450 feat(test-scripts): add registration attempt to TypeScript DID generator
- Added registration attempt to TypeScript DID generator to match Python version
- Added node-fetch and types for HTTP request
- Both scripts now show same UNREGISTERED_USER error from server
- Cleaned up package.json devDependencies formatting
2025-03-03 10:43:42 +00:00
9ffdb54c20 change permissions on test-deeplinks script to allow execution 2025-03-01 17:07:33 -07:00
Matthew Raymer
f4c5567471 test: enhance deep link testing with real JWT examples
Changes:
- Add real JWT example for invite testing
- Add detailed JWT payload documentation
- Update test-deeplinks.sh with valid claim IDs
- Add test case for single contact invite
- Improve test descriptions and organization

This improves test coverage by using real-world JWT examples
and valid claim identifiers.
2025-03-01 12:28:48 +00:00
Matthew Raymer
86c1abb9be Fixing a goof I made moving between workstations
Merge branch 'deep_linking' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into deep_linking
2025-03-01 10:26:14 +00:00
e96617ca0f tweak tests for clarity 2025-02-28 12:17:22 -07:00
Jose Olarte III
aa09827317 Type fixes 2025-02-28 21:00:03 +08:00
Matthew Raymer
cc1780bd01 refactor: extract ActivityListItem into separate component
- Move activity list item markup from HomeView to new component
- Improve code organization and reusability
- Pass required props for claim handling and image viewing
- Maintain existing functionality while reducing component complexity
- Clean up unused commented code in HomeView

This refactor improves code maintainability by extracting the activity
feed item logic into its own component.
2025-02-28 09:34:59 +00:00
Server
d700be9e5b documentation: Updated build instructions for IOS and refreshed package-lock 2025-02-28 00:31:22 -08:00
Server
317fb2c644 chore: add ios and ruby-version to ignore 2025-02-28 00:12:10 -08:00
b91f2a5df7 fix one more styling error 2025-02-27 18:02:05 -07:00
f6871e139d fix linting 2025-02-27 17:51:57 -07:00
Jose Olarte III
e5d9c25ad4 Comments 2025-02-27 21:21:01 +08:00
Jose Olarte III
ef8c2e6093 In-progress: homeview design refresh
I had to comment out line 544 because it was causing errors (and seemed redundant?)
2025-02-27 21:12:29 +08:00
61afba3bca show totals of gives to a project 2025-02-22 20:34:23 -07:00
201 changed files with 16298 additions and 3787 deletions

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
# Admin DID credentials
ADMIN_DID=did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F
ADMIN_PRIVATE_KEY=2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b
# API Configuration
ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim

View File

@@ -26,6 +26,7 @@ module.exports = {
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off"
"@typescript-eslint/no-unnecessary-type-constraint": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
},
};

19
.gitignore vendored
View File

@@ -38,5 +38,20 @@ pnpm-debug.log*
/dist-capacitor/
/test-playwright-results/
playwright-tests
test-playwright
dist-electron-packages
dist-electron-packages
.ruby-version
+.env
# Generated test files
.generated/
.env.default
vendor/
# Build logs
build_logs/
android/app/src/main/assets/public
android/app/src/main/res
android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
android/.gradle/file-system.probe

View File

@@ -4,31 +4,52 @@ This guide explains how to build TimeSafari for different platforms.
## Prerequisites
For a quick dev environment setup, use [pkgx](https://pkgx.dev).
- Node.js (LTS version recommended)
- npm (comes with Node.js)
- Git
- For iOS builds: macOS with Xcode 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
## Forks
If you have forked this to make your own app, you'll want to customize the iOS & Android files. You can either edit existing ones, or you can remove the `ios` and `android` directories and regenerate them before the `npx cap sync` step in each setup.
```bash
npx cap add android
npx cap add ios
```
You'll also want to edit the deep link configuration (see below).
## Initial Setup
1. Clone the repository:
```bash
git clone [repository-url]
cd TimeSafari
```
2. Install dependencies:
Install dependencies:
```bash
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:
@@ -36,17 +57,66 @@ To build for web deployment:
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
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)
### Building for Linux
### Linux Build
1. Build the electron app in production mode:
@@ -105,28 +175,35 @@ Prerequisites: macOS with Xcode installed
1. Build the web assets:
```bash
npm run build -- --mode capacitor
npm run build:capacitor
```
2. Add iOS platform if not already added:
```bash
npx cap add ios
```
3. Update iOS project with latest build:
2. Update iOS project with latest build:
```bash
npx cap sync ios
```
4. Open the project in Xcode:
3. Copy the assets:
```bash
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
npx capacitor-assets generate --ios
```
3. Open the project in Xcode:
```bash
npx cap open ios
```
5. 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
@@ -135,21 +212,26 @@ Prerequisites: Android Studio with SDK installed
1. Build the web assets:
```bash
npm run build -- --mode capacitor
rm -rf dist
npm run build:web
npm run build:capacitor
cd android
./gradlew clean
./gradlew assembleDebug
```
2. Add Android platform if not already added:
```bash
npx cap add android
```
3. Update Android project with latest build:
2. Update Android project with latest build:
```bash
npx cap sync android
```
3. Copy the assets
```bash
npx capacitor-assets generate --android
```
4. Open the project in Android Studio:
```bash
@@ -158,14 +240,94 @@ Prerequisites: Android Studio with SDK installed
5. Use Android Studio to build and run on emulator or device.
## Development
To run the application in development mode:
1. Start the development server:
## Android Build from the console
```bash
npm run dev
cd android
./gradlew clean
./gradlew build -Dlint.baselines.continue=true
cd ..
npx cap run android
```
... or, to create the `aab` file, `bundle` instead of `build`:
```bash
./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
```
## First-time Android Configuration for deep links
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
```xml
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="timesafari" />
</intent-filter>
```
You must also add the following to the `android/app/build.gradle` file:
```gradle
android {
// ... existing config ...
lintOptions {
disable 'UnsanitizedFilenameFromContentProvider'
abortOnError false
baseline file("lint-baseline.xml")
// Ignore Capacitor module issues
ignore 'DefaultLocale'
ignore 'UnsanitizedFilenameFromContentProvider'
ignore 'LintBaseline'
ignore 'LintBaselineFixed'
}
}
```
Modify `/android/build.gradle` to use a stable version of AGP and make sure Kotlin version is compatible.
```gradle
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
// Use a stable version of AGP
classpath 'com.android.tools.build:gradle:8.1.0'
// Make sure Kotlin version is compatible
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
// Add this to handle version conflicts
configurations.all {
resolutionStrategy {
force 'org.jetbrains.kotlin:kotlin-stdlib:1.8.0'
force 'org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0'
}
}
```
## PyWebView Desktop Build
@@ -254,17 +416,13 @@ The packaged application will be in `dist/TimeSafari`
## Testing
Run local tests:
Run all tests (requires XCode and Android Studio/device):
```bash
npm run test-local
npm run test:all
```
Run all tests (includes building):
```bash
npm run test-all
```
See [TESTING.md](test-playwright/TESTING.md) for more details.
## Linting
@@ -351,3 +509,19 @@ mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist
- For iOS: Xcode command line tools must be installed
- For Android: Correct SDK version must be installed
- Check Capacitor configuration in capacitor.config.ts
# List all installed packages
adb shell pm list packages | grep timesafari
# Force stop the app (if it's running)
adb shell am force-stop app.timesafari
# Clear app data (if you don't want to fully uninstall)
adb shell pm clear app.timesafari
# Uninstall for all users
adb shell pm uninstall -k --user 0 app.timesafari
# Check if app is installed
adb shell pm path app.timesafari

View File

@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.4.5] - 2025.02.23
### Added
- Total amounts of gives on project page
### Changed in DB or environment
- Requires Endorser.ch version 4.2.6+
## [0.4.4] - 2025.02.17
### Fixed in 0.4.4

5
Gemfile Normal file
View File

@@ -0,0 +1,5 @@
source "https://rubygems.org"
gem "fastlane"
gem "cocoapods"

321
Gemfile.lock Normal file
View File

@@ -0,0 +1,321 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
activesupport (7.2.2.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.2)
aws-partitions (1.1066.0)
aws-sdk-core (3.220.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.182.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.9)
claide (1.1.0)
cocoapods (1.16.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.16.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.27.0, < 2.0)
cocoapods-core (1.16.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (2.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
drb (2.2.1)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
excon (0.112.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.227.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
ffi (1.17.1)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm-linux-gnu)
ffi (1.17.1-arm-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86-linux-gnu)
ffi (1.17.1-x86-linux-musl)
ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.9.0)
mutex_m
i18n (1.14.7)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
json (2.10.2)
jwt (2.10.1)
base64
logger (1.6.6)
mini_magick (4.13.2)
mini_mime (1.1.5)
minitest (5.25.5)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.2)
public_suffix (4.0.7)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.1)
rouge (3.28.0)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
securerandom (0.4.1)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.0)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
ruby
x86-linux-gnu
x86-linux-musl
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
cocoapods
fastlane
BUNDLED WITH
2.6.5

View File

@@ -17,72 +17,18 @@ npm install
npm run dev
```
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use <http://localhost:3000> for local endorser.ch
See [BUILDING.md](BUILDING.md) for more details.
### Build the test & production app
```bash
npm run serve
```
### Lint and fix files
```bash
npm run lint
```
### Run all UI tests
Look below for the "test-all" instructions.
### 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.
## Tests
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
## Icons
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.

3
android/.gitignore vendored
View File

@@ -1,5 +1,8 @@
# 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
*.apk
*.aar

View File

@@ -0,0 +1,2 @@
#Wed Apr 09 09:01:13 UTC 2025
gradle.version=8.11.1

Binary file not shown.

View File

View File

@@ -1,14 +1,38 @@
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("app/gradle.properties.secrets")
if (secretsPropertiesFile.exists()) {
Properties secretsProperties = new Properties()
secretsProperties.load(new FileInputStream(secretsPropertiesFile))
secretsProperties.each { name, value ->
project.ext[name] = value
}
}
}
android {
namespace "app.timesafari.app"
namespace 'app.timesafari'
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
versionCode 10
versionName "0.4.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// 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:!*~'
}
}
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 {
release {
minifyEnabled false
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

@@ -10,6 +10,10 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-camera')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-share')
implementation project(':capawesome-capacitor-file-picker')
}

View File

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

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
@@ -8,15 +7,13 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:exported="true"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
android:theme="@style/AppTheme.NoActionBarLaunch">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -28,7 +25,6 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="timesafari" />
</intent-filter>
</activity>
<provider
@@ -36,13 +32,13 @@
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

View File

@@ -0,0 +1,21 @@
{
"appId": "app.timesafari",
"appName": "TimeSafari",
"webDir": "dist",
"bundledWebRuntime": false,
"server": {
"cleartext": true
},
"plugins": {
"App": {
"appUrlOpen": {
"handlers": [
{
"url": "timesafari://*",
"autoVerify": true
}
]
}
}
}
}

View File

@@ -0,0 +1,22 @@
[
{
"pkg": "@capacitor/app",
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
},
{
"pkg": "@capacitor/camera",
"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
},
{
"pkg": "@capacitor/filesystem",
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
},
{
"pkg": "@capacitor/share",
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
},
{
"pkg": "@capawesome/capacitor-file-picker",
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
}
]

View File

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2480 4005 c-25 -7 -58 -20 -75 -29 -16 -9 -40 -16 -52 -16 -17 0
-24 -7 -28 -27 -3 -16 -14 -45 -24 -65 -21 -41 -13 -55 18 -38 25 13 67 13 92
-1 15 -8 35 -4 87 17 99 39 130 41 197 10 64 -29 77 -31 107 -15 20 11 20 11
-3 35 -12 13 -30 24 -38 24 -24 1 -132 38 -148 51 -8 7 -11 20 -7 32 12 37
-40 47 -126 22z"/>
<path d="M1450 3775 c-7 -8 -18 -15 -24 -15 -7 0 -31 -14 -54 -32 -29 -22 -38
-34 -29 -40 17 -11 77 -10 77 1 0 5 16 16 35 25 60 29 220 19 290 -18 17 -9
33 -16 37 -16 4 0 31 -15 60 -34 108 -70 224 -215 282 -353 30 -71 53 -190 42
-218 -10 -27 -23 -8 -52 75 -30 90 -88 188 -120 202 -13 6 -26 9 -29 6 -3 -2
11 -51 30 -108 28 -83 35 -119 35 -179 0 -120 -22 -127 -54 -17 -11 37 -13 21
-18 -154 -5 -180 -8 -200 -32 -264 -51 -132 -129 -245 -199 -288 -21 -12 -79
-49 -129 -80 -161 -102 -294 -141 -473 -141 -228 0 -384 76 -535 259 -81 99
-118 174 -154 312 -31 121 -35 273 -11 437 19 127 19 125 -4 125 -23 0 -51
-34 -87 -104 -14 -28 -33 -64 -41 -81 -19 -34 -22 -253 -7 -445 9 -106 12
-119 44 -170 19 -30 42 -67 50 -81 64 -113 85 -140 130 -169 28 -18 53 -44 61
-62 8 -20 36 -45 83 -76 62 -39 80 -46 151 -54 44 -5 96 -13 115 -18 78 -20
238 -31 282 -19 24 6 66 8 95 5 76 -9 169 24 319 114 32 19 80 56 106 82 27
26 52 48 58 48 5 0 27 26 50 58 48 66 56 70 132 71 62 1 165 29 238 64 112 55
177 121 239 245 37 76 39 113 10 267 -12 61 -23 131 -26 156 -5 46 -5 47 46
87 92 73 182 70 263 -8 l51 -49 -6 -61 c-4 -34 -13 -85 -21 -113 -28 -103 -30
-161 -4 -228 16 -44 32 -67 55 -83 18 -11 39 -37 47 -58 10 -23 37 -53 73 -81
32 -25 69 -57 82 -71 14 -14 34 -26 47 -26 12 0 37 -7 56 -15 20 -8 66 -17
104 -20 107 -10 110 -11 150 -71 50 -75 157 -177 197 -187 18 -5 53 -24 78
-42 71 -51 176 -82 304 -89 61 -4 127 -12 147 -18 29 -9 45 -8 77 6 23 9 50
16 60 16 31 0 163 46 216 76 28 15 75 46 105 69 30 23 69 49 85 58 17 8 46 31
64 51 19 20 40 36 47 36 18 0 77 70 100 120 32 66 45 108 55 173 5 32 16 71
24 87 43 84 43 376 0 549 -27 105 -43 127 -135 188 -30 21 -65 46 -77 57 -13
11 -23 17 -23 14 0 -3 21 -46 47 -94 79 -151 85 -166 115 -263 25 -83 28 -110
28 -226 0 -144 -17 -221 -75 -335 -39 -77 -208 -244 -304 -299 -451 -263 -975
-67 -1138 426 -23 70 -26 95 -28 254 -1 108 -7 183 -14 196 -6 12 -11 31 -11
43 0 32 31 122 52 149 10 13 18 28 18 34 0 5 25 40 56 78 60 73 172 170 219
190 30 12 30 13 6 17 -15 2 -29 -2 -37 -12 -6 -9 -16 -16 -22 -16 -6 0 -23
-11 -39 -24 -15 -12 -33 -25 -40 -27 -17 -6 -82 -60 -117 -97 -65 -70 -75 -82
-107 -133 -23 -34 -35 -46 -37 -35 -3 16 20 87 44 134 6 12 9 34 6 48 -4 22
-8 25 -31 19 -14 -3 -38 -15 -53 -26 -34 -24 -34 -21 -6 28 65 112 184 206
291 227 15 3 39 9 55 12 l27 6 -24 9 c-90 35 -304 -66 -478 -225 -39 -36 -74
-66 -77 -66 -22 0 18 82 72 148 19 23 32 46 28 49 -4 4 -26 13 -49 19 -73 21
-161 54 -171 64 -6 6 -20 10 -32 10 -21 0 -21 -1 -8 -40 45 -130 8 -247 -93
-299 -25 -13 -31 0 -14 29 15 22 1 33 -22 17 -56 -36 -117 -22 -117 28 0 13
-16 47 -35 76 -22 34 -33 60 -29 73 4 16 -3 26 -26 39 -16 10 -30 21 -30 25 1
18 54 64 87 76 l38 13 -33 5 c-30 4 -115 -18 -154 -42 -13 -7 -20 -5 -27 8 -9
16 -12 16 -53 1 -160 -61 -258 -104 -258 -114 0 -7 10 -20 21 -31 103 -91 217
-297 249 -449 28 -135 41 -237 35 -276 -14 -91 -48 -170 -97 -220 -44 -47 -68
-60 -68 -40 0 6 4 12 8 15 5 3 24 35 42 72 l33 67 -6 141 c-4 103 -11 158 -26
205 -12 35 -21 70 -21 77 0 7 -20 56 -45 108 -82 173 -227 322 -392 401 -67
33 -90 39 -163 42 -108 5 -130 10 -130 28 0 20 -63 20 -80 0z"/>
<path d="M3710 3765 c0 -20 8 -28 39 -41 22 -8 42 -22 45 -30 5 -14 42 -19 70
-8 10 4 -7 21 -58 55 -41 27 -79 49 -85 49 -6 0 -11 -11 -11 -25z"/>
<path d="M3173 3734 c-9 -25 10 -36 35 -18 12 8 22 19 22 25 0 16 -50 10 -57
-7z"/>
<path d="M1982 3728 c6 -16 36 -34 44 -26 3 4 4 14 1 23 -7 17 -51 21 -45 3z"/>
<path d="M1540 3620 c0 -5 7 -10 16 -10 8 0 12 5 9 10 -3 6 -10 10 -16 10 -5
0 -9 -4 -9 -10z"/>
<path d="M4467 3624 c-4 -4 23 -27 60 -50 84 -56 99 -58 67 -9 -28 43 -107 79
-127 59z"/>
<path d="M655 3552 c-11 -2 -26 -9 -33 -14 -7 -6 -27 -18 -45 -27 -36 -18 -58
-64 -39 -83 9 -9 25 1 70 43 53 48 78 78 70 84 -2 1 -12 -1 -23 -3z"/>
<path d="M1015 3460 c-112 -24 -247 -98 -303 -165 -53 -65 -118 -214 -136
-311 -20 -113 -20 -145 -1 -231 20 -88 49 -153 102 -230 79 -113 186 -182 331
-214 108 -24 141 -24 247 1 130 30 202 72 316 181 102 100 153 227 152 384 0
142 -58 293 -150 395 -60 67 -180 145 -261 171 -75 23 -232 34 -297 19z m340
-214 c91 -43 174 -154 175 -234 0 -18 -9 -51 -21 -73 -19 -37 -19 -42 -5 -64
35 -54 12 -121 -48 -142 -22 -7 -47 -19 -55 -27 -9 -8 -41 -27 -71 -42 -50
-26 -64 -29 -155 -29 -111 0 -152 14 -206 68 -49 49 -63 85 -64 162 0 59 4 78
28 118 31 52 96 105 141 114 23 5 33 17 56 68 46 103 121 130 225 81z"/>
<path d="M3985 3464 c-44 -7 -154 -44 -200 -67 -55 -28 -138 -96 -162 -132
-10 -16 -39 -75 -64 -130 l-44 -100 0 -160 0 -160 45 -90 c53 -108 152 -214
245 -264 59 -31 215 -71 281 -71 53 0 206 40 255 67 98 53 203 161 247 253 53
113 74 193 74 280 -1 304 -253 564 -557 575 -49 2 -103 1 -120 -1z m311 -220
c129 -68 202 -209 160 -309 -15 -35 -15 -42 -1 -72 26 -55 -3 -118 -59 -129
-19 -3 -43 -15 -53 -26 -26 -29 -99 -64 -165 -78 -45 -10 -69 -10 -120 -1 -74
15 -113 37 -161 91 -110 120 -50 331 109 385 24 8 44 23 52 39 6 14 18 38 25
53 33 72 127 93 213 47z"/>
<path d="M487 3394 c-21 -12 -27 -21 -25 -40 2 -14 7 -26 12 -27 14 -3 48 48
44 66 -3 14 -6 14 -31 1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

View File

@@ -0,0 +1,11 @@
Model Information:
* title: Lupine Plant
* source: https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439
* author: rufusrockwell (https://sketchfab.com/rufusrockwell)
Model License:
* license type: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
* requirements: Author must be credited. Commercial use is allowed.
If you use this 3D model in your project be sure to copy paste this credit wherever you share it:
This work is based on "Lupine Plant" (https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439) by rufusrockwell (https://sketchfab.com/rufusrockwell) licensed under CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)

View File

@@ -0,0 +1,229 @@
{
"accessors": [
{
"bufferView": 2,
"componentType": 5126,
"count": 2759,
"max": [
41.3074951171875,
40.37548828125,
87.85917663574219
],
"min": [
-35.245540618896484,
-36.895416259765625,
-0.9094290137290955
],
"type": "VEC3"
},
{
"bufferView": 2,
"byteOffset": 33108,
"componentType": 5126,
"count": 2759,
"max": [
0.9999382495880127,
0.9986748695373535,
0.9985831379890442
],
"min": [
-0.9998949766159058,
-0.9975876212120056,
-0.411094069480896
],
"type": "VEC3"
},
{
"bufferView": 3,
"componentType": 5126,
"count": 2759,
"max": [
0.9987699389457703,
0.9998998045921326,
0.9577858448028564,
1.0
],
"min": [
-0.9987726807594299,
-0.9990445971488953,
-0.999801516532898,
1.0
],
"type": "VEC4"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 2759,
"max": [
1.0061479806900024,
0.9993550181388855
],
"min": [
0.00279300007969141,
0.0011620000004768372
],
"type": "VEC2"
},
{
"bufferView": 0,
"componentType": 5125,
"count": 6378,
"type": "SCALAR"
}
],
"asset": {
"extras": {
"author": "rufusrockwell (https://sketchfab.com/rufusrockwell)",
"license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)",
"source": "https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439",
"title": "Lupine Plant"
},
"generator": "Sketchfab-12.68.0",
"version": "2.0"
},
"bufferViews": [
{
"buffer": 0,
"byteLength": 25512,
"name": "floatBufferViews",
"target": 34963
},
{
"buffer": 0,
"byteLength": 22072,
"byteOffset": 25512,
"byteStride": 8,
"name": "floatBufferViews",
"target": 34962
},
{
"buffer": 0,
"byteLength": 66216,
"byteOffset": 47584,
"byteStride": 12,
"name": "floatBufferViews",
"target": 34962
},
{
"buffer": 0,
"byteLength": 44144,
"byteOffset": 113800,
"byteStride": 16,
"name": "floatBufferViews",
"target": 34962
}
],
"buffers": [
{
"byteLength": 157944,
"uri": "scene.bin"
}
],
"images": [
{
"uri": "textures/lambert2SG_baseColor.png"
},
{
"uri": "textures/lambert2SG_normal.png"
}
],
"materials": [
{
"alphaCutoff": 0.2,
"alphaMode": "MASK",
"doubleSided": true,
"name": "lambert2SG",
"normalTexture": {
"index": 1
},
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0
},
"metallicFactor": 0.0
}
}
],
"meshes": [
{
"name": "Object_0",
"primitives": [
{
"attributes": {
"NORMAL": 1,
"POSITION": 0,
"TANGENT": 2,
"TEXCOORD_0": 3
},
"indices": 4,
"material": 0,
"mode": 4
}
]
}
],
"nodes": [
{
"children": [
1
],
"matrix": [
1.0,
0.0,
0.0,
0.0,
0.0,
2.220446049250313e-16,
-1.0,
0.0,
0.0,
1.0,
2.220446049250313e-16,
0.0,
0.0,
0.0,
0.0,
1.0
],
"name": "Sketchfab_model"
},
{
"children": [
2
],
"name": "LupineSF.obj.cleaner.materialmerger.gles"
},
{
"mesh": 0,
"name": "Object_2"
}
],
"samplers": [
{
"magFilter": 9729,
"minFilter": 9987,
"wrapS": 10497,
"wrapT": 10497
}
],
"scene": 0,
"scenes": [
{
"name": "Sketchfab_Scene",
"nodes": [
0
]
}
],
"textures": [
{
"sampler": 0,
"source": 0
},
{
"sampler": 0,
"source": 1
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -0,0 +1,7 @@
package app.timesafari;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {
// ... existing code ...
}

View File

@@ -1,4 +1,4 @@
package app.timesafari.app;
package timesafari.app;
import com.getcapacitor.BridgeActivity;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -2,6 +2,6 @@
<resources>
<string name="app_name">TimeSafari</string>
<string name="title_activity_main">TimeSafari</string>
<string name="package_name">app.timesafari.app</string>
<string name="custom_url_scheme">app.timesafari.app</string>
<string name="package_name">timesafari.app</string>
<string name="custom_url_scheme">timesafari.app</string>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<access origin="*" />
</widget>

View File

@@ -2,4 +2,5 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
<files-path name="my_files" path="." />
</paths>

View File

@@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.8.1'
classpath 'com.android.tools.build:gradle:8.9.1'
classpath 'com.google.gms:google-services:4.4.0'
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -0,0 +1,59 @@
ext {
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1'
cordovaAndroidVersion = project.hasProperty('cordovaAndroidVersion') ? rootProject.ext.cordovaAndroidVersion : '10.1.1'
}
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.1'
}
}
apply plugin: 'com.android.library'
android {
namespace "capacitor.cordova.android.plugins"
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
defaultConfig {
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34
versionCode 1
versionName "1.0"
}
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
repositories {
google()
mavenCentral()
flatDir{
dirs 'src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(dir: 'src/main/libs', include: ['*.jar'])
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "org.apache.cordova:framework:$cordovaAndroidVersion"
// SUB-PROJECT DEPENDENCIES START
// SUB-PROJECT DEPENDENCIES END
}
// PLUGIN GRADLE EXTENSIONS START
apply from: "cordova.variables.gradle"
// PLUGIN GRADLE EXTENSIONS END
for (def func : cdvPluginPostBuildExtras) {
func()
}

View File

@@ -0,0 +1,7 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
ext {
cdvMinSdkVersion = project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22
// Plugin gradle extensions can append to this to have code run at the end.
cdvPluginPostBuildExtras = []
cordovaConfig = [:]
}

View File

@@ -0,0 +1,8 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:amazon="http://schemas.amazon.com/apk/res/android">
<application android:usesCleartextTraffic="true">
</application>
</manifest>

View File

@@ -4,3 +4,15 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
include ':capacitor-camera'
project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
include ':capawesome-capacitor-file-picker'
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')

View File

@@ -20,3 +20,4 @@ org.gradle.jvmargs=-Xmx1536m
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
android.suppressUnsupportedCompileSdk=34

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

BIN
assets/icon-only.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -1,7 +1,7 @@
import type { CapacitorConfig } from '@capacitor/cli';
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'app.timesafari.app',
appId: 'app.timesafari',
appName: 'TimeSafari',
webDir: 'dist',
bundledWebRuntime: false,

17
ios/.gitignore vendored
View File

@@ -1,13 +1,16 @@
App/build
App/Pods
App/output
App/App/public
DerivedData
xcuserdata
App/Pods
App/*.xcodeproj/xcuserdata/
App/*.xcworkspace/xcuserdata/
App/*/public
# Generated Config files
App/*/capacitor.config.json
App/*/config.xml
# Cordova plugins for Capacitor
capacitor-cordova-ios-plugins
# Generated Config files
App/App/capacitor.config.json
App/App/config.xml
DerivedData

View File

@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
2BC611FE3D7967BDB623FF21 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0C2082015AEE6A0776A3EAB /* Pods_App.framework */; };
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 */; };
@@ -14,7 +15,6 @@
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 */
@@ -27,8 +27,10 @@
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; };
821226CEE4D47A540167CC8F /* Pods-Time Safari.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Time Safari.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Time Safari/Pods-Time Safari.debug.xcconfig"; sourceTree = "<group>"; };
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>"; };
E0C2082015AEE6A0776A3EAB /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EF03C3F99471948925ED5AC3 /* Pods-Time Safari.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Time Safari.release.xcconfig"; path = "Pods/Target Support Files/Pods-Time Safari/Pods-Time Safari.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 */
@@ -37,7 +39,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
2BC611FE3D7967BDB623FF21 /* Pods_App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -47,7 +49,7 @@
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
isa = PBXGroup;
children = (
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
E0C2082015AEE6A0776A3EAB /* Pods_App.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -90,6 +92,8 @@
children = (
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
821226CEE4D47A540167CC8F /* Pods-Time Safari.debug.xcconfig */,
EF03C3F99471948925ED5AC3 /* Pods-Time Safari.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
@@ -112,7 +116,7 @@
dependencies = (
);
name = App;
productName = App;
productName = "Time Safari";
productReference = 504EC3041FED79650016851F /* App.app */;
productType = "com.apple.product-type.application";
};
@@ -122,8 +126,8 @@
504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0920;
LastSwiftUpdateCheck = 920;
LastUpgradeCheck = 920;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
@@ -141,8 +145,6 @@
Base,
);
mainGroup = 504EC2FB1FED79650016851F;
packageReferences = (
);
productRefGroup = 504EC3051FED79650016851F /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -348,13 +350,14 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_TEAM = GM3FS5JQPH;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 0.4.4;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.app;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
@@ -368,12 +371,13 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_TEAM = GM3FS5JQPH;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.app;
MARKETING_VERSION = 0.4.4;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;

View File

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

View File

@@ -1,8 +0,0 @@
<?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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -1,14 +1,14 @@
{
"images" : [
"images": [
{
"filename" : "AppIcon-512@2x.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
"idiom": "universal",
"size": "1024x1024",
"filename": "AppIcon-512@2x.png",
"platform": "ios"
}
],
"info" : {
"author" : "xcode",
"version" : 1
"info": {
"author": "xcode",
"version": 1
}
}
}

View File

@@ -22,6 +22,10 @@
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Upload photos and scan friends' QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Upload photos for gifts</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@@ -45,14 +49,16 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>app.timesafari</string>
<key>CFBundleURLSchemes</key>
<array>
<string>timesafari</string>
</array>
</dict>
</array>
</dict>
</array></dict>
</plist>

View File

@@ -12,6 +12,10 @@ def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker'
end
target 'App' do

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

@@ -0,0 +1,52 @@
PODS:
- Capacitor (6.2.1):
- CapacitorCordova
- CapacitorApp (6.0.2):
- Capacitor
- CapacitorCamera (6.1.2):
- Capacitor
- CapacitorCordova (6.2.1)
- CapacitorFilesystem (6.0.3):
- Capacitor
- CapacitorShare (6.0.3):
- Capacitor
- CapawesomeCapacitorFilePicker (6.2.0):
- Capacitor
DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
- "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)"
EXTERNAL SOURCES:
Capacitor:
:path: "../../node_modules/@capacitor/ios"
CapacitorApp:
:path: "../../node_modules/@capacitor/app"
CapacitorCamera:
:path: "../../node_modules/@capacitor/camera"
CapacitorCordova:
:path: "../../node_modules/@capacitor/ios"
CapacitorFilesystem:
:path: "../../node_modules/@capacitor/filesystem"
CapacitorShare:
:path: "../../node_modules/@capacitor/share"
CapawesomeCapacitorFilePicker:
:path: "../../node_modules/@capawesome/capacitor-file-picker"
SPEC CHECKSUMS:
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
PODFILE CHECKSUM: 1e9280368fd410520414f5741bf8fdfe7847b965
COCOAPODS: 1.16.2

6131
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,25 @@
{
"name": "timesafari",
"version": "0.4.4",
"description": "TimeSafari Desktop Application",
"description": "Time Safari Application",
"author": {
"name": "TimeSafari Team"
"name": "Time Safari Team"
},
"scripts": {
"dev": "vite --config vite.config.dev.mts",
"serve": "vite preview",
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build",
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts",
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js",
"test-local": "npx playwright test -c playwright.config-local.ts --trace on",
"test-all": "npm run build && npx playwright test -c playwright.config-local.ts --trace on",
"test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
"test:prerequisites": "node scripts/check-prerequisites.js",
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
"test:mobile": "npm run build:capacitor && npm run test:android && npm run test:ios",
"test:android": "node scripts/test-android.js",
"test:ios": "node scripts/test-ios.js",
"check:android-device": "adb devices | grep -w 'device' || (echo 'No Android device connected' && exit 1)",
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
"clean:electron": "rimraf dist-electron",
"build:pywebview": "vite build --config vite.config.pywebview.mts",
"build:electron": "npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
@@ -21,6 +27,7 @@
"build:web": "vite build --config vite.config.web.mts",
"electron:dev": "npm run build && electron dist-electron",
"electron:start": "electron dist-electron",
"build:android": "rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
@@ -29,17 +36,26 @@
"pywebview:build": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py",
"pywebview:package-linux": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py",
"pywebview:package-win": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/Scripts/python -m PyInstaller --name TimeSafari --add-data 'dist;www' src/pywebview/main.py",
"pywebview:package-mac": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py"
"pywebview:package-mac": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py",
"fastlane:ios:beta": "cd ios && fastlane beta",
"fastlane:ios:release": "cd ios && fastlane release",
"fastlane:android:beta": "cd android && fastlane beta",
"fastlane:android:release": "cd android && fastlane release"
},
"dependencies": {
"@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0",
"@capacitor/camera": "^6.0.0",
"@capacitor/cli": "^6.2.0",
"@capacitor/core": "^6.2.0",
"@capacitor/filesystem": "^6.0.0",
"@capacitor/ios": "^6.2.0",
"@capacitor/share": "^6.0.3",
"@capawesome/capacitor-file-picker": "^6.2.0",
"@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1",
"@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6",
@@ -69,6 +85,7 @@
"dexie-export-import": "^4.1.4",
"did-jwt": "^7.4.7",
"did-resolver": "^4.1.0",
"dotenv": "^16.0.3",
"ethereum-cryptography": "^2.1.3",
"ethereumjs-util": "^7.1.5",
"jdenticon": "^3.2.0",
@@ -91,6 +108,7 @@
"reflect-metadata": "^0.1.14",
"register-service-worker": "^1.7.2",
"simple-vue-camera": "^1.1.3",
"sqlite3": "^5.1.7",
"stream-browserify": "^3.0.0",
"three": "^0.156.1",
"ua-parser-js": "^1.0.37",
@@ -104,12 +122,16 @@
"zod": "^3.24.2"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@playwright/test": "^1.45.2",
"@types/dom-webcodecs": "^0.1.7",
"@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2",
"@types/node": "^20.14.11",
"@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11",
"@types/sqlite3": "^3.1.11",
"@types/three": "^0.155.1",
"@types/ua-parser-js": "^0.7.39",
"@typescript-eslint/eslint-plugin": "^6.21.0",
@@ -138,7 +160,7 @@
},
"main": "./dist-electron/main.js",
"build": {
"appId": "org.timesafari.app",
"appId": "app.timesafari",
"productName": "TimeSafari",
"directories": {
"output": "dist-electron-packages"

5
pkgx.yaml Normal file
View File

@@ -0,0 +1,5 @@
dependencies:
- gradle
- java
# other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing).

View File

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

View File

@@ -1,3 +1,4 @@
eth_keys
pywebview
pyinstaller>=6.12.0
# For development

View File

@@ -0,0 +1,185 @@
/**
* @fileoverview Prerequisites checker for mobile development environment
*
* This script verifies that all necessary tools and configurations are in place
* for mobile app development, including both Android and iOS platforms.
*
* Features:
* - Validates development environment setup
* - Checks required command-line tools
* - Verifies Android SDK and device connectivity
* - Confirms iOS development tools and simulator status
*
* Prerequisites checked:
* - Node.js and npm installation
* - Gradle for Android builds
* - Xcode and command line tools for iOS
* - ANDROID_HOME environment variable
* - Android platform files
* - Connected Android devices/emulators
* - iOS platform files
* - Running iOS simulators
*
* Exit codes:
* - 0: All checks passed
* - 1: One or more checks failed
*
* @example
* // Run directly
* node scripts/check-prerequisites.js
*
* // Run via npm script
* npm run test:prerequisites
*
* @author TimeSafari Team
* @license MIT
*/
const { execSync } = require('child_process');
const { existsSync } = require('fs');
/**
* Checks if a command-line tool is available by attempting to run its --version command
*
* @param {string} command - The command to check (e.g., 'node', 'npm', 'gradle')
* @param {string} errorMessage - The error message to display if the command is not available
* @returns {boolean} - True if the command exists and is executable, false otherwise
*
* @example
* checkCommand('node', 'Node.js is required')
* // Returns true if node is available, false otherwise
*/
function checkCommand(command, errorMessage) {
try {
execSync(command + ' --version', { stdio: 'ignore' });
return true;
} catch (e) {
console.error(`${errorMessage}`);
return false;
}
}
/**
* Verifies Android development environment setup
*
* Checks for:
* 1. ANDROID_HOME environment variable
* 2. Android platform files in project
* 3. Connected Android devices or running emulators
*
* @returns {boolean} - True if Android setup is complete and valid, false otherwise
*
* @example
* if (!checkAndroidSetup()) {
* console.error('Android prerequisites not met');
* }
*/
function checkAndroidSetup() {
// Check ANDROID_HOME environment variable
// This is required for Android SDK tools access
if (!process.env.ANDROID_HOME) {
console.error('❌ ANDROID_HOME environment variable not set');
return false;
}
// Check if Android platform was added to the project
// The 'android' directory should exist if platform was added via 'npx cap add android'
if (!existsSync('android')) {
console.error('❌ Android platform not added. Run: npx cap add android');
return false;
}
// Check for connected devices or running emulators
// Uses ADB (Android Debug Bridge) to list connected devices
try {
const devices = execSync('adb devices').toString();
// Parse ADB output - looking for lines ending with 'device' (not 'offline' or 'unauthorized')
if (!devices.split('\n').slice(1).some(line => line.includes('device'))) {
console.error('❌ No Android devices connected');
return false;
}
} catch (e) {
console.error('❌ ADB not available');
return false;
}
return true;
}
/**
* Verifies iOS development environment setup
*
* Checks for:
* 1. iOS platform files in project
* 2. Running iOS simulators
* 3. Xcode command line tools availability
*
* @returns {boolean} - True if iOS setup is complete and valid, false otherwise
*
* @example
* if (!checkIosSetup()) {
* console.error('iOS prerequisites not met');
* }
*/
function checkIosSetup() {
// Check if iOS platform was added to the project
// The 'ios' directory should exist if platform was added via 'npx cap add ios'
if (!existsSync('ios')) {
console.error('❌ iOS platform not added. Run: npx cap add ios');
return false;
}
// Check for available and running iOS simulators
// Uses xcrun simctl to list simulator devices
try {
const simulators = execSync('xcrun simctl list devices available').toString();
if (!simulators.includes('Booted')) {
console.error('❌ No iOS simulator running');
return false;
}
} catch (e) {
console.error('❌ Xcode command line tools not available');
return false;
}
return true;
}
/**
* Main function to check all prerequisites for mobile development
*
* Verifies:
* 1. Required command line tools (node, npm, gradle, xcodebuild)
* 2. Android development setup
* 3. iOS development setup
*
* Exits with code 1 if any checks fail
*
* @example
* // Run from package.json script:
* // "test:prerequisites": "node scripts/check-prerequisites.js"
*/
function main() {
let success = true;
// Check required command line tools
// These are essential for building and testing the application
success &= checkCommand('node', 'Node.js is required');
success &= checkCommand('npm', 'npm is required');
success &= checkCommand('gradle', 'Gradle is required for Android builds');
success &= checkCommand('xcodebuild', 'Xcode is required for iOS builds');
// Check platform-specific development environments
success &= checkAndroidSetup();
success &= checkIosSetup();
// Exit with error if any checks failed
if (!success) {
process.exit(1);
}
console.log('✅ All prerequisites met!');
}
// Execute the checks
main();

22
scripts/copy-web-assets.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
# Clean the public directory
rm -rf android/app/src/main/assets/public/*
# Copy web assets
cp -r dist/* android/app/src/main/assets/public/
# Ensure the directory structure exists
mkdir -p android/app/src/main/assets/public/assets
# Copy the main index file
cp dist/index.html android/app/src/main/assets/public/
# Copy all assets
cp -r dist/assets/* android/app/src/main/assets/public/assets/
# Copy other necessary files
cp dist/favicon.ico android/app/src/main/assets/public/
cp dist/robots.txt android/app/src/main/assets/public/
echo "Web assets copied successfully!"

22
scripts/generate-icons.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
# Create directories if they don't exist
mkdir -p android/app/src/main/res/mipmap-mdpi
mkdir -p android/app/src/main/res/mipmap-hdpi
mkdir -p android/app/src/main/res/mipmap-xhdpi
mkdir -p android/app/src/main/res/mipmap-xxhdpi
mkdir -p android/app/src/main/res/mipmap-xxxhdpi
# Generate placeholder icons using ImageMagick
convert -size 48x48 xc:blue -gravity center -pointsize 20 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-mdpi/ic_launcher.png
convert -size 72x72 xc:blue -gravity center -pointsize 30 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-hdpi/ic_launcher.png
convert -size 96x96 xc:blue -gravity center -pointsize 40 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
convert -size 144x144 xc:blue -gravity center -pointsize 60 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
convert -size 192x192 xc:blue -gravity center -pointsize 80 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
# Copy to round versions
cp android/app/src/main/res/mipmap-mdpi/ic_launcher.png android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-hdpi/ic_launcher.png android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-xhdpi/ic_launcher.png android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png

View File

@@ -0,0 +1,132 @@
/**
* @fileoverview Runs mobile tests based on available platforms and devices
*
* This script intelligently detects available mobile platforms and their
* associated devices/simulators, then runs tests only for the available
* configurations. This allows for flexible testing across different
* development environments without failing when a platform is unavailable.
*
* Platform detection:
* - Android: Checks for SDK and connected devices/emulators
* - iOS: Checks for macOS, Xcode, and running simulators
*
* Features:
* - Smart platform detection
* - Graceful handling of unavailable platforms
* - Clear logging of test execution
* - Comprehensive error reporting
*
* Exit codes:
* - 0: Tests completed successfully on available platforms
* - 1: Tests failed or no platforms available
*
* @example
* // Run directly
* node scripts/run-available-mobile-tests.js
*
* // Run via npm script
* npm run test:mobile:available
*
* @requires child_process
* @requires fs
*
* @author TimeSafari Team
* @license MIT
*/
const { execSync } = require('child_process');
const { existsSync } = require('fs');
/**
* Executes mobile tests on available platforms
*
* This function performs the following steps:
* 1. Checks Android environment and device availability
* 2. Checks iOS environment and simulator availability (on macOS)
* 3. Runs tests on available platforms
* 4. Reports results and handles errors
*
* Platform-specific checks:
* Android:
* - ANDROID_HOME environment variable
* - Android platform files existence
* - Connected devices via ADB
*
* iOS:
* - macOS operating system
* - iOS platform files existence
* - Running simulators via xcrun
*
* @async
* @throws {Error} If tests fail or no platforms are available
*
* @example
* runAvailableMobileTests().catch(error => {
* console.error('Test execution failed:', error);
* process.exit(1);
* });
*/
async function runAvailableMobileTests() {
try {
// Check Android availability
// Requires both SDK (ANDROID_HOME) and platform files
const androidAvailable = existsSync('android') && process.env.ANDROID_HOME;
let androidDeviceAvailable = false;
if (androidAvailable) {
try {
// Check for connected devices using ADB
const devices = execSync('adb devices').toString();
// Parse ADB output for actually connected devices
// Filters out unauthorized or offline devices
androidDeviceAvailable = devices.split('\n').slice(1).some(line => line.includes('device'));
} catch (e) {
console.log('⚠️ Android SDK available but no devices connected');
}
}
// Check iOS availability
// Only possible on macOS with Xcode installed
const iosAvailable = process.platform === 'darwin' && existsSync('ios');
let iosSimulatorAvailable = false;
if (iosAvailable) {
try {
// Check for running simulators using xcrun
const simulators = execSync('xcrun simctl list devices available').toString();
// Look for 'Booted' state in simulator list
iosSimulatorAvailable = simulators.includes('Booted');
} catch (e) {
console.log('⚠️ iOS platform available but no simulator running');
}
}
// Execute tests for available platforms
if (androidDeviceAvailable) {
console.log('🤖 Running Android tests...');
// Run Android tests via npm script
execSync('npm run test:android', { stdio: 'inherit' });
}
if (iosSimulatorAvailable) {
console.log('🍎 Running iOS tests...');
// Run iOS tests via npm script
execSync('npm run test:ios', { stdio: 'inherit' });
}
// Error if no platforms are available for testing
if (!androidDeviceAvailable && !iosSimulatorAvailable) {
console.error('❌ No mobile platforms available for testing');
process.exit(1);
}
console.log('✅ Available mobile tests completed successfully');
} catch (error) {
// Handle any errors during test execution
console.error('❌ Mobile tests failed:', error);
process.exit(1);
}
}
// Execute the test runner
runAvailableMobileTests();

451
scripts/test-android.js Normal file
View File

@@ -0,0 +1,451 @@
/**
* @fileoverview Android test runner for Capacitor-based mobile app
*
* This script handles the build, installation, and testing of the Android app.
* It ensures the app is properly synced, built, installed on a device/emulator,
* and runs the test suite.
*
* Process flow:
* 1. Sync Capacitor project with latest web build
* 2. Build debug APK
* 3. Install APK on connected device/emulator
* 4. Run instrumented tests
*
* Prerequisites:
* - Android SDK installed and ANDROID_HOME set
* - Gradle installed and in PATH
* - Connected Android device or running emulator
* - Capacitor Android platform added to project
*
* Exit codes:
* - 0: Tests completed successfully
* - 1: Build, installation, or test failure
*
* @example
* // Run directly
* node scripts/test-android.js
*
* // Run via npm script
* npm run test:android
*
* @requires child_process
* @requires path
* @requires readline
*
* @author TimeSafari Team
* @license MIT
*/
const { execSync } = require('child_process');
const { join } = require('path');
const { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } = require('fs');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
// Format date as YYYY-MM-DD-HHMMSS
const getLogFileName = () => {
const now = new Date();
const date = now.toISOString().split('T')[0];
const time = now.toTimeString().split(' ')[0].replace(/:/g, '');
return `build_logs/android-build-${date}-${time}.log`;
};
// Create logger function
const createLogger = (logFile) => {
return (message) => {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(message);
appendFileSync(logFile, logMessage);
};
};
// Check for connected Android devices
const checkConnectedDevices = async (log) => {
log('🔍 Checking for Android devices...');
const devices = execSync('adb devices').toString();
const connectedDevices = devices.split('\n')
.slice(1)
.filter(line => line.includes('device'))
.map(line => line.split('\t')[0])
.filter(Boolean);
if (connectedDevices.length === 0) {
throw new Error('No Android devices or emulators connected. Please connect a device or start an emulator.');
}
log(`📱 Found ${connectedDevices.length} device(s): ${connectedDevices.join(', ')}`);
return connectedDevices;
};
// Verify Java installation
const verifyJavaInstallation = (log) => {
log('🔍 Checking Java...');
const javaHome = process.env.JAVA_HOME;
if (!existsSync(javaHome)) {
throw new Error(`Required Java not found at ${javaHome}. Please install OpenJDK.`);
}
log('✅ Java found');
};
// Generate test data using generate_data.ts
const generateTestData = async (log) => {
log('🔄 Generating test data...');
// Create .generated directory if it doesn't exist
if (!existsSync('.generated')) {
mkdirSync('.generated', { recursive: true });
}
try {
// Generate test data
const testData = {
CONTACT1_DID: "did:ethr:0x1943754837A09684Fd6380C1D80aa53E3F20E338",
CLAIM_ID: "01JPVVX7FH0EKQWTQY9HTXZQDZ"
};
const claimDetails = {
claim_id: "01JPVVX7FH0EKQWTQY9HTXZQDZ",
issuedAt: "2025-03-21T08:07:57ZZ",
issuer: "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F"
};
const contacts = [
{
did: "did:ethr:0x1943754837A09684Fd6380C1D80aa53E3F20E338",
name: "Test Contact"
}
];
// Write files
log('📝 Writing test data files...');
writeFileSync('.generated/test-env.json', JSON.stringify(testData, null, 2));
writeFileSync('.generated/claim_details.json', JSON.stringify(claimDetails, null, 2));
writeFileSync('.generated/contacts.json', JSON.stringify(contacts, null, 2));
// Verify files were written
log('✅ Verifying test data files...');
const files = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
for (const file of files) {
if (!existsSync(file)) {
throw new Error(`Failed to create ${file}`);
}
log(`✅ Created ${file}`);
}
log('✅ Test data generated successfully');
} catch (error) {
log(`❌ Failed to generate test data: ${error.message}`);
throw error;
}
};
// Parse shell environment file
const parseEnvFile = (filePath) => {
const content = readFileSync(filePath, 'utf8');
const env = {};
content.split('\n').forEach(line => {
const match = line.match(/^export\s+(\w+)="(.+)"$/);
if (match) {
env[match[1]] = match[2];
}
});
return env;
};
// Run individual deeplink test
const executeDeeplink = async (url, description, log) => {
log(`\n🔗 Testing deeplink: ${description}`);
log(`URL: ${url}`);
try {
// Stop the app before executing the deep link
execSync('adb shell am force-stop app.timesafari');
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`);
log(`✅ Successfully executed: ${description}`);
// Wait for app to load content
await new Promise(resolve => setTimeout(resolve, 3000));
// Wait for user confirmation before continuing
await question('\n⏎ Press Enter to continue to next test (or Ctrl+C to quit)...');
// Press Back button to ensure app is in consistent state
log(`📱 Sending keystroke (BACK) to device...`);
execSync('adb shell input keyevent KEYCODE_BACK');
// Small delay after keystroke
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
log(`❌ Failed to execute deeplink: ${description}`);
log(`Error: ${error.message}`);
throw error;
}
};
// Run all deeplink tests
const runDeeplinkTests = async (log) => {
log('🔗 Starting deeplink tests...');
try {
// Load test data
const testEnv = JSON.parse(readFileSync('.generated/test-env.json', 'utf8'));
const claimDetails = JSON.parse(readFileSync('.generated/claim_details.json', 'utf8'));
const contacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8'));
// Test URLs
const deeplinkTests = [
{
url: `timesafari://claim/${claimDetails.claim_id}`,
description: 'Claim view'
},
{
url: `timesafari://claim-cert/${claimDetails.claim_id}`,
description: 'Claim certificate view'
},
{
url: `timesafari://claim-add-raw/${claimDetails.claim_id}`,
description: 'Raw claim addition'
},
{
url: 'timesafari://did/test',
description: 'DID view with test identifier'
},
{
url: `timesafari://did/${testEnv.CONTACT1_DID}`,
description: 'DID view with contact DID'
},
{
url: `timesafari://contact-edit/${testEnv.CONTACT1_DID}`,
description: 'Contact editing'
},
{
url: `timesafari://contacts/import?contacts=${encodeURIComponent(JSON.stringify(contacts))}`,
description: 'Contacts import'
}
];
// Show test plan
log('\n📋 Test Plan:');
deeplinkTests.forEach((test, i) => {
log(`${i + 1}. ${test.description}`);
});
// Execute each test
let testsCompleted = 0;
for (const test of deeplinkTests) {
// Show progress
log(`\n📊 Progress: ${testsCompleted}/${deeplinkTests.length} tests completed`);
// Show upcoming test info
log('\n📱 NEXT TEST:');
log('------------------------');
log(`Description: ${test.description}`);
log(`URL: ${test.url}`);
log('------------------------');
await executeDeeplink(test.url, test.description, log);
testsCompleted++;
// If there are more tests, show the next one
if (testsCompleted < deeplinkTests.length) {
const nextTest = deeplinkTests[testsCompleted];
log('\n⏭ NEXT UP:');
log('------------------------');
log(`Next test will be: ${nextTest.description}`);
log(`URL: ${nextTest.url}`);
log('------------------------');
}
}
log('\n🎉 All deeplink tests completed successfully!');
rl.close(); // Close readline interface when done
} catch (error) {
log('❌ Deeplink tests failed');
rl.close(); // Close readline interface on error
throw error;
}
};
// Build web assets
const buildWebAssets = async (log) => {
log('🌐 Building web assets...');
execSync('rm -rf dist', { stdio: 'inherit' });
execSync('npm run build:web', { stdio: 'inherit' });
execSync('npm run build:capacitor', { stdio: 'inherit' });
log('✅ Web assets built successfully');
};
// Configure Android project
const configureAndroidProject = async (log) => {
log('📱 Syncing Capacitor project...');
execSync('npx cap sync android', { stdio: 'inherit' });
log('✅ Capacitor sync completed');
log('⚙️ Configuring Gradle properties...');
const gradleProps = 'android/gradle.properties';
// Create file if it doesn't exist
if (!existsSync(gradleProps)) {
execSync('touch android/gradle.properties');
}
// Check if line exists without using grep
const gradleContent = readFileSync(gradleProps, 'utf8');
if (!gradleContent.includes('android.suppressUnsupportedCompileSdk=34')) {
execSync('echo "android.suppressUnsupportedCompileSdk=34" >> android/gradle.properties');
log('✅ Added SDK suppression to gradle.properties');
} else {
log('✅ SDK suppression already configured in gradle.properties');
}
};
// Build and test Android project
const buildAndTestAndroid = async (log, env) => {
log('🏗️ Building Android project...');
// Kill and restart ADB server first
try {
log('🔄 Restarting ADB server...');
execSync('adb kill-server', { stdio: 'inherit' });
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s
execSync('adb start-server', { stdio: 'inherit' });
await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3s
// Verify device connection
const devices = execSync('adb devices').toString();
if (!devices.includes('\tdevice')) {
throw new Error('No devices connected after ADB restart');
}
log('✅ ADB server restarted successfully');
} catch (error) {
log(`⚠️ ADB restart failed: ${error.message}`);
log('Continuing with build process...');
}
// Clean build
log('🧹 Cleaning project...');
execSync('cd android && ./gradlew clean', { stdio: 'inherit', env });
log('✅ Gradle clean completed');
// Build
log('🏗️ Building project...');
execSync('cd android && ./gradlew build', { stdio: 'inherit', env });
log('✅ Gradle build completed');
// Run tests with retry
log('🧪 Running Android tests...');
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
try {
// Verify ADB connection before tests
execSync('adb devices', { stdio: 'inherit' });
// Run the tests
execSync('cd android && ./gradlew connectedAndroidTest', {
stdio: 'inherit',
env,
timeout: 60000 // 1 minute timeout
});
log('✅ Android tests completed');
return;
} catch (error) {
retryCount++;
log(`⚠️ Test attempt ${retryCount} failed: ${error.message}`);
if (retryCount < maxRetries) {
log('🔄 Restarting ADB and retrying...');
execSync('adb kill-server', { stdio: 'inherit' });
await new Promise(resolve => setTimeout(resolve, 2000));
execSync('adb start-server', { stdio: 'inherit' });
await new Promise(resolve => setTimeout(resolve, 3000));
} else {
throw new Error(`Android tests failed after ${maxRetries} attempts`);
}
}
}
};
// Run the app
const runAndroidApp = async (log, env) => {
log('📱 Running app on device...');
execSync('npx cap run android', { stdio: 'inherit', env });
log('✅ App launched successfully');
};
/**
* Runs the complete Android test suite including build, installation, and testing
*
* The function performs the following steps:
* 1. Checks for connected devices/emulators
* 2. Ensures correct Java version is used
* 3. Checks if app is already installed
* 4. Syncs the Capacitor project with latest build
* 5. Builds and runs instrumented Android tests
*
* @async
* @throws {Error} If any step in the build or test process fails
*
* @example
* runAndroidTests().catch(error => {
* console.error('Test execution failed:', error);
* process.exit(1);
* });
*/
async function runAndroidTests() {
// Create build_logs directory if it doesn't exist
if (!existsSync('build_logs')) {
mkdirSync('build_logs');
}
const logFile = getLogFileName();
const log = createLogger(logFile);
try {
log('🚀 Starting Android build and test process...');
// Generate test data first
await generateTestData(log);
await checkConnectedDevices(log);
await verifyJavaInstallation(log);
await buildWebAssets(log);
await configureAndroidProject(log);
const env = process.env;
await buildAndTestAndroid(log, env);
await runAndroidApp(log, env);
// Run deeplink tests after app is installed
await runDeeplinkTests(log);
log('🎉 Android build and test process completed successfully');
log(`📝 Full build log available at: ${logFile}`);
} catch (error) {
log(`❌ Android tests failed: ${error.message}`);
log(`📝 Check build log for details: ${logFile}`);
process.exit(1);
}
}
// Execute the test suite
runAndroidTests();
// Add cleanup handler for SIGINT
process.on('SIGINT', () => {
rl.close();
process.exit();
});

939
scripts/test-ios.js Normal file
View File

@@ -0,0 +1,939 @@
/**
* @fileoverview iOS test runner for Capacitor-based mobile app
*
* This script handles the build and testing of the iOS app using Xcode's
* command-line tools. It ensures the app is properly synced with the latest
* web build and runs the test suite on a specified iOS simulator.
*
* Process flow:
* 1. Clean and reset iOS platform (if needed)
* 2. Check prerequisites (Xcode, CocoaPods, Capacitor setup)
* 3. Sync Capacitor project with latest web build
* 4. Build app for iOS simulator
* 5. Run XCTest suite
*
* Prerequisites:
* - macOS operating system
* - Xcode installed with command line tools
* - iOS simulator available
* - Capacitor iOS platform added to project
* - Valid iOS development certificates
*
* Exit codes:
* - 0: Tests completed successfully
* - 1: Build or test failure
*
* @example
* // Run directly
* node scripts/test-ios.js
*
* // Run via npm script
* npm run test:ios
*
* @requires child_process
* @requires path
* @requires fs
*
* @author TimeSafari Team
* @license MIT
*/
const { execSync } = require('child_process');
const { join } = require('path');
const { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, readdirSync, statSync, accessSync } = require('fs');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const { constants } = require('fs');
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
// Make sure to close readline at the end
process.on('SIGINT', () => {
rl.close();
process.exit();
});
// Format date as YYYY-MM-DD-HHMMSS
const getLogFileName = () => {
const now = new Date();
const date = now.toISOString().split('T')[0];
const time = now.toTimeString().split(' ')[0].replace(/:/g, '');
return `build_logs/ios-build-${date}-${time}.log`;
};
// Create logger function
const createLogger = (logFile) => {
return (message) => {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(message);
appendFileSync(logFile, logMessage);
};
};
/**
* Clean up and reset iOS platform
* This function completely removes and recreates the iOS platform to ensure a fresh setup
* @param {function} log - Logging function
* @returns {boolean} - Success status
*/
const cleanIosPlatform = async (log) => {
log('🧹 Cleaning iOS platform (complete reset)...');
// Check for package.json and capacitor.config.ts/js
if (!existsSync('package.json')) {
log('⚠️ package.json not found. Are you in the correct directory?');
throw new Error('package.json not found. Cannot continue without project configuration.');
}
log('✅ package.json exists');
const capacitorConfigExists =
existsSync('capacitor.config.ts') ||
existsSync('capacitor.config.js') ||
existsSync('capacitor.config.json');
if (!capacitorConfigExists) {
log('⚠️ Capacitor config file not found');
log('Creating minimal capacitor.config.ts...');
try {
// Get app name from package.json
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
const appName = packageJson.name || 'App';
const appId = packageJson.build.appId || 'io.ionic.starter';
// Create a minimal capacitor config
const capacitorConfig = `
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: '${appId}',
appName: '${appName}',
webDir: 'dist',
bundledWebRuntime: false
};
export default config;
`.trim();
writeFileSync('capacitor.config.ts', capacitorConfig);
log('✅ Created capacitor.config.ts');
} catch (configError) {
log('⚠️ Failed to create Capacitor config file');
log('Please create a capacitor.config.ts file manually');
throw new Error('Capacitor configuration missing. Please configure manually.');
}
} else {
log('✅ Capacitor config exists');
}
// Check if the platform exists first
if (existsSync('ios')) {
log('🗑️ Removing existing iOS platform directory...');
try {
execSync('rm -rf ios', { stdio: 'inherit' });
log('✅ Existing iOS platform removed');
} catch (error) {
log(`⚠️ Error removing iOS platform: ${error.message}`);
log('⚠️ You may need to manually remove the ios directory');
return false;
}
}
// Rebuild web assets first to ensure they're available
log('🔄 Building web assets before adding iOS platform...');
try {
execSync('rm -rf dist', { stdio: 'inherit' });
execSync('npm run build:web', { stdio: 'inherit' });
execSync('npm run build:capacitor', { stdio: 'inherit' });
log('✅ Web assets built successfully');
} catch (error) {
log(`⚠️ Error building web assets: ${error.message}`);
log('⚠️ Continuing with platform addition, but it may fail if web assets are required');
}
// Add the platform back
log(' Adding iOS platform...');
try {
execSync('npx cap add ios', { stdio: 'inherit' });
log('✅ iOS platform added successfully');
// Verify critical files were created
if (!existsSync('ios/App/Podfile')) {
log('⚠️ Podfile was not created - something is wrong with the Capacitor setup');
return false;
}
if (!existsSync('ios/App/App/Info.plist')) {
log('⚠️ Info.plist was not created - something is wrong with the Capacitor setup');
return false;
}
log('✅ iOS platform setup verified - critical files exist');
return true;
} catch (error) {
log(`⚠️ Error adding iOS platform: ${error.message}`);
return false;
}
};
/**
* Check all prerequisites for iOS testing
* Verifies and attempts to install/initialize all required components
*/
const checkPrerequisites = async (log) => {
log('🔍 Checking prerequisites for iOS testing...');
// Check for macOS
if (process.platform !== 'darwin') {
throw new Error('iOS testing is only supported on macOS');
}
log('✅ Running on macOS');
// Verify Xcode installation
try {
const xcodeOutput = execSync('xcode-select -p').toString().trim();
log(`✅ Xcode command line tools found at: ${xcodeOutput}`);
} catch (error) {
log('⚠️ Xcode command line tools not found');
log('Please install Xcode from the App Store and run:');
log('xcode-select --install');
throw new Error('Xcode command line tools not found. Please install Xcode first.');
}
// Check Xcode version
try {
const xcodeVersionOutput = execSync('xcodebuild -version').toString().trim();
log(`✅ Xcode version: ${xcodeVersionOutput.split('\n')[0]}`);
} catch (error) {
log('⚠️ Unable to determine Xcode version');
}
// Check for CocoaPods
try {
const podVersionOutput = execSync('pod --version').toString().trim();
log(`✅ CocoaPods version: ${podVersionOutput}`);
} catch (error) {
log('⚠️ CocoaPods not found');
log('Attempting to install CocoaPods...');
try {
log('🔄 Installing CocoaPods via gem...');
execSync('gem install cocoapods', { stdio: 'inherit' });
log('✅ CocoaPods installed successfully');
} catch (gemError) {
log('⚠️ Failed to install CocoaPods via gem');
log('Please install CocoaPods manually:');
log('1. sudo gem install cocoapods');
log('2. brew install cocoapods');
throw new Error('CocoaPods installation failed. Please install manually.');
}
}
log('✅ All prerequisites for iOS testing are met');
return true;
};
// Check for iOS simulator
const checkSimulator = async (log) => {
log('🔍 Checking for iOS simulator...');
const simulatorsOutput = execSync('xcrun simctl list devices available -j').toString();
const simulatorsData = JSON.parse(simulatorsOutput);
// Get all available devices/simulators with their UDIDs
const allDevices = [];
// Process all runtime groups (iOS versions)
Object.entries(simulatorsData.devices).forEach(([runtime, devices]) => {
devices.forEach(device => {
allDevices.push({
name: device.name,
udid: device.udid,
state: device.state,
runtime: runtime,
isIphone: device.name.includes('iPhone'),
});
});
});
// Check for booted simulators first
const bootedDevices = allDevices.filter(device => device.state === 'Booted');
if (bootedDevices.length > 0) {
log(`📱 Found ${bootedDevices.length} running simulator(s): ${bootedDevices.map(d => d.name).join(', ')}`);
return bootedDevices;
}
// No booted devices found, try to boot one
log('⚠️ No running iOS simulator found. Attempting to boot one...');
// Prefer iPhone devices, especially newer models
const preferredDevices = [
'iPhone 15', 'iPhone 14', 'iPhone 13', 'iPhone 12', 'iPhone', // Prefer newer iPhones first
'iPad' // Then iPads if no iPhones available
];
let deviceToLaunch = null;
// Try to find a device from our preferred list
for (const preferredName of preferredDevices) {
const matchingDevices = allDevices.filter(device =>
device.name.includes(preferredName) && device.state === 'Shutdown');
if (matchingDevices.length > 0) {
// Sort by runtime to prefer newer iOS versions
matchingDevices.sort((a, b) => b.runtime.localeCompare(a.runtime));
deviceToLaunch = matchingDevices[0];
break;
}
}
// If no preferred device found, take any available device
if (!deviceToLaunch && allDevices.length > 0) {
const availableDevices = allDevices.filter(device => device.state === 'Shutdown');
if (availableDevices.length > 0) {
deviceToLaunch = availableDevices[0];
}
}
if (!deviceToLaunch) {
throw new Error('No available iOS simulators found. Please create a simulator in Xcode first.');
}
// Boot the selected simulator
log(`🚀 Booting iOS simulator: ${deviceToLaunch.name} (${deviceToLaunch.runtime})`);
execSync(`xcrun simctl boot ${deviceToLaunch.udid}`);
// Wait for simulator to fully boot
log('⏳ Waiting for simulator to boot completely...');
// Give the simulator time to fully boot before proceeding
await new Promise(resolve => setTimeout(resolve, 10000));
log(`✅ Successfully booted simulator: ${deviceToLaunch.name}`);
return [{ name: deviceToLaunch.name, udid: deviceToLaunch.udid }];
};
// Verify Xcode installation
const verifyXcodeInstallation = (log) => {
log('🔍 Checking Xcode installation...');
try {
execSync('xcode-select -p');
log('✅ Xcode command line tools found');
} catch (error) {
throw new Error('Xcode command line tools not found. Please install Xcode first.');
}
};
// Generate test data using generate_data.ts
const generateTestData = async (log) => {
log('\n🔍 DEBUG: Starting test data generation...');
// Check directory structure
log('📁 Current directory:', process.cwd());
log('📁 Directory contents:', require('fs').readdirSync('.'));
if (!existsSync('.generated')) {
log('📁 Creating .generated directory');
mkdirSync('.generated', { recursive: true });
}
try {
log('🔄 Attempting to run generate_data.ts...');
execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' });
log('✅ Test data generation completed');
// Verify and log generated files content
const requiredFiles = [
'.generated/test-env.json',
'.generated/claim_details.json',
'.generated/contacts.json'
];
log('\n📝 Verifying generated files:');
for (const file of requiredFiles) {
if (!existsSync(file)) {
log(`❌ Missing file: ${file}`);
} else {
const content = readFileSync(file, 'utf8');
log(`\n📄 Content of ${file}:`);
log(content);
try {
const parsed = JSON.parse(content);
if (file.includes('test-env.json')) {
log('🔑 CONTACT1_DID in test-env:', parsed.CONTACT1_DID);
}
if (file.includes('contacts.json')) {
log('👥 First contact DID:', parsed[0]?.did);
}
} catch (e) {
log(`❌ Error parsing ${file}:`, e);
}
}
}
} catch (error) {
log(`\n⚠️ Test data generation failed: ${error.message}`);
log('⚠️ Creating fallback test data...');
// Create fallback data with detailed logging
const fallbackTestEnv = {
"CONTACT1_DID": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B",
"APP_URL": "https://app.timesafari.example"
};
const fallbackContacts = [
{
"id": "contact1",
"name": "Test Contact",
"did": "did:ethr:0x35A71Ac3fA0A4D5a4903f10F0f7A3ac4034FaB5B"
}
];
log('\n📝 Writing fallback data:');
log('TestEnv:', JSON.stringify(fallbackTestEnv, null, 2));
log('Contacts:', JSON.stringify(fallbackContacts, null, 2));
writeFileSync('.generated/test-env.json', JSON.stringify(fallbackTestEnv, null, 2));
writeFileSync('.generated/contacts.json', JSON.stringify(fallbackContacts, null, 2));
// Verify fallback data was written
log('\n🔍 Verifying fallback data:');
try {
const writtenTestEnv = JSON.parse(readFileSync('.generated/test-env.json', 'utf8'));
const writtenContacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8'));
log('Written TestEnv:', writtenTestEnv);
log('Written Contacts:', writtenContacts);
} catch (e) {
log('❌ Error verifying fallback data:', e);
}
}
};
// Build web assets
const buildWebAssets = async (log) => {
log('🌐 Building web assets...');
execSync('rm -rf dist', { stdio: 'inherit' });
execSync('npm run build:web', { stdio: 'inherit' });
execSync('npm run build:capacitor', { stdio: 'inherit' });
log('✅ Web assets built successfully');
};
// Configure iOS project
const configureIosProject = async (log) => {
log('📱 Configuring iOS project...');
// Skip cap sync since we just did a clean platform add
log('✅ Using freshly created iOS platform');
// Register URL scheme for deeplink tests
log('🔗 Configuring URL scheme for deeplink tests...');
if (checkAndRegisterUrlScheme(log)) {
log('✅ URL scheme configuration completed');
} else {
log('⚠️ URL scheme could not be registered automatically');
log('⚠️ Deeplink tests may not work correctly');
}
log('⚙️ Installing CocoaPods dependencies...');
try {
// Try to run pod install normally first
log('🔄 Running "pod install" in ios/App directory...');
execSync('cd ios/App && pod install', { stdio: 'inherit' });
log('✅ CocoaPods installation completed');
} catch (error) {
// If that fails, provide detailed instructions
log(`⚠️ CocoaPods installation failed: ${error.message}`);
log('⚠️ Please ensure CocoaPods is installed correctly:');
log('1. If using system Ruby: "sudo gem install cocoapods"');
log('2. If using Homebrew Ruby: "brew install cocoapods"');
log('3. Then run: "cd ios/App && pod install"');
// Try to continue despite the error
log('⚠️ Attempting to continue with the build process...');
}
// Add information about iOS security dialogs
log('\n📱 iOS Security Dialog Information:');
log('⚠️ iOS will display security confirmation dialogs when testing deeplinks');
log('⚠️ This is a security feature of iOS and cannot be bypassed in normal testing');
log('⚠️ You will need to manually approve each deeplink test by clicking "Open" in the dialog');
log('⚠️ The app must be running in the foreground for deeplinks to work properly');
log('⚠️ If tests appear to hang, check if a security dialog is waiting for your confirmation');
};
// Build and test iOS project
const buildAndTestIos = async (log, simulator) => {
const simulatorName = simulator[0].name;
log('🏗️ Building iOS project...', simulator[0]);
execSync('cd ios/App && xcodebuild clean -workspace App.xcworkspace -scheme App', { stdio: 'inherit' });
log('✅ Xcode clean completed');
log(`🏗️ Building for simulator: ${simulatorName}`);
execSync(`cd ios/App && xcodebuild build -workspace App.xcworkspace -scheme App -destination "platform=iOS Simulator,OS=17.2,name=${simulatorName}"`, { stdio: 'inherit' });
log('✅ Xcode build completed');
// Check if the project is configured for testing by querying the scheme capabilities
try {
log(`🧪 Checking if scheme is configured for testing`);
const schemeInfo = execSync(`cd ios/App && xcodebuild -scheme App -showBuildSettings | grep TEST`).toString();
if (schemeInfo.includes('ENABLE_TESTABILITY = YES')) {
log(`🧪 Attempting to run tests on simulator: ${simulatorName}`);
try {
execSync(`cd ios/App && xcodebuild test -workspace App.xcworkspace -scheme App -destination "platform=iOS Simulator,name=${simulatorName}"`, { stdio: 'inherit' });
log('✅ iOS tests completed successfully');
} catch (testError) {
log(`⚠️ Tests failed or scheme not properly configured for testing: ${testError.message}`);
log('⚠️ This is normal if no test targets have been added to the project');
log('⚠️ Skipping test step and continuing with the app launch');
}
} else {
log('⚠️ Project does not have testing enabled in build settings');
log('⚠️ Skipping test step and continuing with the app launch');
}
} catch (error) {
log('⚠️ Unable to determine if testing is configured');
log('⚠️ Skipping test step and continuing with the app launch');
}
};
// Run the app
const runIosApp = async (log, simulator) => {
const simulatorName = simulator[0].name;
const simulatorUdid = simulator[0].udid;
log(`📱 Running app in simulator: ${simulatorName} (${simulatorUdid})...`);
// Use the --target parameter to specify the device directly, avoiding the UI prompt
execSync(`npx cap run ios --target="${simulatorUdid}"`, { stdio: 'inherit' });
log('✅ App launched successfully');
};
const validateTestData = (log) => {
log('\n=== VALIDATING TEST DATA ===');
const generateFreshTestData = () => {
log('\n🔄 Generating fresh test data...');
try {
// Ensure .generated directory exists
if (!existsSync('.generated')) {
mkdirSync('.generated', { recursive: true });
}
// Execute the generate_data.ts script synchronously
log('Running generate_data.ts...');
execSync('npx ts-node test-scripts/generate_data.ts', {
stdio: 'inherit',
encoding: 'utf8'
});
// Read and validate the generated files
const testEnvPath = '.generated/test-env.json';
const contactsPath = '.generated/contacts.json';
if (!existsSync(testEnvPath) || !existsSync(contactsPath)) {
throw new Error('Generated files not found after running generate_data.ts');
}
const testEnv = JSON.parse(readFileSync(testEnvPath, 'utf8'));
const contacts = JSON.parse(readFileSync(contactsPath, 'utf8'));
// Validate required fields
if (!testEnv.CONTACT1_DID) {
throw new Error('CONTACT1_DID missing from generated test data');
}
log('Generated test data:', {
testEnv: testEnv,
contacts: contacts
});
return { testEnv, contacts };
} catch (error) {
log('❌ Test data generation failed:', error);
throw error;
}
};
try {
// Try to read existing data or generate fresh data
const testEnvPath = '.generated/test-env.json';
const contactsPath = '.generated/contacts.json';
let testData;
// If either file is missing or invalid, generate fresh data
if (!existsSync(testEnvPath) || !existsSync(contactsPath)) {
testData = generateFreshTestData();
} else {
try {
const testEnv = JSON.parse(readFileSync(testEnvPath, 'utf8'));
const contacts = JSON.parse(readFileSync(contactsPath, 'utf8'));
// Validate required fields
if (!testEnv.CLAIM_ID || !testEnv.CONTACT1_DID) {
log('⚠️ Existing test data missing required fields, regenerating...');
testData = generateFreshTestData();
} else {
testData = { testEnv, contacts };
}
} catch (error) {
log('⚠️ Error reading existing test data, regenerating...');
testData = generateFreshTestData();
}
}
// Final validation of data
if (!testData.testEnv.CLAIM_ID || !testData.testEnv.CONTACT1_DID) {
throw new Error('Test data validation failed even after generation');
}
log('✅ Test data validated successfully');
log('📄 Test Environment:', JSON.stringify(testData.testEnv, null, 2));
return testData;
} catch (error) {
log(`❌ Test data validation failed: ${error.message}`);
throw error;
}
};
/**
* Run deeplink tests
* Optionally tests deeplinks if the test data is available
*
* @param {function} log - Logging function
* @returns {Promise<void>}
*/
const runDeeplinkTests = async (log) => {
log('\n=== Starting Deeplink Tests ===');
// Validate test data before proceeding
let testEnv, contacts;
try {
({ testEnv, contacts } = validateTestData(log));
} catch (error) {
log('❌ Cannot proceed with tests due to invalid test data');
log(`Error: ${error.message}`);
log('Please ensure test data is properly generated before running tests');
process.exit(1); // Exit with error code
}
// Now we can safely create the deeplink tests knowing we have valid data
const deeplinkTests = [
{
url: `timesafari://claim/${testEnv.CLAIM_ID}`,
description: 'Claim view'
},
{
url: `timesafari://claim-cert/${testEnv.CERT_ID || testEnv.CLAIM_ID}`,
description: 'Claim certificate view'
},
{
url: `timesafari://claim-add-raw/${testEnv.RAW_CLAIM_ID || testEnv.CLAIM_ID}`,
description: 'Raw claim addition'
},
{
url: 'timesafari://did/test',
description: 'DID view with test identifier'
},
{
url: `timesafari://did/${testEnv.CONTACT1_DID}`,
description: 'DID view with contact DID'
},
{
url: (() => {
if (!testEnv?.CONTACT1_DID) {
throw new Error('Cannot construct contact-edit URL: CONTACT1_DID is missing');
}
const url = `timesafari://contact-edit/${testEnv.CONTACT1_DID}`;
log('Created contact-edit URL:', url);
return url;
})(),
description: 'Contact editing'
},
{
url: `timesafari://contacts/import?contacts=${encodeURIComponent(JSON.stringify(contacts))}`,
description: 'Contacts import'
}
];
// Log the final test configuration
log('\n5. Final Test Configuration:');
deeplinkTests.forEach((test, i) => {
log(`\nTest ${i + 1}:`);
log(`Description: ${test.description}`);
log(`URL: ${test.url}`);
});
// Show instructions for iOS security dialogs
log('\n📱 IMPORTANT: iOS Security Dialog Instructions:');
log('1. Each deeplink test will trigger a security confirmation dialog');
log('2. You MUST click "Open" on each dialog to continue testing');
log('3. The app must be running in the FOREGROUND');
log('4. You will need to press Enter in this terminal after handling each dialog');
log('5. You can abort the testing process by pressing Ctrl+C\n');
// Ensure app is in foreground
log('⚠️ IMPORTANT: Please make sure the app is in the FOREGROUND now');
await question('Press Enter when the app is visible and in the foreground...');
try {
// Execute each test
let testsCompleted = 0;
let testsSkipped = 0;
for (const test of deeplinkTests) {
// Show upcoming test info before execution
log('\n📱 NEXT TEST:');
log('------------------------');
log(`Description: ${test.description}`);
log(`URL to test: ${test.url}`);
log('------------------------');
// Clear prompt for user action
await question('\n⏎ Press Enter to execute this test (or Ctrl+C to quit)...');
try {
log('🚀 Executing deeplink test...');
log('⚠️ iOS SECURITY DIALOG WILL APPEAR - Click "Open" to continue');
execSync(`xcrun simctl openurl booted "${test.url}"`, { stdio: 'pipe' });
log(`✅ Successfully executed: ${test.description}`);
testsCompleted++;
// Show progress
log(`\n📊 Progress: ${testsCompleted}/${deeplinkTests.length} tests completed`);
// If there are more tests, show the next one
if (testsCompleted < deeplinkTests.length) {
const nextTest = deeplinkTests[testsCompleted];
log('\n⏭ NEXT UP:');
log('------------------------');
log(`Next test will be: ${nextTest.description}`);
log(`URL: ${nextTest.url}`);
log('------------------------');
await question('\n⏎ Press Enter when ready for the next test...');
}
} catch (deeplinkError) {
const errorMessage = deeplinkError.message || '';
// Handle specific error for URL scheme not registered
if (errorMessage.includes('OSStatus error -10814') || errorMessage.includes('NSOSStatusErrorDomain, code=-10814')) {
log(`⚠️ URL scheme not properly handled: ${test.description}`);
testsSkipped++;
} else {
log(`⚠️ Failed to execute deeplink test: ${test.description}`);
log(`⚠️ Error: ${errorMessage}`);
}
log('⚠️ Continuing with next test...');
// Show next test info after error handling
if (testsCompleted + testsSkipped < deeplinkTests.length) {
const nextTest = deeplinkTests[testsCompleted + testsSkipped];
log('\n⏭ NEXT UP:');
log('------------------------');
log(`Next test will be: ${nextTest.description}`);
log(`URL: ${nextTest.url}`);
log('------------------------');
await question('\n⏎ Press Enter when ready for the next test...');
}
}
}
log('\n🎉 All deeplink tests completed!');
log(`✅ Successful: ${testsCompleted}`);
log(`⚠️ Skipped: ${testsSkipped}`);
if (testsSkipped > 0) {
log('\n📝 Note about skipped tests:');
log('1. The app needs to have the URL scheme registered in Info.plist');
log('2. The app needs to be rebuilt after registering the URL scheme');
log('3. The app must be running in the foreground for deeplink tests to work');
log('4. iOS security dialogs must be manually approved for each deeplink test');
log('5. If these conditions are met and tests still fail, check URL handling in the app code');
}
} catch (error) {
log(`❌ Deeplink tests setup failed: ${error.message}`);
log('⚠️ Deeplink tests might be unavailable or test data is missing');
}
};
// Check and register URL scheme if needed
const checkAndRegisterUrlScheme = (log) => {
log('🔍 Checking if URL scheme is registered in Info.plist...');
const infoPlistPath = 'ios/App/App/Info.plist';
// Check if Info.plist exists
if (!existsSync(infoPlistPath)) {
log('⚠️ Info.plist not found at: ' + infoPlistPath);
return false;
}
// Read Info.plist content
const infoPlistContent = readFileSync(infoPlistPath, 'utf8');
// Check if URL scheme is already registered
if (infoPlistContent.includes('<string>timesafari</string>')) {
log('✅ URL scheme "timesafari://" is already registered in Info.plist');
return true;
}
log('⚠️ URL scheme "timesafari://" is not registered in Info.plist');
log('⚠️ Attempting to register the URL scheme automatically...');
try {
// Look for the closing dict tag to insert our URL types
const closingDictIndex = infoPlistContent.lastIndexOf('</dict>');
if (closingDictIndex === -1) {
log('⚠️ Could not find closing dict tag in Info.plist');
return false;
}
// Create URL types entry
const urlTypesEntry = `
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>app.timesafari</string>
<key>CFBundleURLSchemes</key>
<array>
<string>timesafari</string>
</array>
</dict>
</array>`;
// Insert URL types entry before closing dict
const updatedPlistContent =
infoPlistContent.substring(0, closingDictIndex) +
urlTypesEntry +
infoPlistContent.substring(closingDictIndex);
// Write updated content back to Info.plist
const { writeFileSync } = require('fs');
writeFileSync(infoPlistPath, updatedPlistContent, 'utf8');
log('✅ URL scheme "timesafari://" registered in Info.plist');
log('⚠️ You will need to rebuild the app for changes to take effect');
return true;
} catch (error) {
log(`⚠️ Failed to register URL scheme: ${error.message}`);
return false;
}
};
// Helper function to get the app identifier from package.json or capacitor config
const getAppIdentifier = () => {
try {
// Try to read from capacitor.config.ts/js/json
if (existsSync('capacitor.config.json')) {
const config = JSON.parse(readFileSync('capacitor.config.json', 'utf8'));
return config.appId;
}
if (existsSync('capacitor.config.js')) {
// We can't directly require the file, but we can try to extract the appId
const content = readFileSync('capacitor.config.js', 'utf8');
const match = content.match(/appId:\s*['"]([^'"]+)['"]/);
if (match && match[1]) return match[1];
}
if (existsSync('capacitor.config.ts')) {
// Similar approach for TypeScript
const content = readFileSync('capacitor.config.ts', 'utf8');
const match = content.match(/appId:\s*['"]([^'"]+)['"]/);
if (match && match[1]) return match[1];
}
// Fall back to package.json
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
if (packageJson.capacitor && packageJson.capacitor.appId) {
return packageJson.capacitor.appId;
}
// Default fallback
return 'app.timesafari';
} catch (error) {
console.error('Error getting app identifier:', error);
return 'app.timesafari'; // Default fallback
}
};
/**
* Runs the complete iOS test suite including build and testing
*
* The function performs the following steps:
* 1. Cleans and resets the iOS platform
* 2. Verifies prerequisites and project setup
* 3. Syncs the Capacitor project with latest web build
* 4. Builds the app using xcodebuild
* 5. Optionally runs tests if configured
* 6. Launches the app in the simulator
*
* If no simulator is running, it automatically selects and boots one.
*
* @async
* @throws {Error} If any step in the build process fails
*
* @example
* runIosTests().catch(error => {
* console.error('Test execution failed:', error);
* process.exit(1);
* });
*/
async function runIosTests() {
// Create build_logs directory if it doesn't exist
if (!existsSync('build_logs')) {
mkdirSync('build_logs');
}
const logFile = getLogFileName();
const log = createLogger(logFile);
try {
log('🚀 Starting iOS build and test process...');
// Clean and reset iOS platform first
const cleanSuccess = await cleanIosPlatform(log);
if (!cleanSuccess) {
throw new Error('Failed to clean and reset iOS platform. Please check the logs for details.');
}
// Check prerequisites
await checkPrerequisites(log);
// Generate test data
await generateTestData(log);
// Verify Xcode installation
verifyXcodeInstallation(log);
// Check for simulator or boot one if needed
const simulator = await checkSimulator(log);
// Configure iOS project
await configureIosProject(log);
// Build and test using the selected simulator
await buildAndTestIos(log, simulator);
// Run the app in the simulator
await runIosApp(log, simulator);
// Run deeplink tests after app is installed
await runDeeplinkTests(log);
log('🎉 iOS build and test process completed successfully');
log(`📝 Full build log available at: ${logFile}`);
} catch (error) {
log(`❌ iOS tests failed: ${error.message}`);
log(`📝 Check build log for details: ${logFile}`);
process.exit(1);
}
}
// Execute the test suite
runIosTests();

View File

@@ -143,7 +143,8 @@
<!--
This "group" of "modal" is the prompt for an answer.
Set "type" as follows: "confirm" for yes/no, and "notification" ones: "-permission", "-mute", "-off"
Set "type" as follows: "confirm" for yes/no, and "notification" ones:
"-permission", "-mute", "-off"
-->
<NotificationGroup group="modal">
<div class="fixed z-[100] top-0 inset-x-0 w-full">
@@ -331,6 +332,7 @@
import { Vue, Component } from "vue-facing-decorator";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
import { NotificationIface } from "./constants/app";
import { logger } from "./utils/logger";
interface Settings {
notifyingNewActivityTime?: string;
@@ -344,38 +346,38 @@ export default class App extends Vue {
stopAsking = false;
// created() {
// console.log(
// logger.log(
// "Component created: Reactivity set up.",
// window.location.pathname,
// );
// }
// beforeCreate() {
// console.log("Component beforeCreate: Instance initialized.");
// logger.log("Component beforeCreate: Instance initialized.");
// }
// beforeMount() {
// console.log("Component beforeMount: Template is about to be rendered.");
// logger.log("Component beforeMount: Template is about to be rendered.");
// }
// mounted() {
// console.log("Component mounted: Template is now rendered.");
// logger.log("Component mounted: Template is now rendered.");
// }
// beforeUpdate() {
// console.log("Component beforeUpdate: DOM is about to be updated.");
// logger.log("Component beforeUpdate: DOM is about to be updated.");
// }
// updated() {
// console.log("Component updated: DOM has been updated.");
// logger.log("Component updated: DOM has been updated.");
// }
// beforeUnmount() {
// console.log("Component beforeUnmount: Cleaning up before removal.");
// logger.log("Component beforeUnmount: Cleaning up before removal.");
// }
// unmounted() {
// console.log("Component unmounted: Component removed from the DOM.");
// logger.log("Component unmounted: Component removed from the DOM.");
// }
truncateLongWords(sentence: string) {
@@ -388,42 +390,42 @@ export default class App extends Vue {
async turnOffNotifications(
notification: NotificationIface,
): Promise<boolean> {
console.log("Starting turnOffNotifications...");
logger.log("Starting turnOffNotifications...");
let subscription: PushSubscriptionJSON | null = null;
let allGoingOff = false;
try {
console.log("Retrieving settings for the active account...");
logger.log("Retrieving settings for the active account...");
const settings: Settings = await retrieveSettingsForActiveAccount();
console.log("Retrieved settings:", settings);
logger.log("Retrieved settings:", settings);
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
const notifyingReminder = !!settings?.notifyingReminderTime;
if (!notifyingNewActivity || !notifyingReminder) {
allGoingOff = true;
console.log("Both notifications are being turned off.");
logger.log("Both notifications are being turned off.");
}
console.log("Checking service worker readiness...");
logger.log("Checking service worker readiness...");
await navigator.serviceWorker?.ready
.then((registration) => {
console.log("Service worker is ready. Fetching subscription...");
logger.log("Service worker is ready. Fetching subscription...");
return registration.pushManager.getSubscription();
})
.then(async (subscript: PushSubscription | null) => {
if (subscript) {
subscription = subscript.toJSON();
console.log("PushSubscription retrieved:", subscription);
logger.log("PushSubscription retrieved:", subscription);
if (allGoingOff) {
console.log("Unsubscribing from push notifications...");
logger.log("Unsubscribing from push notifications...");
await subscript.unsubscribe();
console.log("Successfully unsubscribed.");
logger.log("Successfully unsubscribed.");
}
} else {
logConsoleAndDb("Subscription object is not available.");
console.log("No subscription found.");
logger.log("No subscription found.");
}
})
.catch((error) => {
@@ -432,11 +434,11 @@ export default class App extends Vue {
JSON.stringify(error),
true,
);
console.error("Error during subscription fetch:", error);
logger.error("Error during subscription fetch:", error);
});
if (!subscription) {
console.log("No subscription available. Notifying user...");
logger.log("No subscription available. Notifying user...");
this.$notify(
{
group: "alert",
@@ -446,7 +448,7 @@ export default class App extends Vue {
},
5000,
);
console.log("Exiting as there is no subscription to process.");
logger.log("Exiting as there is no subscription to process.");
return true;
}
@@ -455,12 +457,12 @@ export default class App extends Vue {
};
if (!allGoingOff) {
serverSubscription["notifyType"] = notification.title;
console.log(
logger.log(
`Server subscription updated with notifyType: ${notification.title}`,
);
}
console.log("Sending unsubscribe request to the server...");
logger.log("Sending unsubscribe request to the server...");
const pushServerSuccess = await fetch("/web-push/unsubscribe", {
method: "POST",
headers: {
@@ -475,9 +477,9 @@ export default class App extends Vue {
`Push server failed: ${response.status} ${errorBody}`,
true,
);
console.error("Push server error response:", errorBody);
logger.error("Push server error response:", errorBody);
}
console.log(`Server response status: ${response.status}`);
logger.log(`Server response status: ${response.status}`);
return response.ok;
})
.catch((error) => {
@@ -485,14 +487,14 @@ export default class App extends Vue {
"Push server communication failed: " + JSON.stringify(error),
true,
);
console.error("Error during server communication:", error);
logger.error("Error during server communication:", error);
return false;
});
const message = pushServerSuccess
? "Notification is off."
: "Notification is still on. Try to turn it off again.";
console.log("Server response processed. Message:", message);
logger.log("Server response processed. Message:", message);
this.$notify(
{
@@ -505,11 +507,11 @@ export default class App extends Vue {
);
if (notification.callback) {
console.log("Executing notification callback...");
logger.log("Executing notification callback...");
notification.callback(pushServerSuccess);
}
console.log(
logger.log(
"Completed turnOffNotifications with success:",
pushServerSuccess,
);
@@ -519,7 +521,7 @@ export default class App extends Vue {
"Error turning off notifications: " + JSON.stringify(error),
true,
);
console.error("Critical error in turnOffNotifications:", error);
logger.error("Critical error in turnOffNotifications:", error);
this.$notify(
{

View File

@@ -0,0 +1,265 @@
<template>
<li>
<!-- Last viewed separator -->
<div
v-if="record.jwtId == lastViewedClaimId"
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-6 font-bold text-sm"
>
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
You've already seen all the following
</span>
</div>
<div
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">
<EntityIcon
:entity-id="record.issuerDid"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</div>
<div v-else>
<font-awesome
icon="person-circle-question"
class="text-slate-300 text-[2rem]"
/>
</div>
<div>
<h3 class="font-semibold">
{{ record.issuer.known ? record.issuer.displayName : "" }}
</h3>
<p class="ms-auto text-xs text-slate-500 italic">
{{ friendlyDate }}
</p>
</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 -->
<div
v-if="record.image"
class="bg-cover mb-6 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
:style="`background-image: url(${record.image});`"
>
<a
class="block bg-slate-100/50 backdrop-blur-md px-6 py-4 cursor-pointer"
@click="$emit('viewImage', record.image)"
>
<img
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
:src="record.image"
alt="Activity image"
@load="$emit('cacheImage', record.image)"
/>
</a>
</div>
<div
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mb-5"
>
<!-- Source -->
<div
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>
<!-- Project Icon -->
<div v-if="record.providerPlanName">
<ProjectIcon
:entity-id="record.providerPlanName"
:icon-size="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</div>
<!-- Identicon for DIDs -->
<div v-else-if="record.agentDid">
<EntityIcon
:entity-id="record.agentDid"
:profile-image-url="record.issuer.profileImageUrl"
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
/>
</div>
<!-- Unknown Person -->
<div v-else>
<font-awesome
icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]"
/>
</div>
</div>
</div>
<div
v-if="record.providerPlanName || record.giver.known"
class="text-xs mt-2 truncate"
>
<font-awesome
:icon="record.providerPlanName ? 'users' : 'user'"
class="fa-fw text-slate-400"
/>
{{ record.providerPlanName || record.giver.displayName }}
</div>
</div>
<!-- Arrow -->
<div
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 pe-[15px]">
{{ fetchAmount }}
</div>
<div class="flex items-center">
<hr
class="grow border-t-[18px] sm:border-t-[24px] border-slate-300"
/>
<div
class="shrink-0 w-0 h-0 border border-slate-300 border-t-[20px] sm:border-t-[25px] border-t-transparent border-b-[20px] sm:border-b-[25px] border-b-transparent border-s-[27px] sm:border-s-[34px] border-e-0"
></div>
</div>
</div>
<!-- Destination -->
<div
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>
<!-- Project Icon -->
<div v-if="record.recipientProjectName">
<ProjectIcon
:entity-id="record.recipientProjectName"
:icon-size="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</div>
<!-- Identicon for DIDs -->
<div v-else-if="record.recipientDid">
<EntityIcon
:entity-id="record.recipientDid"
:profile-image-url="record.receiver.profileImageUrl"
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
/>
</div>
<!-- Unknown Person -->
<div v-else>
<font-awesome
icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]"
/>
</div>
</div>
</div>
<div
v-if="record.recipientProjectName || record.receiver.known"
class="text-xs mt-2 truncate"
>
<font-awesome
:icon="record.recipientProjectName ? 'users' : 'user'"
class="fa-fw text-slate-400"
/>
{{ record.recipientProjectName || record.receiver.displayName }}
</div>
</div>
</div>
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
{{ description }}
</a>
</p>
</div>
</li>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "../types";
import EntityIcon from "./EntityIcon.vue";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
import { containsHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue";
@Component({
components: {
EntityIcon,
ProjectIcon,
},
})
export default class ActivityListItem extends Vue {
@Prop() record!: GiveRecordWithContactInfo;
@Prop() lastViewedClaimId?: string;
@Prop() isRegistered!: boolean;
@Prop() activeDid!: string;
@Prop() confirmerIdList?: string[];
get fetchAmount(): string {
const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
const amount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: "";
return amount;
}
get description(): string {
const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
return `${claim.description}`;
}
private displayAmount(code: string, amt: number) {
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
}
private currencyShortWordForCode(unitCode: string, single: boolean) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}
get canConfirm(): boolean {
if (!this.isRegistered) return false;
if (!isGiveClaimType(this.record.fullClaim?.["@type"])) return false;
if (this.confirmerIdList?.includes(this.activeDid)) return false;
if (this.record.issuerDid === this.activeDid) return false;
if (containsHiddenDid(this.record.fullClaim)) return false;
return true;
}
handleConfirmClick() {
if (!this.canConfirm) {
notifyWhyCannotConfirm(
this.$notify,
this.isRegistered,
this.record.fullClaim?.["@type"],
this.record,
this.activeDid,
this.confirmerIdList,
);
return;
}
this.$emit("confirmClaim", this.record);
}
get friendlyDate(): string {
const date = new Date(this.record.issuedAt);
return date.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
}
}
</script>

View File

@@ -0,0 +1,196 @@
/** * Data Export Section Component * * Provides UI and functionality for
exporting user data and backing up identifier seeds. * Includes buttons for seed
backup and database export, with platform-specific download instructions. * *
@component * @displayName DataExportSection * @example * ```vue *
<DataExportSection :active-did="currentDid" />
* ``` */
<template>
<div
id="sectionDataExport"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
>
<div class="mb-2 font-bold">Data Export</div>
<router-link
v-if="activeDid"
:to="{ name: 'seed-backup' }"
class="block w-full text-center text-md 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 mt-2"
>
Backup Identifier Seed
</router-link>
<button
:class="computedStartDownloadLinkClassNames()"
class="block w-full text-center text-md 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"
@click="exportDatabase()"
>
Download Settings & Contacts
<br />
(excluding Identifier Data)
</button>
<a
ref="downloadLink"
:class="computedDownloadLinkClassNames()"
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
>
If no download happened yet, click again here to download now.
</a>
<div v-if="platformCapabilities.needsFileHandlingInstructions" class="mt-4">
<p>
After the download, you can save the file in your preferred storage
location.
</p>
<ul>
<li
v-if="platformCapabilities.isIOS"
class="list-disc list-outside ml-4"
>
On iOS: You will be prompted to choose a location to save your backup
file.
</li>
<li
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
class="list-disc list-outside ml-4"
>
On Android: You will be prompted to choose a location to save your
backup file.
</li>
</ul>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app";
import { db } from "../db/index";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import {
PlatformService,
PlatformCapabilities,
} from "../services/PlatformService";
/**
* @vue-component
* Data Export Section Component
* Handles database export and seed backup functionality with platform-specific behavior
*/
@Component
export default class DataExportSection extends Vue {
/**
* Notification function injected by Vue
* Used to show success/error messages to the user
*/
$notify!: (notification: NotificationIface, timeout?: number) => void;
/**
* Active DID (Decentralized Identifier) of the user
* Controls visibility of seed backup option
* @required
*/
@Prop({ required: true }) readonly activeDid!: string;
/**
* URL for the database export download
* Created and revoked dynamically during export process
* Only used in web platform
*/
downloadUrl = "";
/**
* Platform service instance for platform-specific operations
*/
private platformService: PlatformService =
PlatformServiceFactory.getInstance();
/**
* Platform capabilities for the current platform
*/
private get platformCapabilities(): PlatformCapabilities {
return this.platformService.getCapabilities();
}
/**
* Lifecycle hook to clean up resources
* Revokes object URL when component is unmounted (web platform only)
*/
beforeUnmount() {
if (this.downloadUrl && this.platformCapabilities.hasFileDownload) {
URL.revokeObjectURL(this.downloadUrl);
}
}
/**
* Exports the database to a JSON file
* Uses platform-specific methods for saving the exported data
* Shows success/error notifications to user
*
* @throws {Error} If export fails
* @emits {Notification} Success or error notification
*/
public async exportDatabase() {
try {
const blob = await db.export({ prettyJson: true });
const fileName = `${db.name}-backup.json`;
if (this.platformCapabilities.hasFileDownload) {
// Web platform: Use download link
this.downloadUrl = URL.createObjectURL(blob);
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
downloadAnchor.href = this.downloadUrl;
downloadAnchor.download = fileName;
downloadAnchor.click();
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to app directory
const content = await blob.text();
await this.platformService.writeAndShareFile(fileName, content);
}
this.$notify(
{
group: "alert",
type: "success",
title: "Export Successful",
text: this.platformCapabilities.hasFileDownload
? "See your downloads directory for the backup. It is in the Dexie format."
: "Please choose a location to save your backup file.",
},
-1,
);
} catch (error) {
logger.error("Export Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Export Error",
text: "There was an error exporting the data.",
},
3000,
);
}
}
/**
* Computes class names for the initial download button
* @returns Object with 'hidden' class when download is in progress (web platform only)
*/
public computedStartDownloadLinkClassNames() {
return {
hidden: this.downloadUrl && this.platformCapabilities.hasFileDownload,
};
}
/**
* Computes class names for the secondary download link
* @returns Object with 'hidden' class when no download is available or not on web platform
*/
public computedDownloadLinkClassNames() {
return {
hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload,
};
}
}
</script>

View File

@@ -1,4 +1,5 @@
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="w-fit" v-html="generateIcon()"></div>
</template>
<script lang="ts">

View File

@@ -162,7 +162,7 @@ export default class GiftedDialog extends Vue {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error retrieving settings from database:", err);
logger.error("Error retrieving settings from database:", err);
this.$notify(
{
group: "alert",
@@ -311,7 +311,7 @@ export default class GiftedDialog extends Vue {
this.isGiveCreationError(result.response)
) {
const errorMessage = this.getGiveCreationErrorMessage(result);
console.error("Error with give creation result:", result);
logger.error("Error with give creation result:", result);
this.$notify(
{
group: "alert",
@@ -337,7 +337,7 @@ export default class GiftedDialog extends Vue {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error with give recordation caught:", error);
logger.error("Error with give recordation caught:", error);
const errorMessage =
error.userMessage ||
serverMessageForUser(error) ||

View File

@@ -28,7 +28,7 @@
:src="imageUrl"
class="max-h-[calc(100vh-5rem)] w-full h-full object-contain"
alt="expanded shared content"
@click.stop
@click="close"
/>
</div>
</div>
@@ -74,7 +74,7 @@ export default class ImageViewer extends Vue {
}
}
} catch (error) {
console.warn("Share failed, opening in new tab:", error);
logger.warn("Share failed, opening in new tab:", error);
window.open(this.imageUrl, "_blank");
}
}

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>
<div ref="scrollContainer">
<slot />
@@ -8,13 +14,51 @@
<script lang="ts">
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
export default class InfiniteScroll extends Vue {
/** Distance in pixels from the bottom at which to trigger the reached-bottom event */
@Prop({ default: 200 })
readonly distance!: number;
/** Intersection Observer instance for detecting scroll position */
private observer!: IntersectionObserver;
/** Flag to track initial render state */
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() {
if (!this.observer) {
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() {
if (this.observer) {
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")
handleIntersection(entries: IntersectionObserverEntry[]) {
const entry = entries[0];
if (entry.isIntersecting) {
return true;
if (entry.isIntersecting && !this.isLoading) {
// 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;
}

View File

@@ -61,7 +61,10 @@
</div>
<div class="flex justify-center">
<!-- always have at least one refresh button even without members in case the organizer changes the password -->
<!--
always have at least one refresh button even without members in case the organizer
changes the password
-->
<button
class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
title="Refresh members list"

Some files were not shown because too many files have changed in this diff Show More