- 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
@ -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. |
@ -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. |
@ -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. |
@ -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 |
@ -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. |
@ -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 |
@ -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. |
@ -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. |
@ -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. |
@ -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 |
@ -0,0 +1,2 @@ |
|||
/build/* |
|||
!/build/.npmkeep |
@ -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") |
|||
} |
@ -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') |
|||
} |
@ -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 |
@ -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()); |
|||
} |
|||
} |
@ -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> |
@ -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 |
|||
``` |
@ -0,0 +1,5 @@ |
|||
package com.example.app; |
|||
|
|||
import com.getcapacitor.BridgeActivity; |
|||
|
|||
public class MainActivity extends BridgeActivity {} |
@ -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(); |
|||
} |
|||
} |
@ -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()); |
|||
} |
|||
} |
@ -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' }; |
|||
} |
|||
} |
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 17 KiB |
@ -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> |
@ -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> |
After Width: | Height: | Size: 3.9 KiB |
@ -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> |
@ -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> |
@ -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> |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,4 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<color name="ic_launcher_background">#FFFFFF</color> |
|||
</resources> |
@ -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> |
@ -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> |
@ -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> |
@ -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); |
|||
} |
|||
} |
@ -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' |
|||
} |
@ -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') |
@ -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 |
@ -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 |
@ -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" "$@" |
@ -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 |
@ -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' |
@ -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' |
|||
} |
@ -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; |
@ -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 |
|||
}; |
@ -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, |
|||
}; |
@ -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 |
|||
}; |
@ -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() |
|||
} |
|||
} |
@ -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 |
|||
``` |
@ -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' |
|||
}; |
|||
} |
|||
} |
@ -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'] |
|||
}; |
@ -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 "$@" |
@ -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); |
|||
}); |
@ -0,0 +1 @@ |
|||
|
@ -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(); |
@ -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; |
|||
} |
|||
} |
|||
} |
@ -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'; |
@ -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'; |
|||
} |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
|||
} |
@ -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'); |
|||
}); |
|||
}); |
|||
}); |
@ -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); |
|||
}); |
|||
}); |
|||
}); |
@ -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'); |
|||
}); |
|||
}); |
|||
}); |
@ -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(); |
|||
}); |
|||
}); |
|||
}); |
@ -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(); |
|||
}); |
@ -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"] |
|||
} |