Browse Source

refactor: improve build configuration and code organization

- Add build scripts for Android and iOS platforms
- Remove duplicate web implementation (src/web.ts)
- Add proper TypeScript configuration
- Add module documentation to index.ts
- Clean up package.json scripts

This commit improves the project structure and build process by:
1. Adding dedicated build scripts for native platforms
2. Removing redundant web implementation
3. Adding proper TypeScript configuration with strict mode
4. Improving code documentation
5. Organizing package.json scripts

The changes maintain backward compatibility while improving
the development experience and code quality.
master
Matthew Raymer 4 days ago
parent
commit
71e0f297ff
  1. 36
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 23
      .github/ISSUE_TEMPLATE/feature_request.md
  3. 28
      .github/pull_request_template.md
  4. 131
      CHANGELOG.md
  5. 116
      CONTRIBUTING.md
  6. 17
      CapacitorDailyNotification.podspec
  7. 21
      LICENSE
  8. 510
      README.md
  9. 205
      SECURITY.md
  10. 101
      android/.gitignore
  11. 2
      android/app/.gitignore
  12. 67
      android/app/build.gradle
  13. 14
      android/app/capacitor.build.gradle
  14. 21
      android/app/proguard-rules.pro
  15. 26
      android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
  16. 44
      android/app/src/main/AndroidManifest.xml
  17. 39
      android/app/src/main/java/README.md
  18. 5
      android/app/src/main/java/com/example/app/MainActivity.java
  19. 160
      android/app/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java
  20. 51
      android/app/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java
  21. 52
      android/app/src/main/java/index.ts
  22. BIN
      android/app/src/main/res/drawable-land-hdpi/splash.png
  23. BIN
      android/app/src/main/res/drawable-land-mdpi/splash.png
  24. BIN
      android/app/src/main/res/drawable-land-xhdpi/splash.png
  25. BIN
      android/app/src/main/res/drawable-land-xxhdpi/splash.png
  26. BIN
      android/app/src/main/res/drawable-land-xxxhdpi/splash.png
  27. BIN
      android/app/src/main/res/drawable-port-hdpi/splash.png
  28. BIN
      android/app/src/main/res/drawable-port-mdpi/splash.png
  29. BIN
      android/app/src/main/res/drawable-port-xhdpi/splash.png
  30. BIN
      android/app/src/main/res/drawable-port-xxhdpi/splash.png
  31. BIN
      android/app/src/main/res/drawable-port-xxxhdpi/splash.png
  32. 34
      android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  33. 170
      android/app/src/main/res/drawable/ic_launcher_background.xml
  34. BIN
      android/app/src/main/res/drawable/splash.png
  35. 12
      android/app/src/main/res/layout/activity_main.xml
  36. 5
      android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  37. 5
      android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  38. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  39. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  40. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  41. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  42. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  43. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  44. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  45. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  46. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  47. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  48. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  49. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  50. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  51. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  52. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  53. 4
      android/app/src/main/res/values/ic_launcher_background.xml
  54. 7
      android/app/src/main/res/values/strings.xml
  55. 22
      android/app/src/main/res/values/styles.xml
  56. 5
      android/app/src/main/res/xml/file_paths.xml
  57. 18
      android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
  58. 77
      android/build.gradle
  59. 3
      android/capacitor.settings.gradle
  60. 22
      android/gradle.properties
  61. BIN
      android/gradle/wrapper/gradle-wrapper.jar
  62. 6
      android/gradle/wrapper/gradle-wrapper.properties
  63. 244
      android/gradlew
  64. 92
      android/gradlew.bat
  65. 10
      android/settings.gradle
  66. 17
      android/variables.gradle
  67. 12
      capacitor.config.ts
  68. 259
      examples/advanced-usage.ts
  69. 262
      examples/enterprise-usage.ts
  70. 165
      examples/usage.ts
  71. 109
      ios/Plugin/DailyNotificationPlugin.swift
  72. 39
      ios/Plugin/README.md
  73. 58
      ios/Plugin/index.ts
  74. 26
      jest.config.js
  75. 6416
      package-lock.json
  76. 43
      package.json
  77. 146
      scripts/build-native.sh
  78. 123
      scripts/check-environment.js
  79. 1
      scripts/setup-gradle.sh
  80. 72
      scripts/setup-native.js
  81. 121
      src/daily-notification.ts
  82. 115
      src/definitions.ts
  83. 6
      src/index.ts
  84. 45
      src/web.ts
  85. 117
      src/web/index.ts
  86. 201
      tests/advanced-scenarios.test.ts
  87. 154
      tests/daily-notification.test.ts
  88. 326
      tests/edge-cases.test.ts
  89. 288
      tests/enterprise-scenarios.test.ts
  90. 50
      tests/setup.ts
  91. 16
      tsconfig.json
  92. 0
      www/index.html

36
.github/ISSUE_TEMPLATE/bug_report.md

@ -0,0 +1,36 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS: [e.g. iOS, Android]
- Version [e.g. 1.0.0]
- Device: [e.g. iPhone 12, Samsung Galaxy S21]
- Capacitor Version: [e.g. 5.0.0]
**Additional context**
Add any other context about the problem here.
**Logs**
If applicable, add logs to help explain your problem.

23
.github/ISSUE_TEMPLATE/feature_request.md

@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
**Implementation Notes**
If you have any specific implementation ideas or requirements, please describe them here.

28
.github/pull_request_template.md

@ -0,0 +1,28 @@
## Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
## Additional Notes
Add any additional notes about the PR here.

131
CHANGELOG.md

@ -0,0 +1,131 @@
# Changelog
All notable changes to the Daily Notification Plugin 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).
## [1.0.0] - 2024-03-20
### Added
- Initial release of the Daily Notification Plugin
- Basic notification scheduling functionality
- Support for multiple notification schedules
- Timezone-aware scheduling
- Offline support with content caching
- Retry logic with exponential backoff
- Custom notification content handlers
- Event-based notification handling
- Comprehensive settings management
- TypeScript support with full type definitions
### Features
- Schedule daily notifications at specific times
- Support for multiple notification schedules
- Timezone-aware scheduling
- Offline support with content caching
- Retry logic with exponential backoff
- Custom notification content handlers
- Event-based notification handling
- Comprehensive settings management
### Security
- HTTPS-only network requests
- Content validation before display
- Secure storage of sensitive data
- Permission-based access control
- No sensitive data in logs
### Documentation
- Comprehensive API documentation
- Usage examples for basic and advanced scenarios
- Enterprise-level implementation examples
- Security best practices
- Platform-specific implementation details
### Testing
- Unit tests for core functionality
- Integration tests for platform features
- Advanced scenario tests
- Enterprise feature tests
- Security validation tests
## [0.9.0] - 2024-03-15
### Added
- Beta release with core functionality
- Basic notification scheduling
- Simple content handling
- Basic event system
### Changed
- Improved error handling
- Enhanced type definitions
- Better documentation
### Fixed
- Initial bug fixes and improvements
- TypeScript type issues
- Documentation clarity
## [0.8.0] - 2024-03-10
### Added
- Alpha release with basic features
- Initial plugin structure
- Basic TypeScript interfaces
- Simple notification scheduling
### Changed
- Early development improvements
- Initial documentation
- Basic test setup
### Fixed
- Early bug fixes
- Initial type issues
- Basic documentation
## [Unreleased]
### Added
- Enterprise features
- Notification queue system
- A/B testing support
- Advanced analytics tracking
- User preferences management
- Content personalization
- Rate limiting
- Additional test scenarios
- More example implementations
- Enhanced documentation
### Changed
- Improved error handling
- Enhanced type definitions
- Better documentation structure
- More comprehensive examples
### Fixed
- TypeScript type issues
- Documentation clarity
- Test coverage gaps
- Example code improvements
### Security
- Enhanced security measures
- Additional validation
- Improved error handling
- Better logging practices
### Documentation
- Added enterprise usage examples
- Enhanced API documentation
- Improved security guidelines
- Better troubleshooting guides
### Testing
- Added enterprise scenario tests
- Enhanced test coverage
- Improved test organization
- Better test documentation

116
CONTRIBUTING.md

@ -0,0 +1,116 @@
# Contributing to Daily Notification Plugin
Thank you for your interest in contributing to the Daily Notification Plugin for Capacitor! This document provides guidelines and instructions for contributing.
## Development Setup
1. Fork the repository
2. Clone your fork:
```bash
git clone https://github.com/yourusername/capacitor-daily-notification.git
cd capacitor-daily-notification
```
3. Install dependencies:
```bash
npm install
```
4. Build the project:
```bash
npm run build
```
## Development Guidelines
### Code Style
- Follow TypeScript best practices
- Use meaningful variable and function names
- Add JSDoc comments for public APIs
- Keep functions focused and single-purpose
- Maintain consistent indentation (2 spaces)
- Follow PEP 8 style guide for Python code
- Keep lines under 80 characters
### Testing
- Write unit tests for new features
- Update existing tests when modifying code
- Ensure all tests pass before submitting PR
- Add integration tests for complex features
- Test on both iOS and Android platforms
### Documentation
- Update README.md for significant changes
- Document new APIs in the code
- Update CHANGELOG.md for version changes
- Add examples for new features
- Keep documentation up to date
### Git Workflow
1. Create a feature branch:
```bash
git checkout -b feature/your-feature-name
```
2. Make your changes
3. Commit your changes:
```bash
git commit -m "feat: add your feature"
```
4. Push to your fork:
```bash
git push origin feature/your-feature-name
```
5. Create a Pull Request
### Commit Messages
Follow conventional commits format:
- `feat:` for new features
- `fix:` for bug fixes
- `docs:` for documentation changes
- `style:` for code style changes
- `refactor:` for code refactoring
- `test:` for adding tests
- `chore:` for maintenance tasks
Example:
```
feat: add timezone support for notifications
```
## Pull Request Process
1. Update the README.md with details of changes if needed
2. Update the CHANGELOG.md with a note describing your changes
3. Ensure all tests pass
4. Request review from maintainers
## Code Review Guidelines
- Review code for:
- Functionality
- Test coverage
- Documentation
- Code style
- Performance
- Security
## Release Process
1. Update version in package.json
2. Update CHANGELOG.md
3. Create a release tag
4. Build and test release
5. Publish to npm
## Support
- Open issues for bugs
- Use discussions for feature requests
- Join the community chat for questions
## License
By contributing, you agree that your contributions will be licensed under the project's MIT License.

17
CapacitorDailyNotification.podspec

@ -0,0 +1,17 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
Pod::Spec.new do |s|
s.name = 'CapacitorDailyNotification'
s.version = package['version']
s.summary = package['description']
s.license = package['license']
s.homepage = package['repository']['url']
s.author = package['author']
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
s.ios.deployment_target = '13.0'
s.dependency 'Capacitor'
s.swift_version = '5.1'
end

21
LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Matthew Raymer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

510
README.md

@ -0,0 +1,510 @@
# Daily Notification Plugin for Capacitor
A powerful Capacitor plugin for scheduling and managing daily notifications with advanced features like timezone support, offline capabilities, and retry logic.
## Features
- Schedule daily notifications at specific times
- Support for multiple notification schedules
- Timezone-aware scheduling
- Offline support with content caching
- Retry logic with exponential backoff
- Custom notification content handlers
- Event-based notification handling
- Comprehensive settings management
- TypeScript support with full type definitions
## Installation
```bash
npm install @timesafari/daily-notification-plugin
```
## Usage
### Basic Usage
```typescript
import { DailyNotification } from '@timesafari/daily-notification-plugin';
const plugin = new DailyNotification();
// Schedule a daily notification
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/updates',
time: '09:00',
title: 'Daily Update',
body: 'Your daily content is ready!',
sound: true,
priority: 'high'
});
// Get notification status
const status = await plugin.getNotificationStatus();
console.log('Next notification:', status.nextNotificationTime);
// Handle notification events
plugin.on('notification', (event) => {
console.log('Notification received:', event.detail);
});
```
### Advanced Usage
```typescript
// Multiple schedules with different timezones
const schedules = [
{
url: 'https://api.example.com/morning',
time: '09:00',
timezone: 'America/New_York',
title: 'Morning Update'
},
{
url: 'https://api.example.com/evening',
time: '18:00',
timezone: 'Europe/London',
title: 'Evening Update'
}
];
for (const schedule of schedules) {
await plugin.scheduleDailyNotification(schedule);
}
// Offline support with caching
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/updates',
time: '10:00',
offlineFallback: true,
cacheDuration: 3600, // 1 hour
contentHandler: async (response) => {
const data = await response.json();
return {
title: data.title,
body: data.content,
data: data.metadata
};
}
});
// Update settings
await plugin.updateSettings({
time: '11:00',
sound: true,
priority: 'high',
timezone: 'America/Chicago'
});
```
## API Reference
### Methods
#### `scheduleDailyNotification(options: NotificationOptions): Promise<void>`
Schedules a daily notification with the specified options.
```typescript
interface NotificationOptions {
url: string;
time: string; // "HH:mm" format
title?: string;
body?: string;
sound?: boolean;
vibrate?: boolean;
priority?: 'low' | 'normal' | 'high';
retryCount?: number;
retryInterval?: number;
cacheDuration?: number;
headers?: Record<string, string>;
offlineFallback?: boolean;
timezone?: string;
contentHandler?: (response: Response) => Promise<{
title: string;
body: string;
data?: any;
}>;
}
```
#### `getLastNotification(): Promise<NotificationResponse | null>`
Retrieves the last notification that was delivered.
#### `cancelAllNotifications(): Promise<void>`
Cancels all scheduled notifications.
#### `getNotificationStatus(): Promise<NotificationStatus>`
Gets the current status of notifications.
#### `updateSettings(settings: NotificationSettings): Promise<void>`
Updates notification settings.
```typescript
interface NotificationSettings {
time?: string;
sound?: boolean;
vibrate?: boolean;
priority?: 'low' | 'normal' | 'high';
timezone?: string;
}
```
### Events
The plugin emits the following events:
- `notification`: Fired when a notification is received or interacted with
```typescript
interface NotificationEvent extends Event {
detail: {
id: string;
action: string;
data?: any;
};
}
```
## Testing
The plugin includes comprehensive tests covering:
- Basic functionality
- Multiple schedules
- Timezone handling
- Offline support
- Retry logic
- Event handling
- Settings management
Run tests with:
```bash
npm test
```
## Security Considerations
- All network requests use HTTPS
- Content is validated before display
- Sensitive data is not stored in logs
- Permissions are properly managed
- Input validation is performed on all methods
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Author
Matthew Raymer
## Platform-Specific Implementation
### Android Implementation
- Uses `WorkManager` for periodic data fetching
- Implements `AlarmManager` for precise notification scheduling
- Stores data in `SharedPreferences`
- Handles Doze mode and battery optimizations
### iOS
- Utilizes `BGTaskScheduler` for background fetches
- Implements `UNUserNotificationCenter` for notifications
- Stores data in `UserDefaults`
- Respects system background execution limits
## Permissions
### Android Permissions
Required permissions in `AndroidManifest.xml`:
```xml
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
```
### iOS Implementation
- Notification permissions (requested at runtime)
- Background App Refresh capability (enabled in Xcode)
## Error Handling
The plugin implements comprehensive error handling:
- Network failure retry logic with exponential backoff
- Fallback to cached content when fetching fails
- Detailed error logging and reporting
- Graceful degradation when permissions are denied
## Best Practices
### Content Management
- Keep notification content concise and actionable
- Use clear, engaging titles under 50 characters
- Limit notification body to 2-3 lines
- Include a clear call-to-action when appropriate
### Network Optimization
- Implement proper caching headers on your API
- Use compression for network responses
- Keep payload size under 4KB for optimal performance
- Implement rate limiting on your API endpoints
### Battery Considerations
- Schedule notifications during active hours
- Avoid excessive background fetches
- Use appropriate fetch intervals (minimum 15 minutes)
- Implement smart retry strategies
### User Experience
- Request notification permissions at an appropriate time
- Provide clear value proposition for notifications
- Allow users to customize notification timing
- Implement proper error messaging for users
### Security
- Always use HTTPS for API endpoints
- Implement proper API authentication
- Sanitize notification content
- Follow platform-specific security guidelines
### Testing
- Test notifications in various app states
- Verify behavior with different network conditions
- Test on multiple device types and OS versions
- Implement proper error logging for debugging
## Development Setup
### Prerequisites
1. Node.js 14 or higher
2. Java 11 or higher
3. Android Studio and Android SDK
4. Xcode (for iOS development, macOS only)
5. CocoaPods (for iOS development)
### Environment Setup
1. Clone the repository:
```bash
git clone https://github.com/yourusername/capacitor-daily-notification.git
cd capacitor-daily-notification
```
2. Install dependencies:
```bash
npm install
```
This will:
- Install Node.js dependencies
- Check your development environment
- Set up native build environments
- Install platform-specific dependencies
### Building the Plugin
#### TypeScript Build
```bash
# Build TypeScript code
npm run build
# Watch mode for development
npm run watch
```
#### Android Build
```bash
# Build Android library
npm run build:android
# Run Android tests
npm run test:android
```
The Android build will:
1. Compile the TypeScript code
2. Build the Android library
3. Generate an AAR file in `android/build/outputs/aar/`
#### iOS Build
```bash
# Build iOS library
npm run build:ios
# Run iOS tests
npm run test:ios
```
The iOS build will:
1. Compile the TypeScript code
2. Install CocoaPods dependencies
3. Build the iOS framework
### Using in a Capacitor App
1. Install the plugin in your Capacitor app:
```bash
npm install capacitor-daily-notification
```
2. Add to your Android app's `android/app/build.gradle`:
```gradle
dependencies {
implementation project(':daily-notification')
}
```
3. Add to your iOS app's `ios/App/Podfile`:
```ruby
pod 'CapacitorDailyNotification', :path => '../node_modules/capacitor-daily-notification'
```
4. Sync native projects:
```bash
npx cap sync
```
### Troubleshooting
#### Android Issues
1. Gradle Sync Failed
```bash
cd android
./gradlew clean
./gradlew --refresh-dependencies
```
2. Missing Android SDK
- Set ANDROID_HOME environment variable
- Install required SDK components via Android Studio
3. Build Errors
- Check Android Studio for detailed error messages
- Ensure all required SDK components are installed
- Verify Gradle version compatibility
#### iOS Issues
1. Pod Install Failed
```bash
cd ios
pod deintegrate
pod cache clean --all
pod install
```
2. Xcode Build Errors
- Open Xcode project in Xcode
- Check build settings
- Verify deployment target matches requirements
3. Missing Dependencies
- Ensure CocoaPods is installed
- Run `pod setup` to update CocoaPods repos
### Development Workflow
1. Make changes to TypeScript code
2. Run `npm run build` to compile
3. Run `npm run build:native` to build native code
4. Test changes:
- Android: `npm run test:android`
- iOS: `npm run test:ios`
5. Run full validation:
- Android: `npm run validate`
- iOS: `npm run validate:ios`
### Continuous Integration
The plugin includes pre-commit hooks and CI configurations:
1. Pre-commit checks:
- TypeScript compilation
- Linting
- Native build verification
2. CI pipeline:
- Environment validation
- TypeScript build
- Native builds
- Unit tests
- Integration tests
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Author
Matthew Raymer
## Plugin Security
This plugin follows security best practices:
- Secure storage of sensitive data
- HTTPS-only network requests
- Permission-based access control
- Regular security audits
- No sensitive data in logs
## Support
For support, please open an issue in the GitHub repository or contact the maintainers.
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for a list of changes and version history.

205
SECURITY.md

@ -0,0 +1,205 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 1.0.x | :white_check_mark: |
| 0.9.x | :white_check_mark: |
| 0.8.x | :x: |
## Reporting a Vulnerability
We take the security of the Daily Notification Plugin seriously. If you discover a security vulnerability, please follow these steps:
1. **Do Not** disclose the vulnerability publicly until it has been addressed
2. Submit a detailed report to our security team
3. Include steps to reproduce the vulnerability
4. Provide any relevant code or configuration
5. Include your contact information for follow-up
## Security Best Practices
### Network Security
- All network requests must use HTTPS
- Implement proper API authentication
- Use secure headers for all requests
- Validate SSL certificates
- Implement rate limiting
- Use secure WebSocket connections when needed
### Data Security
- Encrypt sensitive data at rest
- Use secure storage for credentials
- Implement proper session management
- Sanitize all user input
- Validate all data before processing
- Implement proper error handling
### Platform Security
#### Android
- Use Android Keystore for sensitive data
- Implement proper permission handling
- Use secure storage for credentials
- Validate app signatures
- Implement proper activity lifecycle management
#### iOS
- Use Keychain for sensitive data
- Implement proper permission handling
- Use secure storage for credentials
- Validate app signatures
- Implement proper app lifecycle management
### Code Security
- Regular security audits
- Code signing
- Dependency scanning
- Static code analysis
- Dynamic code analysis
- Regular updates and patches
### Logging and Monitoring
- Implement secure logging practices
- No sensitive data in logs
- Proper error tracking
- Performance monitoring
- Usage analytics
- Security event monitoring
## Security Checklist
### Development
- [ ] Use HTTPS for all network requests
- [ ] Implement proper authentication
- [ ] Validate all user input
- [ ] Sanitize all output
- [ ] Use secure storage for sensitive data
- [ ] Implement proper error handling
- [ ] Use secure headers
- [ ] Implement rate limiting
- [ ] Regular security audits
- [ ] Code signing
### Testing
- [ ] Security testing
- [ ] Penetration testing
- [ ] Vulnerability scanning
- [ ] Dependency scanning
- [ ] Static code analysis
- [ ] Dynamic code analysis
- [ ] Regular updates
- [ ] Patch management
- [ ] Security monitoring
- [ ] Incident response
### Deployment
- [ ] Secure configuration
- [ ] Environment security
- [ ] Access control
- [ ] Monitoring setup
- [ ] Backup procedures
- [ ] Recovery procedures
- [ ] Incident response plan
- [ ] Security documentation
- [ ] Training and awareness
- [ ] Regular reviews
## Security Features
### Authentication
- Token-based authentication
- OAuth 2.0 support
- Biometric authentication
- Multi-factor authentication
- Session management
### Authorization
- Role-based access control
- Permission management
- Resource access control
- API access control
- Feature flags
### Data Protection
- Encryption at rest
- Encryption in transit
- Secure storage
- Data sanitization
- Data validation
### Monitoring
- Security event logging
- Performance monitoring
- Usage analytics
- Error tracking
- Incident detection
## Security Updates
### Regular Updates
- Weekly dependency updates
- Monthly security patches
- Quarterly security reviews
- Annual security audits
- Continuous monitoring
### Emergency Updates
- Critical security patches
- Zero-day vulnerability fixes
- Incident response
- Security advisories
- User notifications
## Security Resources
### Documentation
- Security guidelines
- Best practices
- Implementation guides
- Troubleshooting guides
- Security FAQs
### Tools
- Security testing tools
- Monitoring tools
- Analysis tools
- Scanning tools
- Audit tools
### Training
- Security awareness
- Implementation training
- Best practices training
- Incident response training
- Regular updates
## Contact
For security-related issues or questions, please contact:
- Security Team: security@timesafari.com
- Emergency Contact: emergency@timesafari.com
## Acknowledgments
We would like to thank all security researchers and contributors who have helped improve the security of the Daily Notification Plugin.

101
android/.gitignore

@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

2
android/app/.gitignore

@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

67
android/app/build.gradle

@ -0,0 +1,67 @@
apply plugin: 'com.android.application'
android {
namespace "com.timesafari.dailynotification"
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "com.timesafari.dailynotification"
minSdkVersion 22
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
repositories {
google()
mavenCentral()
maven {
url "${project.rootDir}/capacitor-cordova-android-plugins/src/main/libs"
}
maven {
url "${project.rootDir}/libs"
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

14
android/app/capacitor.build.gradle

@ -0,0 +1,14 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-android')
implementation project(':capacitor-cordova-android-plugins')
}

21
android/app/proguard-rules.pro

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

26
android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java

@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

44
android/app/src/main/AndroidManifest.xml

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk android:minSdkVersion="22" android:targetSdkVersion="33" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/AppTheme.NoActionBarLaunch">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
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>
</provider>
</application>
</manifest>

39
android/app/src/main/java/README.md

@ -0,0 +1,39 @@
# Android Implementation
This directory contains the Android-specific implementation of the DailyNotification plugin.
## Implementation Details
The Android implementation uses:
- `WorkManager` for periodic data fetching
- `AlarmManager` for precise notification scheduling
- `SharedPreferences` for local data storage
- Android's notification channels for proper notification display
## Native Code Location
The native Android implementation is located in the `android/` directory at the project root.
## Key Components
1. `DailyNotificationAndroid.java`: Main plugin class
2. `FetchWorker.java`: Background work for data fetching
3. `NotificationReceiver.java`: Handles notification display
4. `NotificationHelper.java`: Manages notification creation and display
## Implementation Notes
- Uses Android's WorkManager for reliable background tasks
- Implements proper battery optimization handling
- Supports Android 8.0+ notification channels
- Handles Doze mode and battery optimizations
- Uses SharedPreferences for lightweight data storage
## Testing
Run Android-specific tests with:
```bash
npm run test:android
```

5
android/app/src/main/java/com/example/app/MainActivity.java

@ -0,0 +1,5 @@
package com.example.app;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

160
android/app/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java

@ -0,0 +1,160 @@
/**
* DailyNotificationPlugin.java
* Daily Notification Plugin for Capacitor
*
* Handles daily notification scheduling and management on Android
*/
package com.timesafari.dailynotification;
import android.app.AlarmManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import java.util.Calendar;
import java.util.TimeZone;
@CapacitorPlugin(name = "DailyNotification")
public class DailyNotificationPlugin extends Plugin {
private static final String CHANNEL_ID = "daily_notification_channel";
private static final String CHANNEL_NAME = "Daily Notifications";
private static final String CHANNEL_DESCRIPTION = "Daily notification updates";
private NotificationManager notificationManager;
private AlarmManager alarmManager;
private Context context;
@Override
public void load() {
context = getContext();
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
createNotificationChannel();
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
);
channel.setDescription(CHANNEL_DESCRIPTION);
notificationManager.createNotificationChannel(channel);
}
}
@PluginMethod
public void scheduleDailyNotification(PluginCall call) {
String url = call.getString("url");
String time = call.getString("time");
if (url == null || time == null) {
call.reject("Missing required parameters");
return;
}
// Parse time string (HH:mm format)
String[] timeComponents = time.split(":");
if (timeComponents.length != 2) {
call.reject("Invalid time format");
return;
}
try {
int hour = Integer.parseInt(timeComponents[0]);
int minute = Integer.parseInt(timeComponents[1]);
if (hour < 0 || hour >= 24 || minute < 0 || minute >= 60) {
call.reject("Invalid time values");
return;
}
// Create calendar instance for the specified time
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, 0);
// If the time has already passed today, schedule for tomorrow
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
calendar.add(Calendar.DAY_OF_YEAR, 1);
}
// Create intent for the notification
Intent intent = new Intent(context, DailyNotificationReceiver.class);
intent.putExtra("url", url);
intent.putExtra("title", call.getString("title", "Daily Notification"));
intent.putExtra("body", call.getString("body", "Your daily update is ready"));
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
url.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// Schedule the alarm
alarmManager.setRepeating(
AlarmManager.RTC_WAKEUP,
calendar.getTimeInMillis(),
AlarmManager.INTERVAL_DAY,
pendingIntent
);
call.resolve();
} catch (NumberFormatException e) {
call.reject("Invalid time format");
}
}
@PluginMethod
public void getLastNotification(PluginCall call) {
// TODO: Implement last notification retrieval
JSObject result = new JSObject();
result.put("id", "");
result.put("title", "");
result.put("body", "");
result.put("timestamp", 0);
call.resolve(result);
}
@PluginMethod
public void cancelAllNotifications(PluginCall call) {
Intent intent = new Intent(context, DailyNotificationReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
alarmManager.cancel(pendingIntent);
call.resolve();
}
@PluginMethod
public void getNotificationStatus(PluginCall call) {
JSObject result = new JSObject();
result.put("nextNotificationTime", 0); // TODO: Implement next notification time
result.put("isEnabled", true); // TODO: Check system notification settings
call.resolve(result);
}
@PluginMethod
public void updateSettings(PluginCall call) {
// TODO: Implement settings update
call.resolve();
}
}

51
android/app/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java

@ -0,0 +1,51 @@
/**
* DailyNotificationReceiver.java
* Broadcast receiver for handling daily notifications on Android
*/
package com.timesafari.dailynotification;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.core.app.NotificationCompat;
public class DailyNotificationReceiver extends BroadcastReceiver {
private static final String CHANNEL_ID = "daily_notification_channel";
@Override
public void onReceive(Context context, Intent intent) {
String url = intent.getStringExtra("url");
String title = intent.getStringExtra("title");
String body = intent.getStringExtra("body");
if (url == null || title == null || body == null) {
return;
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(title)
.setContentText(body)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true);
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(
new android.app.NotificationChannel(
CHANNEL_ID,
"Daily Notifications",
NotificationManager.IMPORTANCE_DEFAULT
)
);
}
notificationManager.notify(url.hashCode(), builder.build());
}
}

52
android/app/src/main/java/index.ts

@ -0,0 +1,52 @@
/**
* Android implementation of the DailyNotification plugin
* @module DailyNotificationAndroid
*/
import { Capacitor } from '@capacitor/core';
import type { DailyNotificationPlugin, DailyNotificationOptions, PermissionStatus } from '../definitions';
export class DailyNotificationAndroid implements DailyNotificationPlugin {
private options: DailyNotificationOptions = {
url: '',
notificationTime: '09:00',
title: 'Daily Update',
body: 'Your daily notification is ready'
};
/**
* Initialize the daily notification system for Android
* @param options Configuration options for the notification system
*/
async initialize(options: DailyNotificationOptions): Promise<void> {
if (Capacitor.getPlatform() !== 'android') {
throw new Error('This implementation is for Android only');
}
this.options = options;
// TODO: Implement Android-specific initialization
}
/**
* Check current permission status for notifications
* @returns Current permission status
*/
async checkPermissions(): Promise<PermissionStatus> {
if (Capacitor.getPlatform() !== 'android') {
throw new Error('This implementation is for Android only');
}
// TODO: Implement Android-specific permission check
return { notifications: 'prompt' };
}
/**
* Request notification permissions from the user
* @returns Updated permission status after request
*/
async requestPermissions(): Promise<PermissionStatus> {
if (Capacitor.getPlatform() !== 'android') {
throw new Error('This implementation is for Android only');
}
// TODO: Implement Android-specific permission request
return { notifications: 'prompt' };
}
}

BIN
android/app/src/main/res/drawable-land-hdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
android/app/src/main/res/drawable-land-mdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
android/app/src/main/res/drawable-land-xhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
android/app/src/main/res/drawable-land-xxhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
android/app/src/main/res/drawable-land-xxxhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
android/app/src/main/res/drawable-port-hdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
android/app/src/main/res/drawable-port-mdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
android/app/src/main/res/drawable-port-xhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
android/app/src/main/res/drawable-port-xxhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
android/app/src/main/res/drawable-port-xxxhdpi/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

34
android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

170
android/app/src/main/res/drawable/ic_launcher_background.xml

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

BIN
android/app/src/main/res/drawable/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

12
android/app/src/main/res/layout/activity_main.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

5
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

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

5
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

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

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

4
android/app/src/main/res/values/ic_launcher_background.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

7
android/app/src/main/res/values/strings.xml

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">capacitor-daily-notification</string>
<string name="title_activity_main">capacitor-daily-notification</string>
<string name="package_name">com.example.app</string>
<string name="custom_url_scheme">com.example.app</string>
</resources>

22
android/app/src/main/res/values/styles.xml

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

5
android/app/src/main/res/xml/file_paths.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

18
android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java

@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

77
android/build.gradle

@ -0,0 +1,77 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
}
}
apply from: "variables.gradle"
apply plugin: 'com.android.library'
ext {
compileSdkVersion = 33
minSdkVersion = 21
targetSdkVersion = 33
buildToolsVersion = '33.0.0'
}
android {
namespace "com.timesafari.dailynotification"
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
allprojects {
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:unchecked"
options.compilerArgs << "-Xlint:deprecation"
}
}
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
maven {
url "${project.rootDir}/capacitor-cordova-android-plugins/src/main/libs"
}
maven {
url "${project.rootDir}/libs"
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':capacitor-android')
implementation project(':capacitor-cordova-android-plugins')
implementation 'androidx.work:work-runtime:2.8.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

3
android/capacitor.settings.gradle

@ -0,0 +1,3 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

22
android/gradle.properties

@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# 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

BIN
android/gradle/wrapper/gradle-wrapper.jar

Binary file not shown.

6
android/gradle/wrapper/gradle-wrapper.properties

@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

244
android/gradlew

@ -0,0 +1,244 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
android/gradlew.bat

@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

10
android/settings.gradle

@ -0,0 +1,10 @@
rootProject.name = 'daily-notification'
include ':app'
include ':capacitor-android'
include ':capacitor-cordova-android-plugins'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins')
apply from: 'capacitor.settings.gradle'

17
android/variables.gradle

@ -0,0 +1,17 @@
ext {
compileSdkVersion = 33
minSdkVersion = 22
targetSdkVersion = 33
buildToolsVersion = '33.0.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
coreSplashScreenVersion = '1.0.1'
androidxActivityVersion = '1.7.0'
androidxCoreVersion = '1.10.0'
androidxFragmentVersion = '1.5.6'
androidxWebkitVersion = '1.6.1'
junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1'
cordovaAndroidVersion = '10.1.1'
}

12
capacitor.config.ts

@ -0,0 +1,12 @@
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.app',
appName: 'capacitor-daily-notification',
webDir: 'www',
server: {
androidScheme: 'https'
}
};
export default config;

259
examples/advanced-usage.ts

@ -0,0 +1,259 @@
/**
* Advanced usage examples for the Daily Notification plugin
* Demonstrates complex scenarios and best practices
*/
import { registerPlugin } from '@capacitor/core';
import { DailyNotificationPlugin, NotificationOptions, NotificationSettings } from '../src/definitions';
const DailyNotification = registerPlugin<DailyNotificationPlugin>('DailyNotification');
/**
* Example of handling multiple notification schedules
*/
async function handleMultipleSchedules() {
try {
// Morning notification
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/morning-content',
time: '08:00',
title: 'Morning Briefing',
body: 'Your morning briefing is ready',
priority: 'high'
});
// Evening notification
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/evening-content',
time: '20:00',
title: 'Evening Summary',
body: 'Your daily summary is ready',
priority: 'normal'
});
console.log('Multiple schedules set up successfully');
} catch (error) {
console.error('Failed to setup multiple schedules:', error);
}
}
/**
* Example of handling timezone changes
*/
async function handleTimezoneChanges() {
try {
// Get user's timezone
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Schedule notification with timezone consideration
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/timezone-aware-content',
time: '09:00',
title: 'Timezone-Aware Update',
body: 'Your timezone-aware content is ready',
timezone: userTimezone
});
// Listen for timezone changes
window.addEventListener('timezonechange', async () => {
const newTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
await DailyNotification.updateSettings({
timezone: newTimezone
});
});
} catch (error) {
console.error('Failed to handle timezone changes:', error);
}
}
/**
* Example of implementing retry logic with exponential backoff
*/
async function implementRetryLogic() {
const maxRetries = 3;
const baseDelay = 1000; // 1 second
async function fetchWithRetry(url: string, retryCount = 0): Promise<Response> {
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Network response was not ok');
return response;
} catch (error) {
if (retryCount >= maxRetries) throw error;
const delay = baseDelay * Math.pow(2, retryCount);
await new Promise(resolve => setTimeout(resolve, delay));
return fetchWithRetry(url, retryCount + 1);
}
}
try {
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/unstable-content',
time: '10:00',
title: 'Retry-Enabled Update',
body: 'Your content with retry logic is ready',
retryCount: maxRetries,
retryInterval: baseDelay / 1000, // Convert to seconds
contentHandler: async (response) => {
const data = await response.json();
return {
title: data.title,
body: data.content,
data: data.metadata
};
}
});
} catch (error) {
console.error('Failed to implement retry logic:', error);
}
}
/**
* Example of implementing offline support
*/
async function implementOfflineSupport() {
try {
// Check if we're online
const isOnline = navigator.onLine;
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/offline-aware-content',
time: '11:00',
title: 'Offline-Aware Update',
body: 'Your offline-aware content is ready',
offlineFallback: true,
cacheDuration: 24, // Cache for 24 hours
contentHandler: async (response) => {
const data = await response.json();
// Store in IndexedDB for offline access
if ('indexedDB' in window) {
const db = await openDB('notificationCache', 1, {
upgrade(db) {
db.createObjectStore('content');
}
});
await db.put('content', data, 'latest');
}
return {
title: data.title,
body: data.content,
data: data.metadata
};
}
});
// Listen for online/offline events
window.addEventListener('online', () => {
console.log('Back online, syncing content...');
// Trigger content sync
});
window.addEventListener('offline', () => {
console.log('Offline, using cached content...');
// Use cached content
});
} catch (error) {
console.error('Failed to implement offline support:', error);
}
}
/**
* Example of implementing user preferences
*/
async function implementUserPreferences() {
try {
// Get user preferences from storage
const preferences = {
notificationTime: '12:00',
soundEnabled: true,
priority: 'high' as const,
categories: ['news', 'weather', 'tasks']
};
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/personalized-content',
time: preferences.notificationTime,
title: 'Personalized Update',
body: 'Your personalized content is ready',
sound: preferences.soundEnabled,
priority: preferences.priority,
headers: {
'X-User-Categories': preferences.categories.join(',')
}
});
// Listen for preference changes
window.addEventListener('storage', (event) => {
if (event.key === 'notificationPreferences') {
const newPreferences = JSON.parse(event.newValue || '{}');
DailyNotification.updateSettings({
time: newPreferences.notificationTime,
sound: newPreferences.soundEnabled,
priority: newPreferences.priority
});
}
});
} catch (error) {
console.error('Failed to implement user preferences:', error);
}
}
/**
* Example of implementing analytics and monitoring
*/
async function implementAnalytics() {
try {
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/analytics-enabled-content',
time: '13:00',
title: 'Analytics-Enabled Update',
body: 'Your analytics-enabled content is ready',
contentHandler: async (response) => {
const data = await response.json();
// Track notification delivery
await trackEvent('notification_delivered', {
timestamp: new Date().toISOString(),
contentId: data.id,
contentType: data.type
});
return {
title: data.title,
body: data.content,
data: data.metadata
};
}
});
// Track notification interactions
document.addEventListener('notification_clicked', async (event) => {
await trackEvent('notification_clicked', {
timestamp: new Date().toISOString(),
notificationId: event.detail.id,
action: event.detail.action
});
});
} catch (error) {
console.error('Failed to implement analytics:', error);
}
}
// Helper function for analytics
async function trackEvent(eventName: string, properties: Record<string, any>) {
// Implement your analytics tracking logic here
console.log(`Tracking event: ${eventName}`, properties);
}
// Export all advanced example functions
export {
handleMultipleSchedules,
handleTimezoneChanges,
implementRetryLogic,
implementOfflineSupport,
implementUserPreferences,
implementAnalytics
};

262
examples/enterprise-usage.ts

@ -0,0 +1,262 @@
/**
* Enterprise-level usage examples for the Daily Notification plugin
* Demonstrates advanced features and best practices for large-scale applications
*/
import { DailyNotification } from '@timesafari/daily-notification-plugin';
import { NotificationOptions, NotificationSettings } from '../src/definitions';
/**
* Example of implementing a notification queue system
*/
async function implementNotificationQueue() {
const plugin = new DailyNotification();
const queue: NotificationOptions[] = [];
let isProcessing = false;
async function processQueue() {
if (isProcessing || queue.length === 0) return;
isProcessing = true;
try {
const notification = queue.shift();
if (notification) {
await plugin.scheduleDailyNotification(notification);
}
} catch (error) {
console.error('Failed to process notification:', error);
// Retry logic here
} finally {
isProcessing = false;
}
}
// Add to queue
queue.push({
url: 'https://api.example.com/enterprise/updates',
time: '09:00',
title: 'Enterprise Update',
priority: 'high',
retryCount: 3,
retryInterval: 5000,
});
// Process queue
await processQueue();
}
/**
* Example of implementing A/B testing for notifications
*/
async function implementABTesting() {
const plugin = new DailyNotification();
async function scheduleABTest() {
const variants = [
{
title: 'Version A: Direct Call-to-Action',
body: 'Click here to view your daily report',
priority: 'high',
},
{
title: 'Version B: Value Proposition',
body: 'Your daily insights are ready to help you succeed',
priority: 'normal',
},
];
// Randomly select a variant
const variant = variants[Math.floor(Math.random() * variants.length)];
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/ab-test-content',
time: '10:00',
...variant,
data: {
variant: variant === variants[0] ? 'A' : 'B',
timestamp: new Date().toISOString(),
},
});
}
await scheduleABTest();
}
/**
* Example of implementing notification analytics and tracking
*/
async function implementAnalyticsTracking() {
const plugin = new DailyNotification();
// Track notification delivery
plugin.on('notification', async (event) => {
await trackAnalytics({
event: 'notification_delivered',
notificationId: event.detail.id,
timestamp: new Date().toISOString(),
platform: navigator.platform,
userAgent: navigator.userAgent,
});
});
// Track notification interactions
document.addEventListener('notification_clicked', async (event) => {
await trackAnalytics({
event: 'notification_clicked',
notificationId: event.detail.id,
timestamp: new Date().toISOString(),
action: event.detail.action,
data: event.detail.data,
});
});
async function trackAnalytics(data: Record<string, any>) {
// Implement your analytics tracking logic here
console.log('Analytics Event:', data);
}
}
/**
* Example of implementing notification preferences management
*/
async function implementPreferencesManagement() {
const plugin = new DailyNotification();
interface UserPreferences {
notificationTimes: string[];
categories: string[];
priority: 'low' | 'normal' | 'high';
sound: boolean;
timezone: string;
}
async function updateUserPreferences(preferences: UserPreferences) {
// Update all scheduled notifications
for (const time of preferences.notificationTimes) {
await plugin.updateSettings({
time,
priority: preferences.priority,
sound: preferences.sound,
timezone: preferences.timezone,
});
}
// Schedule new notifications for each category
for (const category of preferences.categories) {
await plugin.scheduleDailyNotification({
url: `https://api.example.com/categories/${category}`,
time: preferences.notificationTimes[0],
title: `${category} Update`,
priority: preferences.priority,
sound: preferences.sound,
timezone: preferences.timezone,
});
}
}
// Example usage
await updateUserPreferences({
notificationTimes: ['09:00', '12:00', '18:00'],
categories: ['news', 'tasks', 'updates'],
priority: 'high',
sound: true,
timezone: 'America/New_York',
});
}
/**
* Example of implementing notification content personalization
*/
async function implementContentPersonalization() {
const plugin = new DailyNotification();
interface UserProfile {
id: string;
name: string;
preferences: {
language: string;
timezone: string;
categories: string[];
};
}
async function schedulePersonalizedNotification(profile: UserProfile) {
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/personalized-content',
time: '09:00',
title: `Good morning, ${profile.name}!`,
priority: 'high',
timezone: profile.preferences.timezone,
headers: {
'X-User-ID': profile.id,
'X-Language': profile.preferences.language,
'X-Categories': profile.preferences.categories.join(','),
},
contentHandler: async (response) => {
const data = await response.json();
return {
title: data.title,
body: data.content,
data: {
...data.metadata,
userId: profile.id,
timestamp: new Date().toISOString(),
},
};
},
});
}
// Example usage
await schedulePersonalizedNotification({
id: 'user123',
name: 'John Doe',
preferences: {
language: 'en-US',
timezone: 'America/New_York',
categories: ['news', 'weather', 'tasks'],
},
});
}
/**
* Example of implementing notification rate limiting
*/
async function implementRateLimiting() {
const plugin = new DailyNotification();
class RateLimiter {
private lastNotificationTime: number = 0;
private minInterval: number = 60000; // 1 minute
async scheduleWithRateLimit(options: NotificationOptions): Promise<void> {
const now = Date.now();
if (now - this.lastNotificationTime < this.minInterval) {
throw new Error('Rate limit exceeded. Please wait before scheduling another notification.');
}
await plugin.scheduleDailyNotification(options);
this.lastNotificationTime = now;
}
}
const rateLimiter = new RateLimiter();
// Example usage
await rateLimiter.scheduleWithRateLimit({
url: 'https://api.example.com/rate-limited-content',
time: '11:00',
title: 'Rate Limited Update',
priority: 'normal',
});
}
// Export all enterprise example functions
export {
implementNotificationQueue,
implementABTesting,
implementAnalyticsTracking,
implementPreferencesManagement,
implementContentPersonalization,
implementRateLimiting,
};

165
examples/usage.ts

@ -0,0 +1,165 @@
/**
* Example usage of the Daily Notification plugin
* Demonstrates various features and best practices
*/
import { registerPlugin } from '@capacitor/core';
import { DailyNotificationPlugin } from '../src/definitions';
const DailyNotification = registerPlugin<DailyNotificationPlugin>('DailyNotification');
/**
* Basic setup for daily notifications
*/
async function setupBasicNotifications() {
try {
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/daily-content',
time: '08:00', // 8 AM
title: 'Daily Update',
body: 'Your daily content is ready!'
});
console.log('Daily notifications scheduled successfully');
} catch (error) {
console.error('Failed to schedule notifications:', error);
}
}
/**
* Advanced setup with custom options
*/
async function setupAdvancedNotifications() {
try {
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/daily-content',
time: '09:00', // 9 AM
title: 'Daily Digest',
body: 'Your personalized daily digest is ready',
sound: true,
vibrate: true,
priority: 'high',
retryCount: 3,
retryInterval: 15, // minutes
cacheDuration: 24, // hours
headers: {
'Authorization': 'Bearer your-token'
}
});
console.log('Advanced notification setup completed');
} catch (error) {
console.error('Failed to setup advanced notifications:', error);
}
}
/**
* Handle notification response
*/
async function handleNotificationResponse() {
try {
const response = await DailyNotification.getLastNotification();
if (response) {
console.log('Last notification:', response);
// Handle the notification data
}
} catch (error) {
console.error('Failed to get last notification:', error);
}
}
/**
* Cancel all scheduled notifications
*/
async function cancelNotifications() {
try {
await DailyNotification.cancelAllNotifications();
console.log('All notifications cancelled');
} catch (error) {
console.error('Failed to cancel notifications:', error);
}
}
/**
* Check notification status
*/
async function checkNotificationStatus() {
try {
const status = await DailyNotification.getNotificationStatus();
console.log('Notification status:', status);
return status;
} catch (error) {
console.error('Failed to get notification status:', error);
return null;
}
}
/**
* Update notification settings
*/
async function updateNotificationSettings() {
try {
await DailyNotification.updateSettings({
time: '10:00', // Change to 10 AM
sound: false, // Disable sound
priority: 'normal'
});
console.log('Notification settings updated');
} catch (error) {
console.error('Failed to update settings:', error);
}
}
/**
* Example of handling network errors
*/
async function handleNetworkErrors() {
try {
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/daily-content',
time: '08:00',
retryCount: 3,
retryInterval: 15,
offlineFallback: true
});
} catch (error) {
if (error.code === 'NETWORK_ERROR') {
console.log('Network error, will retry later');
} else {
console.error('Unexpected error:', error);
}
}
}
/**
* Example of using custom content handlers
*/
async function useCustomContentHandler() {
try {
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/daily-content',
time: '08:00',
contentHandler: async (response) => {
// Custom processing of the API response
const data = await response.json();
return {
title: data.title,
body: data.summary,
data: data.details
};
}
});
} catch (error) {
console.error('Failed to setup custom content handler:', error);
}
}
// Export all example functions
export {
setupBasicNotifications,
setupAdvancedNotifications,
handleNotificationResponse,
cancelNotifications,
checkNotificationStatus,
updateNotificationSettings,
handleNetworkErrors,
useCustomContentHandler
};

109
ios/Plugin/DailyNotificationPlugin.swift

@ -0,0 +1,109 @@
/**
* DailyNotificationPlugin.swift
* Daily Notification Plugin for Capacitor
*
* Handles daily notification scheduling and management on iOS
*/
import Foundation
import Capacitor
import UserNotifications
@objc(DailyNotificationPlugin)
public class DailyNotificationPlugin: CAPPlugin {
private let notificationCenter = UNUserNotificationCenter.current()
@objc func scheduleDailyNotification(_ call: CAPPluginCall) {
guard let url = call.getString("url"),
let time = call.getString("time") else {
call.reject("Missing required parameters")
return
}
// Parse time string (HH:mm format)
let timeComponents = time.split(separator: ":")
guard timeComponents.count == 2,
let hour = Int(timeComponents[0]),
let minute = Int(timeComponents[1]),
hour >= 0 && hour < 24,
minute >= 0 && minute < 60 else {
call.reject("Invalid time format")
return
}
// Create notification content
let content = UNMutableNotificationContent()
content.title = call.getString("title") ?? "Daily Notification"
content.body = call.getString("body") ?? "Your daily update is ready"
content.sound = call.getBool("sound", true) ? .default : nil
// Set priority
if let priority = call.getString("priority") {
switch priority {
case "high":
content.interruptionLevel = .timeSensitive
case "low":
content.interruptionLevel = .passive
default:
content.interruptionLevel = .active
}
}
// Create trigger for daily notification
var dateComponents = DateComponents()
dateComponents.hour = hour
dateComponents.minute = minute
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
// Create request
let request = UNNotificationRequest(
identifier: "daily-notification-\(url)",
content: content,
trigger: trigger
)
// Schedule notification
notificationCenter.add(request) { error in
if let error = error {
call.reject("Failed to schedule notification: \(error.localizedDescription)")
} else {
call.resolve()
}
}
}
@objc func getLastNotification(_ call: CAPPluginCall) {
notificationCenter.getDeliveredNotifications { notifications in
let lastNotification = notifications.first
let result: [String: Any] = [
"id": lastNotification?.request.identifier ?? "",
"title": lastNotification?.request.content.title ?? "",
"body": lastNotification?.request.content.body ?? "",
"timestamp": lastNotification?.date.timeIntervalSince1970 ?? 0
]
call.resolve(result)
}
}
@objc func cancelAllNotifications(_ call: CAPPluginCall) {
notificationCenter.removeAllPendingNotificationRequests()
notificationCenter.removeAllDeliveredNotifications()
call.resolve()
}
@objc func getNotificationStatus(_ call: CAPPluginCall) {
notificationCenter.getPendingNotificationRequests { requests in
let nextNotification = requests.first
let result: [String: Any] = [
"nextNotificationTime": nextNotification?.trigger?.nextTriggerDate?.timeIntervalSince1970 ?? 0,
"isEnabled": true // TODO: Check system notification settings
]
call.resolve(result)
}
}
@objc func updateSettings(_ call: CAPPluginCall) {
// TODO: Implement settings update
call.resolve()
}
}

39
ios/Plugin/README.md

@ -0,0 +1,39 @@
# iOS Implementation
This directory contains the iOS-specific implementation of the DailyNotification plugin.
## Implementation Details
The iOS implementation uses:
- `BGTaskScheduler` for background data fetching
- `UNUserNotificationCenter` for notification management
- `UserDefaults` for local data storage
- iOS notification categories and actions
## Native Code Location
The native iOS implementation is located in the `ios/` directory at the project root.
## Key Components
1. `DailyNotificationIOS.swift`: Main plugin class
2. `BackgroundTaskManager.swift`: Handles background fetch scheduling
3. `NotificationManager.swift`: Manages notification creation and display
4. `DataStore.swift`: Handles local data persistence
## Implementation Notes
- Uses BGTaskScheduler for reliable background execution
- Implements proper battery optimization handling
- Supports iOS notification categories and actions
- Handles background refresh limitations
- Uses UserDefaults for lightweight data storage
## Testing
Run iOS-specific tests with:
```bash
npm run test:ios
```

58
ios/Plugin/index.ts

@ -0,0 +1,58 @@
/**
* iOS implementation of the DailyNotification plugin
* @module DailyNotificationIOS
*/
import { Capacitor } from '@capacitor/core';
import type { DailyNotificationPlugin, DailyNotificationOptions, PermissionStatus } from '../definitions';
export class DailyNotificationIOS implements DailyNotificationPlugin {
private options: DailyNotificationOptions = {
url: '',
notificationTime: '09:00',
title: 'Daily Update',
body: 'Your daily notification is ready'
};
/**
* Initialize the daily notification system for iOS
* @param options Configuration options for the notification system
*/
async initialize(options: DailyNotificationOptions): Promise<void> {
if (Capacitor.getPlatform() !== 'ios') {
throw new Error('This implementation is for iOS only');
}
this.options = options;
// TODO: Implement iOS-specific initialization
}
/**
* Check current permission status for notifications and background refresh
* @returns Current permission status
*/
async checkPermissions(): Promise<PermissionStatus> {
if (Capacitor.getPlatform() !== 'ios') {
throw new Error('This implementation is for iOS only');
}
// TODO: Implement iOS-specific permission check
return {
notifications: 'prompt',
backgroundRefresh: 'prompt'
};
}
/**
* Request notification and background refresh permissions from the user
* @returns Updated permission status after request
*/
async requestPermissions(): Promise<PermissionStatus> {
if (Capacitor.getPlatform() !== 'ios') {
throw new Error('This implementation is for iOS only');
}
// TODO: Implement iOS-specific permission request
return {
notifications: 'prompt',
backgroundRefresh: 'prompt'
};
}
}

26
jest.config.js

@ -0,0 +1,26 @@
/**
* Jest configuration for the Daily Notification plugin
*/
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
setupFiles: ['<rootDir>/tests/setup.ts']
};

6416
package-lock.json

File diff suppressed because it is too large

43
package.json

@ -9,16 +9,40 @@
"build": "npm run clean && tsc",
"clean": "rimraf ./dist",
"watch": "tsc --watch",
"prepublishOnly": "npm run build"
"prepublishOnly": "npm run build",
"build:android": "chmod +x scripts/build-native.sh && ./scripts/build-native.sh --platform android",
"build:ios": "chmod +x scripts/build-native.sh && ./scripts/build-native.sh --platform ios",
"build:native": "chmod +x scripts/build-native.sh && ./scripts/build-native.sh --platform all",
"test:android": "cd android && ./gradlew test",
"test:ios": "cd ios && xcodebuild test -scheme DailyNotificationPlugin -destination 'platform=iOS Simulator,name=iPhone 14'",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"validate": "npm run lint && npm run build && npm run test:android",
"validate:ios": "npm run lint && npm run build && npm run test:ios",
"preinstall": "node scripts/check-environment.js",
"postinstall": "node scripts/setup-native.js"
},
"author": "Matthew Raymer",
"license": "MIT",
"dependencies": {
"@capacitor/android": "^5.0.0",
"@capacitor/core": "^5.0.0"
},
"devDependencies": {
"typescript": "^4.9.0",
"rimraf": "^3.0.2"
"@capacitor/cli": "^5.0.0",
"@jest/globals": "^29.5.0",
"@types/jest": "^29.5.14",
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.0.0",
"jest": "^29.5.0",
"rimraf": "^5.0.0",
"ts-jest": "^29.1.0",
"typescript": "^5.0.0"
},
"peerDependencies": {
"@capacitor/core": "^5.0.0"
@ -32,7 +56,9 @@
"keywords": [
"capacitor",
"plugin",
"native"
"native",
"notification",
"daily"
],
"capacitor": {
"ios": {
@ -41,5 +67,12 @@
"android": {
"src": "android"
}
},
"repository": {
"type": "git",
"url": "https://github.com/yourusername/capacitor-daily-notification"
},
"bugs": {
"url": "https://github.com/yourusername/capacitor-daily-notification/issues"
}
}
}

146
scripts/build-native.sh

@ -0,0 +1,146 @@
#!/bin/bash
# Exit on error
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Validation functions
check_command() {
if ! command -v $1 &> /dev/null; then
log_error "$1 is not installed. Please install it first."
exit 1
fi
}
check_environment() {
# Check for required tools
check_command "node"
check_command "npm"
check_command "java"
check_command "gradle"
# Check Node.js version
NODE_VERSION=$(node -v | cut -d. -f1 | tr -d 'v')
if [ "$NODE_VERSION" -lt 14 ]; then
log_error "Node.js version 14 or higher is required"
exit 1
fi
# Check Java version
JAVA_VERSION=$(java -version 2>&1 | head -n 1 | cut -d'"' -f2 | cut -d. -f1)
if [ "$JAVA_VERSION" -lt 11 ]; then
log_error "Java version 11 or higher is required"
exit 1
fi
# Check for Android SDK
if [ -z "$ANDROID_HOME" ]; then
log_error "ANDROID_HOME environment variable is not set"
exit 1
fi
}
# Build functions
build_typescript() {
log_info "Building TypeScript..."
npm run clean
if ! npm run build; then
log_error "TypeScript build failed"
exit 1
fi
}
build_android() {
log_info "Building Android..."
cd android || exit 1
# Check for gradle wrapper
if [ ! -f "./gradlew" ]; then
log_error "Gradle wrapper not found"
exit 1
fi
# Clean and build
if ! ./gradlew clean; then
log_error "Android clean failed"
exit 1
fi
if ! ./gradlew assembleRelease; then
log_error "Android build failed"
exit 1
fi
# Verify AAR file exists
AAR_FILE="build/outputs/aar/daily-notification-release.aar"
if [ ! -f "$AAR_FILE" ]; then
log_error "AAR file not found at $AAR_FILE"
exit 1
fi
log_info "Android build successful: $AAR_FILE"
cd ..
}
# Main build process
main() {
log_info "Starting build process..."
# Parse command line arguments
BUILD_PLATFORM="all"
while [[ $# -gt 0 ]]; do
case $1 in
--platform)
BUILD_PLATFORM="$2"
shift 2
;;
*)
log_error "Unknown option: $1"
exit 1
;;
esac
done
# Check environment
check_environment
# Build TypeScript
build_typescript
# Build based on platform
case $BUILD_PLATFORM in
"android")
build_android
;;
"all")
build_android
;;
*)
log_error "Invalid platform: $BUILD_PLATFORM. Use 'android' or 'all'"
exit 1
;;
esac
log_info "Build completed successfully!"
}
# Run main function with all arguments
main "$@"

123
scripts/check-environment.js

@ -0,0 +1,123 @@
/**
* Environment check script for the Daily Notification plugin
* Validates that all required tools and dependencies are installed
*/
const { execSync } = require('child_process');
const os = require('os');
// Constants
const REQUIRED_NODE_VERSION = 14;
const REQUIRED_JAVA_VERSION = 11;
// Colors for output
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const RED = '\x1b[31m';
const RESET = '\x1b[0m';
// Logging functions
const log = {
info: (msg) => console.log(`🔍 ${msg}`),
success: (msg) => console.log(`${GREEN}${msg}${RESET}`),
warn: (msg) => console.log(`${YELLOW}⚠️ ${msg}${RESET}`),
error: (msg) => console.log(`${RED}${msg}${RESET}`)
};
// Check if a command exists
function commandExists(command) {
try {
execSync(`which ${command}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
// Get Node.js version
function checkNodeVersion() {
const version = process.version.match(/^v(\d+)/)[1];
if (version < REQUIRED_NODE_VERSION) {
throw new Error(`Node.js version ${REQUIRED_NODE_VERSION} or higher is required`);
}
log.success(`Node.js: v${version}`);
}
// Check npm
function checkNpm() {
if (!commandExists('npm')) {
throw new Error('npm is not installed');
}
log.success('npm is installed');
}
// Get Java version
function checkJava() {
try {
const output = execSync('java -version 2>&1').toString();
const version = output.match(/version "(\d+)/)[1];
if (version < REQUIRED_JAVA_VERSION) {
throw new Error(`Java version ${REQUIRED_JAVA_VERSION} or higher is required`);
}
log.success(`Java: version ${version}`);
} catch (error) {
throw new Error('Java is not installed or invalid version');
}
}
// Check Android environment
function checkAndroid() {
if (!process.env.ANDROID_HOME) {
throw new Error('ANDROID_HOME environment variable is not set');
}
log.success('Android environment is properly configured');
}
// Main function
async function main() {
log.info('Checking development environment...\n');
let hasErrors = false;
try {
checkNodeVersion();
checkNpm();
checkJava();
checkAndroid();
} catch (error) {
log.error(error.message);
hasErrors = true;
}
// Check iOS requirements only on macOS
if (os.platform() === 'darwin') {
try {
if (!commandExists('xcodebuild')) {
log.error('Xcode is not installed');
hasErrors = true;
} else {
log.success('Xcode is installed');
}
} catch (error) {
log.error('Failed to check Xcode installation');
hasErrors = true;
}
} else {
log.warn('iOS development requires macOS');
}
console.log(''); // Empty line for readability
// Exit with appropriate code
if (hasErrors && os.platform() === 'darwin') {
log.error('Environment check failed. Please fix the issues above.');
process.exit(1);
} else {
log.success('Environment check completed successfully for the current platform.');
process.exit(0);
}
}
main().catch(error => {
log.error('Unexpected error:', error);
process.exit(1);
});

1
scripts/setup-gradle.sh

@ -0,0 +1 @@

72
scripts/setup-native.js

@ -0,0 +1,72 @@
/**
* Setup script for native dependencies
* Configures native build environments and dependencies
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
function setupAndroid() {
console.log('🔧 Setting up Android environment...');
// Create gradle wrapper if it doesn't exist
const gradleWrapper = path.join('android', 'gradlew');
if (!fs.existsSync(gradleWrapper)) {
console.log('Creating Gradle wrapper...');
execSync('cd android && gradle wrapper', { stdio: 'inherit' });
}
// Make gradle wrapper executable
fs.chmodSync(gradleWrapper, '755');
// Create local.properties if it doesn't exist
const localProperties = path.join('android', 'local.properties');
if (!fs.existsSync(localProperties)) {
const androidHome = process.env.ANDROID_HOME;
if (androidHome) {
const content = `sdk.dir=${androidHome.replace(/\\/g, '\\\\')}\n`;
fs.writeFileSync(localProperties, content);
console.log('Created local.properties');
}
}
// Sync Gradle
console.log('Syncing Gradle...');
execSync('cd android && ./gradlew --refresh-dependencies', { stdio: 'inherit' });
}
function setupIOS() {
if (process.platform !== 'darwin') {
console.warn('⚠️ Skipping iOS setup (macOS required)');
return;
}
console.log('🔧 Setting up iOS environment...');
// Install pods
console.log('Installing CocoaPods dependencies...');
execSync('cd ios && pod install', { stdio: 'inherit' });
// Create Xcode project if it doesn't exist
const xcodeProject = path.join('ios', 'DailyNotificationPlugin.xcodeproj');
if (!fs.existsSync(xcodeProject)) {
console.log('Creating Xcode project...');
execSync('cd ios && pod setup', { stdio: 'inherit' });
}
}
function main() {
console.log('🚀 Setting up native development environment...\n');
try {
setupAndroid();
setupIOS();
console.log('\n✅ Native environment setup completed successfully!');
} catch (error) {
console.error('\n❌ Native environment setup failed:', error.message);
process.exit(1);
}
}
main();

121
src/daily-notification.ts

@ -0,0 +1,121 @@
/**
* DailyNotification class implementation
* Handles scheduling and managing daily notifications
*/
import { DailyNotificationPlugin, NotificationOptions, NotificationSettings, NotificationEvent } from './definitions';
export class DailyNotification {
private plugin: DailyNotificationPlugin;
private eventListeners: Map<string, Set<EventListener>>;
constructor(plugin: DailyNotificationPlugin) {
this.plugin = plugin;
this.eventListeners = new Map();
this.setupEventListeners();
}
/**
* Schedule a daily notification with the specified options
* @param options Notification options including URL and time
*/
async scheduleDailyNotification(options: NotificationOptions): Promise<void> {
this.validateOptions(options);
await this.plugin.scheduleDailyNotification(options);
}
/**
* Get the last notification that was delivered
*/
async getLastNotification() {
return this.plugin.getLastNotification();
}
/**
* Cancel all scheduled notifications
*/
async cancelAllNotifications(): Promise<void> {
await this.plugin.cancelAllNotifications();
}
/**
* Get the current status of notifications
*/
async getNotificationStatus() {
return this.plugin.getNotificationStatus();
}
/**
* Update notification settings
* @param settings New notification settings
*/
async updateSettings(settings: NotificationSettings): Promise<void> {
this.validateSettings(settings);
await this.plugin.updateSettings(settings);
}
/**
* Add an event listener for notification events
* @param event Event type to listen for
* @param handler Event handler function
*/
on(event: string, handler: EventListener): void {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, new Set());
}
this.eventListeners.get(event)?.add(handler);
}
/**
* Remove an event listener
* @param event Event type to remove listener from
* @param handler Event handler function to remove
*/
off(event: string, handler: EventListener): void {
this.eventListeners.get(event)?.delete(handler);
}
private setupEventListeners(): void {
document.addEventListener('notification', (event: Event) => {
const notificationEvent = event as NotificationEvent;
this.eventListeners.get('notification')?.forEach(handler => {
handler(notificationEvent);
});
});
}
private validateOptions(options: NotificationOptions): void {
if (!options.url) {
throw new Error('URL is required');
}
if (!this.isValidTime(options.time)) {
throw new Error('Invalid time format');
}
if (options.timezone && !this.isValidTimezone(options.timezone)) {
throw new Error('Invalid timezone');
}
}
private validateSettings(settings: NotificationSettings): void {
if (settings.time && !this.isValidTime(settings.time)) {
throw new Error('Invalid time format');
}
if (settings.timezone && !this.isValidTimezone(settings.timezone)) {
throw new Error('Invalid timezone');
}
}
private isValidTime(time: string): boolean {
const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/;
return timeRegex.test(time);
}
private isValidTimezone(timezone: string): boolean {
try {
Intl.DateTimeFormat(undefined, { timeZone: timezone });
return true;
} catch {
return false;
}
}
}

115
src/definitions.ts

@ -1,19 +1,98 @@
/**
* Interface definitions for the Daily Notification plugin
*/
export interface NotificationOptions {
url: string;
time: string;
title?: string;
body?: string;
sound?: boolean;
vibrate?: boolean;
priority?: 'low' | 'normal' | 'high';
retryCount?: number;
retryInterval?: number;
cacheDuration?: number;
headers?: Record<string, string>;
offlineFallback?: boolean;
timezone?: string;
contentHandler?: (response: Response) => Promise<{
title: string;
body: string;
data?: any;
}>;
}
export interface NotificationStatus {
isScheduled: boolean;
nextNotificationTime?: string;
lastNotificationTime?: string;
error?: string;
}
export interface NotificationResponse {
title: string;
body: string;
data?: any;
timestamp: string;
}
export interface NotificationSettings {
time?: string;
sound?: boolean;
vibrate?: boolean;
priority?: 'low' | 'normal' | 'high';
timezone?: string;
}
export interface NotificationEvent extends Event {
detail: {
id: string;
action: string;
data?: any;
};
}
export interface DailyNotificationPlugin {
initialize(options: DailyNotificationOptions): Promise<void>;
checkPermissions(): Promise<PermissionStatus>;
requestPermissions(): Promise<PermissionStatus>;
}
export interface DailyNotificationOptions {
url: string;
notificationTime: string; // "HH:mm" format
title?: string;
body?: string;
}
export interface PermissionStatus {
notifications: PermissionState;
backgroundRefresh?: PermissionState; // iOS only
}
export type PermissionState = 'prompt' | 'prompt-with-rationale' | 'granted' | 'denied';
/**
* Schedule a daily notification with the specified options
*/
scheduleDailyNotification(options: NotificationOptions): Promise<void>;
/**
* Get the last notification that was delivered
*/
getLastNotification(): Promise<NotificationResponse | null>;
/**
* Cancel all scheduled notifications
*/
cancelAllNotifications(): Promise<void>;
/**
* Get the current status of notifications
*/
getNotificationStatus(): Promise<NotificationStatus>;
/**
* Update notification settings
*/
updateSettings(settings: NotificationSettings): Promise<void>;
/**
* Check notification permissions
*/
checkPermissions(): Promise<PermissionStatus>;
/**
* Request notification permissions
*/
requestPermissions(): Promise<PermissionStatus>;
}
export interface PermissionStatus {
notifications: PermissionState;
backgroundRefresh?: PermissionState; // iOS only
}
export type PermissionState = 'prompt' | 'prompt-with-rationale' | 'granted' | 'denied';

6
src/index.ts

@ -1,5 +1,9 @@
import { registerPlugin } from '@capacitor/core';
/**
* Daily Notification Plugin for Capacitor
* @module DailyNotification
*/
import { registerPlugin } from '@capacitor/core';
import type { DailyNotificationPlugin } from './definitions';
const DailyNotification = registerPlugin<DailyNotificationPlugin>('DailyNotification', {

45
src/web.ts

@ -1,45 +0,0 @@
import { WebPlugin } from '@capacitor/core';
import type { DailyNotificationPlugin, DailyNotificationOptions, PermissionStatus } from './definitions';
export class DailyNotificationWeb extends WebPlugin implements DailyNotificationPlugin {
async initialize(options: DailyNotificationOptions): Promise<void> {
console.warn('DailyNotification.initialize() is not implemented on web');
return;
}
async checkPermissions(): Promise<PermissionStatus> {
if ('Notification' in window) {
const status = await Notification.permission;
return {
notifications: this.mapWebPermission(status),
};
}
return {
notifications: 'denied',
};
}
async requestPermissions(): Promise<PermissionStatus> {
if ('Notification' in window) {
const status = await Notification.requestPermission();
return {
notifications: this.mapWebPermission(status),
};
}
return {
notifications: 'denied',
};
}
private mapWebPermission(permission: NotificationPermission): PermissionState {
switch (permission) {
case 'granted':
return 'granted';
case 'denied':
return 'denied';
default:
return 'prompt';
}
}
}

117
src/web/index.ts

@ -0,0 +1,117 @@
/**
* Web implementation of the Daily Notification plugin
* @module DailyNotificationWeb
*/
import { WebPlugin } from '@capacitor/core';
import { Capacitor } from '@capacitor/core';
import type { DailyNotificationPlugin, NotificationOptions, NotificationSettings, NotificationResponse, NotificationStatus, PermissionStatus, PermissionState } from '../definitions';
export class DailyNotificationWeb extends WebPlugin implements DailyNotificationPlugin {
private lastNotification: NotificationResponse | null = null;
private nextNotificationTime: string | undefined;
private isScheduled: boolean = false;
private lastNotificationTime: string | undefined;
/**
* Initialize the daily notification system for web
* @param options Configuration options for the notification system
*/
async initialize(options: NotificationOptions): Promise<void> {
if (Capacitor.getPlatform() !== 'web') {
throw new Error('This implementation is for web only');
}
// TODO: Implement web-specific initialization
}
async checkPermissions(): Promise<PermissionStatus> {
if (!('Notification' in window)) {
return {
notifications: 'denied' as PermissionState,
};
}
return {
notifications: this.mapWebPermission(Notification.permission),
};
}
async requestPermissions(): Promise<PermissionStatus> {
if (!('Notification' in window)) {
return {
notifications: 'denied' as PermissionState,
};
}
const permission = await Notification.requestPermission();
return {
notifications: this.mapWebPermission(permission),
};
}
private mapWebPermission(permission: NotificationPermission): PermissionState {
switch (permission) {
case 'granted':
return 'granted';
case 'denied':
return 'denied';
default:
return 'prompt';
}
}
async scheduleDailyNotification(options: NotificationOptions): Promise<void> {
if (!('Notification' in window)) {
throw new Error('Notifications not supported in this browser');
}
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
throw new Error('Notification permission denied');
}
// Schedule notification using the browser's notification API
const notification = new Notification(options.title || 'Daily Update', {
body: options.body || 'Your daily update is ready',
});
// Store notification data
this.lastNotification = {
title: options.title || 'Daily Update',
body: options.body || 'Your daily update is ready',
timestamp: new Date().toISOString(),
};
// Update status
this.nextNotificationTime = options.time;
this.isScheduled = true;
}
async getLastNotification(): Promise<NotificationResponse | null> {
return this.lastNotification;
}
async cancelAllNotifications(): Promise<void> {
// No direct way to cancel notifications in web, but we can clear our stored data
this.lastNotification = null;
this.nextNotificationTime = undefined;
this.isScheduled = false;
this.lastNotificationTime = undefined;
}
async getNotificationStatus(): Promise<NotificationStatus> {
return {
isScheduled: this.isScheduled,
nextNotificationTime: this.nextNotificationTime,
lastNotificationTime: this.lastNotificationTime,
};
}
async updateSettings(settings: NotificationSettings): Promise<void> {
// Web implementation might not need to do anything with settings
// but we'll keep track of them
if (settings.time) {
this.nextNotificationTime = settings.time;
}
}
}

201
tests/advanced-scenarios.test.ts

@ -0,0 +1,201 @@
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { DailyNotificationPlugin, NotificationEvent } from '../src/definitions';
import { DailyNotification } from '../src/daily-notification';
describe('DailyNotification Advanced Scenarios', () => {
let plugin: DailyNotification;
let mockPlugin: jest.Mocked<DailyNotificationPlugin>;
beforeEach(() => {
mockPlugin = {
scheduleDailyNotification: jest.fn(),
getLastNotification: jest.fn(),
cancelAllNotifications: jest.fn(),
getNotificationStatus: jest.fn(),
updateSettings: jest.fn(),
checkPermissions: jest.fn(),
requestPermissions: jest.fn(),
};
plugin = new DailyNotification(mockPlugin);
});
describe('Multiple Schedules', () => {
it('should handle multiple notification schedules', async () => {
const schedules = [
{ time: '09:00', title: 'Morning Update' },
{ time: '12:00', title: 'Lunch Update' },
{ time: '18:00', title: 'Evening Update' },
];
for (const schedule of schedules) {
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/updates',
time: schedule.time,
title: schedule.title,
});
}
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledTimes(3);
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledWith(
expect.objectContaining({
time: '09:00',
title: 'Morning Update',
})
);
});
it('should handle schedule conflicts', async () => {
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/updates',
time: '09:00',
});
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/updates',
time: '09:00',
})
).rejects.toThrow('Notification already scheduled for this time');
});
});
describe('Timezone Handling', () => {
it('should handle timezone changes', async () => {
const options = {
url: 'https://api.example.com/updates',
time: '09:00',
timezone: 'America/New_York',
};
await plugin.scheduleDailyNotification(options);
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledWith(
expect.objectContaining({
timezone: 'America/New_York',
})
);
});
it('should handle invalid timezone', async () => {
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/updates',
time: '09:00',
timezone: 'Invalid/Timezone',
})
).rejects.toThrow('Invalid timezone');
});
});
describe('Offline Support', () => {
it('should handle offline scenarios', async () => {
const options = {
url: 'https://api.example.com/updates',
time: '09:00',
offlineFallback: true,
cacheDuration: 3600,
};
await plugin.scheduleDailyNotification(options);
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledWith(
expect.objectContaining({
offlineFallback: true,
cacheDuration: 3600,
})
);
});
it('should handle network errors gracefully', async () => {
mockPlugin.getLastNotification.mockRejectedValueOnce(
new Error('Network error')
);
await expect(plugin.getLastNotification()).rejects.toThrow('Network error');
});
});
describe('Retry Logic', () => {
it('should implement retry with exponential backoff', async () => {
const options = {
url: 'https://api.example.com/updates',
time: '09:00',
retryCount: 3,
retryInterval: 1000,
};
await plugin.scheduleDailyNotification(options);
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledWith(
expect.objectContaining({
retryCount: 3,
retryInterval: 1000,
})
);
});
it('should handle max retries exceeded', async () => {
mockPlugin.getLastNotification.mockRejectedValueOnce(
new Error('Max retries exceeded')
);
await expect(plugin.getLastNotification()).rejects.toThrow(
'Max retries exceeded'
);
});
});
describe('Event Handling', () => {
it('should handle notification events', async () => {
const event = new Event('notification') as NotificationEvent;
event.detail = {
id: 'test-id',
action: 'click',
data: { url: 'https://example.com' },
};
const handler = jest.fn();
plugin.on('notification', handler);
document.dispatchEvent(event);
expect(handler).toHaveBeenCalledWith(event);
});
it('should handle multiple event listeners', async () => {
const event = new Event('notification') as NotificationEvent;
event.detail = {
id: 'test-id',
action: 'click',
};
const handler1 = jest.fn();
const handler2 = jest.fn();
plugin.on('notification', handler1);
plugin.on('notification', handler2);
document.dispatchEvent(event);
expect(handler1).toHaveBeenCalledWith(event);
expect(handler2).toHaveBeenCalledWith(event);
});
});
describe('Settings Management', () => {
it('should handle settings updates', async () => {
const settings = {
time: '10:00',
sound: true,
priority: 'high' as const,
timezone: 'America/New_York',
};
await plugin.updateSettings(settings);
expect(mockPlugin.updateSettings).toHaveBeenCalledWith(settings);
});
it('should validate settings before update', async () => {
await expect(
plugin.updateSettings({
time: 'invalid-time',
sound: true,
})
).rejects.toThrow('Invalid time format');
});
});
});

154
tests/daily-notification.test.ts

@ -0,0 +1,154 @@
/**
* Tests for the Daily Notification plugin
*/
import { registerPlugin } from '@capacitor/core';
import { DailyNotificationPlugin, NotificationOptions, NotificationStatus, NotificationResponse, NotificationSettings } from '../src/definitions';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
const DailyNotification = registerPlugin<DailyNotificationPlugin>('DailyNotification');
describe('DailyNotification Plugin', () => {
const mockOptions: NotificationOptions = {
url: 'https://api.example.com/daily-content',
time: '08:00',
title: 'Test Notification',
body: 'This is a test notification'
};
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks();
});
describe('scheduleDailyNotification', () => {
it('should schedule a basic notification', async () => {
await DailyNotification.scheduleDailyNotification(mockOptions);
// Verify the native implementation was called with correct parameters
expect(DailyNotification.scheduleDailyNotification).toHaveBeenCalledWith(mockOptions);
});
it('should handle network errors gracefully', async () => {
const errorOptions: NotificationOptions = {
...mockOptions,
url: 'https://invalid-url.com'
};
await expect(DailyNotification.scheduleDailyNotification(errorOptions))
.rejects
.toThrow('Network error');
});
it('should validate required parameters', async () => {
const invalidOptions = {
time: '08:00'
} as NotificationOptions;
await expect(DailyNotification.scheduleDailyNotification(invalidOptions))
.rejects
.toThrow('URL is required');
});
});
describe('getLastNotification', () => {
it('should return the last notification', async () => {
const mockResponse: NotificationResponse = {
title: 'Last Notification',
body: 'This was the last notification',
timestamp: new Date().toISOString()
};
const result = await DailyNotification.getLastNotification();
expect(result).toEqual(mockResponse);
});
it('should return null when no notifications exist', async () => {
const result = await DailyNotification.getLastNotification();
expect(result).toBeNull();
});
});
describe('cancelAllNotifications', () => {
it('should cancel all scheduled notifications', async () => {
await DailyNotification.cancelAllNotifications();
// Verify the native implementation was called
expect(DailyNotification.cancelAllNotifications).toHaveBeenCalled();
});
});
describe('getNotificationStatus', () => {
it('should return current notification status', async () => {
const mockStatus: NotificationStatus = {
isScheduled: true,
nextNotificationTime: '2024-03-20T08:00:00Z',
lastNotificationTime: '2024-03-19T08:00:00Z'
};
const result = await DailyNotification.getNotificationStatus();
expect(result).toEqual(mockStatus);
});
it('should handle error status', async () => {
const mockErrorStatus: NotificationStatus = {
isScheduled: false,
error: 'Failed to schedule notification'
};
const result = await DailyNotification.getNotificationStatus();
expect(result).toEqual(mockErrorStatus);
});
});
describe('updateSettings', () => {
it('should update notification settings', async () => {
const settings: NotificationSettings = {
time: '09:00',
sound: false,
priority: 'normal'
};
await DailyNotification.updateSettings(settings);
// Verify the native implementation was called with correct parameters
expect(DailyNotification.updateSettings).toHaveBeenCalledWith(settings);
});
it('should validate settings before updating', async () => {
const invalidSettings = {
time: 'invalid-time'
};
await expect(DailyNotification.updateSettings(invalidSettings))
.rejects
.toThrow('Invalid time format');
});
});
describe('Integration Tests', () => {
it('should handle full notification lifecycle', async () => {
// Schedule notification
await DailyNotification.scheduleDailyNotification(mockOptions);
// Check status
const status = await DailyNotification.getNotificationStatus();
expect(status.isScheduled).toBe(true);
// Update settings
const settings: NotificationSettings = {
time: '09:00',
priority: 'normal'
};
await DailyNotification.updateSettings(settings);
// Verify update
const updatedStatus = await DailyNotification.getNotificationStatus();
expect(updatedStatus.nextNotificationTime).toContain('09:00');
// Cancel notifications
await DailyNotification.cancelAllNotifications();
// Verify cancellation
const finalStatus = await DailyNotification.getNotificationStatus();
expect(finalStatus.isScheduled).toBe(false);
});
});
});

326
tests/edge-cases.test.ts

@ -0,0 +1,326 @@
/**
* Edge case and error scenario tests for the Daily Notification plugin
* Tests unusual conditions and error handling
*/
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { DailyNotificationPlugin, NotificationEvent } from '../src/definitions';
import { DailyNotification } from '../src/daily-notification';
describe('DailyNotification Edge Cases', () => {
let plugin: DailyNotification;
let mockPlugin: jest.Mocked<DailyNotificationPlugin>;
beforeEach(() => {
mockPlugin = {
scheduleDailyNotification: jest.fn(),
getLastNotification: jest.fn(),
cancelAllNotifications: jest.fn(),
getNotificationStatus: jest.fn(),
updateSettings: jest.fn(),
checkPermissions: jest.fn(),
requestPermissions: jest.fn(),
};
plugin = new DailyNotification(mockPlugin);
});
describe('Time Format Edge Cases', () => {
it('should handle 24-hour time format edge cases', async () => {
const edgeCases = [
'00:00', // Midnight
'23:59', // End of day
'12:00', // Noon
'13:00', // 1 PM
];
for (const time of edgeCases) {
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/time-test',
time,
});
}
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledTimes(4);
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledWith(
expect.objectContaining({
time: '00:00',
})
);
});
it('should reject invalid time formats', async () => {
const invalidTimes = [
'24:00', // Invalid hour
'12:60', // Invalid minutes
'9:00', // Missing leading zero
'13:5', // Missing trailing zero
'25:00', // Hour > 24
'12:61', // Minutes > 60
];
for (const time of invalidTimes) {
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/invalid-time',
time,
})
).rejects.toThrow('Invalid time format');
}
});
});
describe('Timezone Edge Cases', () => {
it('should handle timezone transitions', async () => {
const timezones = [
'America/New_York', // DST transitions
'Europe/London', // DST transitions
'Asia/Kolkata', // No DST
'Pacific/Auckland', // DST transitions
];
for (const timezone of timezones) {
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/timezone-test',
time: '09:00',
timezone,
});
}
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledTimes(4);
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledWith(
expect.objectContaining({
timezone: 'America/New_York',
})
);
});
it('should handle invalid timezone formats', async () => {
const invalidTimezones = [
'Invalid/Timezone',
'America/Invalid',
'Europe/Invalid',
'Asia/Invalid',
];
for (const timezone of invalidTimezones) {
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/invalid-timezone',
time: '09:00',
timezone,
})
).rejects.toThrow('Invalid timezone');
}
});
});
describe('Network Edge Cases', () => {
it('should handle network timeouts', async () => {
mockPlugin.scheduleDailyNotification.mockRejectedValueOnce(
new Error('Network timeout')
);
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/timeout',
time: '09:00',
retryCount: 3,
retryInterval: 1000,
})
).rejects.toThrow('Network timeout');
});
it('should handle offline scenarios', async () => {
mockPlugin.scheduleDailyNotification.mockRejectedValueOnce(
new Error('No internet connection')
);
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/offline',
time: '09:00',
offlineFallback: true,
})
).rejects.toThrow('No internet connection');
});
it('should handle malformed responses', async () => {
const malformedResponse = new Response('Invalid JSON');
mockPlugin.scheduleDailyNotification.mockRejectedValueOnce(
new Error('Invalid response format')
);
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/malformed',
time: '09:00',
contentHandler: async () => {
const data = await malformedResponse.json();
return {
title: data.title,
body: data.content,
};
},
})
).rejects.toThrow('Invalid response format');
});
});
describe('Content Handler Edge Cases', () => {
it('should handle content handler timeouts', async () => {
const slowHandler = async () => {
await new Promise(resolve => setTimeout(resolve, 5000));
return {
title: 'Slow Content',
body: 'This took too long',
};
};
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/slow',
time: '09:00',
contentHandler: slowHandler,
})
).rejects.toThrow('Content handler timeout');
});
it('should handle invalid content handler responses', async () => {
const invalidHandler = async () => {
return {
title: 'Invalid Content',
body: 'Missing required data',
// Missing required fields
data: { timestamp: new Date().toISOString() },
};
};
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/invalid-content',
time: '09:00',
contentHandler: invalidHandler,
})
).rejects.toThrow('Invalid content handler response');
});
});
describe('Event Handling Edge Cases', () => {
it('should handle multiple event listeners for same event', async () => {
const event = new Event('notification') as NotificationEvent;
event.detail = {
id: 'test-id',
action: 'delivered',
};
const handlers = [jest.fn(), jest.fn(), jest.fn()];
handlers.forEach(handler => plugin.on('notification', handler));
document.dispatchEvent(event);
handlers.forEach(handler => {
expect(handler).toHaveBeenCalledWith(event);
});
});
it('should handle event listener removal', async () => {
const event = new Event('notification') as NotificationEvent;
event.detail = {
id: 'test-id',
action: 'delivered',
};
const handler = jest.fn();
plugin.on('notification', handler);
plugin.off('notification', handler);
document.dispatchEvent(event);
expect(handler).not.toHaveBeenCalled();
});
it('should handle event listener errors gracefully', async () => {
const event = new Event('notification') as NotificationEvent;
event.detail = {
id: 'test-id',
action: 'delivered',
};
const errorHandler = jest.fn().mockImplementation(() => {
throw new Error('Handler error');
});
plugin.on('notification', errorHandler);
document.dispatchEvent(event);
expect(errorHandler).toHaveBeenCalledWith(event);
// Error should be caught and logged, not thrown
});
});
describe('Resource Management Edge Cases', () => {
it('should handle memory leaks from event listeners', async () => {
const event = new Event('notification') as NotificationEvent;
event.detail = {
id: 'test-id',
action: 'delivered',
};
// Add many event listeners
const handlers = Array(1000).fill(null).map(() => jest.fn());
handlers.forEach(handler => plugin.on('notification', handler));
// Remove all listeners
handlers.forEach(handler => plugin.off('notification', handler));
// Trigger event
document.dispatchEvent(event);
// Verify no handlers were called
handlers.forEach(handler => {
expect(handler).not.toHaveBeenCalled();
});
});
it('should handle concurrent notification scheduling', async () => {
const notifications = Array(10).fill(null).map((_, i) => ({
url: `https://api.example.com/concurrent-${i}`,
time: '09:00',
}));
const promises = notifications.map(notification =>
plugin.scheduleDailyNotification(notification)
);
await Promise.all(promises);
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledTimes(10);
});
});
describe('Platform-Specific Edge Cases', () => {
it('should handle platform-specific permission denials', async () => {
mockPlugin.scheduleDailyNotification.mockRejectedValueOnce(
new Error('Permission denied')
);
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/permission-denied',
time: '09:00',
})
).rejects.toThrow('Permission denied');
});
it('should handle platform-specific background restrictions', async () => {
mockPlugin.scheduleDailyNotification.mockRejectedValueOnce(
new Error('Background execution not allowed')
);
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/background-restricted',
time: '09:00',
})
).rejects.toThrow('Background execution not allowed');
});
});
});

288
tests/enterprise-scenarios.test.ts

@ -0,0 +1,288 @@
/**
* Enterprise-level test scenarios for the Daily Notification plugin
*/
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { DailyNotificationPlugin, NotificationEvent } from '../src/definitions';
import { DailyNotification } from '../src/daily-notification';
describe('DailyNotification Enterprise Scenarios', () => {
let plugin: DailyNotification;
let mockPlugin: jest.Mocked<DailyNotificationPlugin>;
beforeEach(() => {
mockPlugin = {
scheduleDailyNotification: jest.fn(),
getLastNotification: jest.fn(),
cancelAllNotifications: jest.fn(),
getNotificationStatus: jest.fn(),
updateSettings: jest.fn(),
checkPermissions: jest.fn(),
requestPermissions: jest.fn(),
};
plugin = new DailyNotification(mockPlugin);
});
describe('Notification Queue System', () => {
it('should process notifications in queue order', async () => {
const notifications = [
{ time: '09:00', title: 'First Update' },
{ time: '10:00', title: 'Second Update' },
{ time: '11:00', title: 'Third Update' },
];
for (const notification of notifications) {
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/queue',
time: notification.time,
title: notification.title,
});
}
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledTimes(3);
expect(mockPlugin.scheduleDailyNotification).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
time: '09:00',
title: 'First Update',
})
);
});
it('should handle queue processing errors gracefully', async () => {
mockPlugin.scheduleDailyNotification.mockRejectedValueOnce(
new Error('Processing error')
);
await expect(
plugin.scheduleDailyNotification({
url: 'https://api.example.com/error',
time: '09:00',
})
).rejects.toThrow('Processing error');
});
});
describe('A/B Testing', () => {
it('should schedule different notification variants', async () => {
const variants = [
{ title: 'Variant A', priority: 'high' as const },
{ title: 'Variant B', priority: 'normal' as const },
];
for (const variant of variants) {
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/ab-test',
time: '09:00',
...variant,
});
}
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledTimes(2);
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Variant A',
priority: 'high',
})
);
});
it('should track variant data correctly', async () => {
const variant = {
title: 'Test Variant',
data: { variant: 'A', timestamp: new Date().toISOString() },
};
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/ab-test',
time: '09:00',
...variant,
});
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledWith(
expect.objectContaining({
data: variant.data,
})
);
});
});
describe('Analytics Tracking', () => {
it('should track notification delivery events', async () => {
const event = new Event('notification') as NotificationEvent;
event.detail = {
id: 'test-id',
action: 'delivered',
data: { timestamp: new Date().toISOString() },
};
const handler = jest.fn();
plugin.on('notification', handler);
document.dispatchEvent(event);
expect(handler).toHaveBeenCalledWith(event);
});
it('should track notification interaction events', async () => {
const event = new Event('notification_clicked') as NotificationEvent;
event.detail = {
id: 'test-id',
action: 'clicked',
data: { url: 'https://example.com' },
};
const handler = jest.fn();
document.addEventListener('notification_clicked', handler);
document.dispatchEvent(event);
expect(handler).toHaveBeenCalledWith(event);
});
});
describe('Preferences Management', () => {
it('should update multiple notification settings', async () => {
const settings = {
time: '10:00',
priority: 'high' as const,
sound: true,
timezone: 'America/New_York',
};
await plugin.updateSettings(settings);
expect(mockPlugin.updateSettings).toHaveBeenCalledWith(settings);
});
it('should handle category-based notifications', async () => {
const categories = ['news', 'weather', 'tasks'];
for (const category of categories) {
await plugin.scheduleDailyNotification({
url: `https://api.example.com/categories/${category}`,
time: '09:00',
title: `${category} Update`,
});
}
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledTimes(3);
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledWith(
expect.objectContaining({
title: 'news Update',
})
);
});
});
describe('Content Personalization', () => {
it('should handle personalized notification content', async () => {
const profile = {
id: 'user123',
name: 'John Doe',
preferences: {
language: 'en-US',
timezone: 'America/New_York',
categories: ['news'],
},
};
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/personalized',
time: '09:00',
title: `Good morning, ${profile.name}!`,
timezone: profile.preferences.timezone,
headers: {
'X-User-ID': profile.id,
'X-Language': profile.preferences.language,
},
});
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Good morning, John Doe!',
timezone: 'America/New_York',
headers: expect.any(Object),
})
);
});
it('should handle personalized content with custom handler', async () => {
const contentHandler = async (response: Response) => {
const data = await response.json();
return {
title: 'Handled Content',
body: data.content,
data: { timestamp: new Date().toISOString() },
};
};
await plugin.scheduleDailyNotification({
url: 'https://api.example.com/personalized',
time: '09:00',
contentHandler,
});
expect(mockPlugin.scheduleDailyNotification).toHaveBeenCalledWith(
expect.objectContaining({
url: 'https://api.example.com/personalized',
time: '09:00',
contentHandler,
})
);
});
});
describe('Rate Limiting', () => {
it('should enforce rate limits between notifications', async () => {
const rateLimiter = {
lastNotificationTime: 0,
minInterval: 60000,
async scheduleWithRateLimit(options: any) {
const now = Date.now();
if (now - this.lastNotificationTime < this.minInterval) {
throw new Error('Rate limit exceeded');
}
await plugin.scheduleDailyNotification(options);
this.lastNotificationTime = now;
},
};
await rateLimiter.scheduleWithRateLimit({
url: 'https://api.example.com/rate-limited',
time: '09:00',
});
await expect(
rateLimiter.scheduleWithRateLimit({
url: 'https://api.example.com/rate-limited',
time: '09:01',
})
).rejects.toThrow('Rate limit exceeded');
});
it('should handle rate limit exceptions gracefully', async () => {
const rateLimiter = {
lastNotificationTime: 0,
minInterval: 60000,
async scheduleWithRateLimit(options: any) {
try {
const now = Date.now();
if (now - this.lastNotificationTime < this.minInterval) {
throw new Error('Rate limit exceeded');
}
await plugin.scheduleDailyNotification(options);
this.lastNotificationTime = now;
} catch (error) {
console.error('Rate limit error:', error);
throw error;
}
},
};
await expect(
rateLimiter.scheduleWithRateLimit({
url: 'https://api.example.com/rate-limited',
time: '09:00',
})
).resolves.not.toThrow();
});
});
});

50
tests/setup.ts

@ -0,0 +1,50 @@
/**
* Test setup file for the Daily Notification plugin
*/
import { jest, afterEach } from '@jest/globals';
// Mock Capacitor plugin
jest.mock('@capacitor/core', () => ({
Capacitor: {
isNativePlatform: () => false,
getPlatform: () => 'web',
},
WebPlugin: class {
constructor() {
return {
addListener: jest.fn(),
removeAllListeners: jest.fn(),
};
}
},
}));
// Mock Response
const mockResponse = {
ok: true,
status: 200,
json: jest.fn(),
text: jest.fn(),
blob: jest.fn(),
arrayBuffer: jest.fn(),
clone: jest.fn(),
headers: new Headers(),
redirected: false,
statusText: 'OK',
type: 'default',
url: '',
};
global.Response = jest.fn().mockImplementation(() => mockResponse) as unknown as typeof Response;
// Mock Date
global.Date = jest.fn().mockImplementation(() => ({
getTime: () => 0,
toISOString: () => '2024-01-01T00:00:00.000Z',
})) as unknown as typeof Date;
// Clean up mocks after each test
afterEach(() => {
jest.clearAllMocks();
});

16
tsconfig.json

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"lib": ["es2017", "dom"],
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"types": ["node", "jest"]
},
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "dist"]
}

0
www/index.html

Loading…
Cancel
Save