diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..a377592
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,73 @@
+name: CI Pipeline
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ pipeline:
+ name: Lint, Type Check & Publish
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
+ - name: Cache node_modules
+ id: cache-modules
+ uses: actions/cache@v4
+ with:
+ path: node_modules
+ key: 20.x-${{ runner.OS }}-build-${{ hashFiles('package.json') }}
+
+ - name: Install Dependencies
+ if: steps.cache-modules.outputs.cache-hit != 'true'
+ run: npm install
+
+ - name: Lint and format check
+ run: |
+ npm run lint &
+ npm run prettier-check &
+ wait
+
+ - name: Type Check
+ run: npm run type-check
+
+ - name: Publish
+ id: publish
+ uses: JS-DevTools/npm-publish@v4
+ with:
+ token: ${{ secrets.NPM_AUTH_TOKEN }}
+
+ - name: Send Slack notification for Success
+ if: success()
+ uses: slackapi/slack-github-action@v2.1.1
+ with:
+ webhook: ${{ secrets.SLACK_SUCCESS_WEBHOOK_URL }}
+ webhook-type: incoming-webhook
+ payload: |
+ text: "✅ CI Pipeline Success - Published to NPM: ${{ steps.publish.outputs.old-version }} → ${{ steps.publish.outputs.version }}"
+ blocks:
+ - type: "section"
+ text:
+ type: "mrkdwn"
+ text: "*✅ CI Pipeline Success*\n\n*Published to NPM:* `${{ steps.publish.outputs.old-version }}` → `${{ steps.publish.outputs.version }}`\n*Repository:* ${{ github.repository }}\n*Commit:* <${{ github.event.head_commit.url }}|${{ github.sha }}>\n*Workflow:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Workflow>"
+
+ - name: Send Slack notification for Failure
+ if: failure() || cancelled()
+ uses: slackapi/slack-github-action@v2.1.1
+ with:
+ webhook: ${{ secrets.SLACK_FAILED_WEBHOOK_URL }}
+ webhook-type: incoming-webhook
+ payload: |
+ text: "❌ CI Pipeline Failed"
+ blocks:
+ - type: "section"
+ text:
+ type: "mrkdwn"
+ text: "*❌ CI Pipeline Failed*\n\n*Repository:* ${{ github.repository }}\n*Commit:* <${{ github.event.head_commit.url }}|${{ github.sha }}>\n*Workflow:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Workflow>"
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 46b93b5..01f80dc 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -2,6 +2,7 @@ name: Pull Request Pipeline
on:
pull_request:
+ types: [opened, synchronize, reopened, ready_for_review]
branches:
- main
@@ -9,14 +10,33 @@ jobs:
pull_request_pipeline:
name: Pull Request Pipeline
runs-on: ubuntu-latest
-
+ if: github.event.pull_request.draft == false
+
steps:
- - uses: actions/checkout@v3
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '18.x'
-
- - name: Install Dependencies
- run: npm install
\ No newline at end of file
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
+ - name: Cache node_modules
+ id: cache-modules
+ uses: actions/cache@v4
+ with:
+ path: node_modules
+ key: 20.x-${{ runner.OS }}-build-${{ hashFiles('package.json') }}
+
+ - name: Install Dependencies
+ if: steps.cache-modules.outputs.cache-hit != 'true'
+ run: npm ci --prefer-offline
+
+ - name: Lint and format check
+ run: |
+ npm run lint &
+ npm run prettier-check &
+ wait
+
+ - name: Type Check
+ run: npm run type-check
diff --git a/.gitignore b/.gitignore
index f778c2c..368e4e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,12 @@ node_modules
.DS_Store
**/local.properties
android/.gradle
+android/build
+android/app/build
+ios/build
+ios/Pods
+ios/*.xcworkspace
+ios/*.xcuserdata
.history
+dist
+*.log
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..6f3427e
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,7 @@
+{
+ "semi": true,
+ "trailingComma": "none",
+ "singleQuote": true,
+ "printWidth": 80
+}
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d064967
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2024 NotificationAPI
+
+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.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6d73c2b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,619 @@
+# NotificationAPI React Native SDK
+
+A React Native SDK for integrating [NotificationAPI](https://notificationapi.com) push notifications into your mobile app.
+
+## 🚀 Push Notification Support
+
+**Cross-platform push notifications with native performance:**
+
+- **Android**: Full FCM (Firebase Cloud Messaging) support
+- **iOS**: Direct APN (Apple Push Notifications) integration
+
+This means you get native push notifications on both platforms with optimal performance.
+
+## 🚀 Quick Start
+
+### 1. Installation
+
+```bash
+npm install @notificationapi/react-native
+# or
+yarn add @notificationapi/react-native
+```
+
+### 2. Setup (One Line!)
+
+```typescript
+import NotificationAPI from '@notificationapi/react-native';
+
+// That's it! This handles initialization, user identification, and permission requests
+await NotificationAPI.setup({
+ clientId: 'your_client_id_here',
+ userId: 'user123',
+ autoRequestPermission: true, // automatically request push permissions
+ region: 'us' // 'us' (default), 'eu', or 'ca'
+});
+```
+
+### 3. Listen to Notifications (Optional)
+
+```typescript
+import { getEventEmitter, Events } from '@notificationapi/react-native';
+
+const eventEmitter = getEventEmitter();
+
+// Listen to notifications received while app is open
+eventEmitter.addListener(Events.NOTIFICATION_RECEIVED, (notification) => {
+ console.log('Received notification:', notification.title);
+});
+
+// Listen to notifications that opened the app
+eventEmitter.addListener(Events.NOTIFICATION_ON_CLICK, (notification) => {
+ console.log('App opened from notification:', notification.title);
+ // Handle deep linking or navigation
+});
+
+// Listen to push token updates
+eventEmitter.addListener(Events.PUSH_TOKEN_RECEIVED, (event) => {
+ console.log('Push token received:', event.token);
+});
+```
+
+### 4. Check Status
+
+```typescript
+// Check if SDK is ready
+if (NotificationAPI.isReady) {
+ console.log('NotificationAPI is ready!');
+}
+
+// Get the current user
+const userId = NotificationAPI.currentUser;
+
+// Get push token
+const token = await NotificationAPI.getPushToken();
+```
+
+## 📱 Complete Example
+
+```typescript
+import React, { useEffect } from 'react';
+import { View, Button, Alert } from 'react-native';
+import NotificationAPI, { getEventEmitter, Events } from '@notificationapi/react-native';
+
+function App() {
+ useEffect(() => {
+ // Setup NotificationAPI
+ NotificationAPI.setup({
+ clientId: 'your_client_id',
+ userId: 'user123',
+ autoRequestPermission: true,
+ }).catch((error) => {
+ console.error('Failed to setup NotificationAPI:', error);
+ });
+
+ // Listen for notifications
+ const eventEmitter = getEventEmitter();
+
+ const receivedListener = eventEmitter.addListener(
+ Events.NOTIFICATION_RECEIVED,
+ (notification) => {
+ Alert.alert('New Notification', notification.title);
+ }
+ );
+
+ const clickListener = eventEmitter.addListener(
+ Events.NOTIFICATION_ON_CLICK,
+ (notification) => {
+ Alert.alert('Notification Clicked', notification.title);
+ // Handle navigation or deep linking
+ }
+ );
+
+ return () => {
+ receivedListener.remove();
+ clickListener.remove();
+ };
+ }, []);
+
+ const handleRequestPermission = async () => {
+ const granted = await NotificationAPI.requestPermission();
+ Alert.alert(
+ 'Permission',
+ granted ? 'Permission granted' : 'Permission denied'
+ );
+ };
+
+ return (
+
+
+
+ );
+}
+
+export default App;
+```
+
+## 🌙 Background Notifications (App Closed)
+
+Background notifications are automatically handled by the native modules. The SDK uses:
+
+- **FCM** for Android background notifications
+- **APN** for iOS background notifications
+
+No additional setup is required for basic background notification handling.
+
+### Handling Notification Taps
+
+When users tap a notification while the app is terminated, it's automatically handled:
+
+```typescript
+const eventEmitter = getEventEmitter();
+
+eventEmitter.addListener(Events.NOTIFICATION_ON_CLICK, (notification) => {
+ console.log('Notification tapped:', notification.title);
+
+ // Handle deep linking or navigation
+ if (notification.data?.deepLink) {
+ // Navigate to specific screen
+ navigation.navigate(notification.data.deepLink);
+ }
+});
+```
+
+## 🔧 API Reference
+
+### NotificationAPI
+
+| Method | Description | Returns |
+| --------------------- | ----------------------------------------------------------------------------------- | ------------------------- |
+| `setup(options)` | One-call setup with initialization, identification, and optional permission request | `Promise` |
+| `requestPermission()` | Request push notification permission | `Promise` |
+| `getPushToken()` | Get the current push token (FCM on Android, APN on iOS) | `Promise` |
+| `getDeviceInfo()` | Get device information | `Promise` |
+| `getService()` | Get the API service instance for advanced usage | `NotificationAPIService` |
+
+### Setup Parameters
+
+```typescript
+await NotificationAPI.setup({
+ clientId: string, // Your NotificationAPI client ID (required)
+ userId: string, // User's unique identifier (required)
+ hashedUserId?: string, // Hashed user ID for privacy (optional)
+ region?: string, // 'us' (default), 'eu', or 'ca'
+ autoRequestPermission?: boolean, // Auto-request push permission (default: true)
+ baseUrl?: string, // Custom base URL (overrides region)
+});
+```
+
+### Properties
+
+- `NotificationAPI.isReady` (boolean): Check if SDK is initialized
+- `NotificationAPI.currentUser` (string | null): Get current user ID
+
+## Events
+
+### Events.NOTIFICATION_PERMISSIONS_REQUESTED
+
+Emitted when notification permission is requested.
+
+**Event data:**
+
+```typescript
+{
+ isGranted: boolean;
+}
+```
+
+### Events.NOTIFICATION_ON_CLICK
+
+Emitted when a notification is clicked/tapped.
+
+**Event data:**
+
+```typescript
+{
+ messageId: string;
+ senderId: string;
+ ttl: number;
+ title: string;
+ body: string;
+ data: Record;
+}
+```
+
+### Events.PUSH_TOKEN_RECEIVED
+
+Emitted when a push token is received.
+
+**Event data:**
+
+```typescript
+{
+ token: string;
+ type: 'FCM' | 'APN';
+}
+```
+
+### Events.NOTIFICATION_RECEIVED
+
+Emitted when a notification is received (foreground).
+
+**Event data:**
+
+```typescript
+{
+ messageId: string;
+ senderId: string;
+ title: string;
+ body: string;
+ data?: Record;
+}
+```
+
+## 🔒 Privacy & Security
+
+- User IDs can be hashed for additional privacy
+- All communication uses HTTPS
+- Push tokens are securely stored in NotificationAPI backend
+
+## 🛠️ Platform Setup
+
+### Requirements
+
+- **React Native**: >= 0.73.0
+- **React**: >= 18.0.0
+- **New Architecture**: Required (enabled by default in React Native 0.73+)
+
+> **Note**: This SDK uses TurboModule (React Native's New Architecture), which requires React Native 0.73.0 or higher. If you're using React Native 0.68-0.72, you'll need to enable the New Architecture manually.
+
+### Android (Firebase Required)
+
+The setup process is as follows:
+
+1. [Set up a Firebase Project](#1-set-up-a-firebase-project)
+2. [Connect NotificationAPI to FCM](#2-connect-notificationapi-to-fcm)
+3. [Configure your React Native project for Firebase](#3-configure-your-react-native-project-for-firebase)
+4. [Add FirebaseMessagingService](#4-add-firebasemessagingservice)
+5. [Install and initialize the NotificationAPI React Native SDK](#5-install-and-initialize-the-notificationapi-react-native-sdk)
+
+---
+
+#### 1. Set up a Firebase Project
+
+If you don't have one already, you'll need to create a Firebase project.
+
+1. Go to the [Firebase Console](https://console.firebase.google.com/) and create a new project.
+2. Within your new project, navigate to **Project Settings** > **General**.
+3. Click the Android icon to add an Android app to your project. Use your React Native app's package name (e.g., `com.example.myapp`).
+ - Find your package name in `android/app/build.gradle` under `applicationId`
+4. Follow the on-screen instructions to register the app, and download the `google-services.json` file.
+5. Place the downloaded `google-services.json` file into the `android/app/` directory of your React Native project.
+
+---
+
+#### 2. Connect NotificationAPI to FCM
+
+To allow NotificationAPI to send notifications on your behalf, you need to provide it with your Firebase project's credentials.
+
+1. In the Firebase Console, go to **Project Settings** > **Service Accounts**.
+2. Click **Generate new private key**. A JSON file containing your service account key will be downloaded.
+
+
+⚠️ Important
+
+Treat this file like a password. Never commit it to version control or expose it in your client-side application.
+
+
+
+3. Go to your [NotificationAPI Dashboard](https://app.notificationapi.com/) and navigate to the **Integrations** page.
+4. Find the **Firebase Cloud Messaging (FCM)** integration and click **Configure**.
+5. Upload the service account JSON file you downloaded from Firebase.
+
+Your NotificationAPI account is now connected to your Firebase project.
+
+---
+
+#### 3. Configure your React Native project for Firebase
+
+Next, you need to add the Google Services plugin to your React Native project's Android configuration.
+
+**In `android/build.gradle`:**
+
+Add the google-services plugin to the `buildscript` dependencies.
+
+```gradle
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ // ...
+ classpath 'com.google.gms:google-services:4.4.2'
+ }
+}
+```
+
+**In `android/app/build.gradle`:**
+
+Apply the `com.google.gms.google-services` plugin at the top of the file.
+
+```gradle
+apply plugin: 'com.android.application'
+apply plugin: 'com.google.gms.google-services'
+
+// ...
+
+dependencies {
+ // ...
+ implementation platform('com.google.firebase:firebase-bom:33.0.0')
+ implementation 'com.google.firebase:firebase-messaging'
+}
+```
+
+**In `android/app/src/main/AndroidManifest.xml`:**
+
+Add the required permissions and register the FirebaseMessagingService:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Note**: Replace `.NotificationApiFirebaseMessagingService` with your actual package path (e.g., `com.example.myapp.NotificationApiFirebaseMessagingService`)
+
+Now, build your app to ensure the Firebase configuration is correct:
+
+```bash
+npx react-native run-android
+```
+
+---
+
+#### 4. Add FirebaseMessagingService
+
+Due to Android's class loading requirements, you need to create a `FirebaseMessagingService` in your app's package. This is a one-time setup.
+
+Create a new file: `android/app/src/main/java/com/yourapp/package/NotificationApiFirebaseMessagingService.kt`
+
+**Replace `com.yourapp.package` with your app's actual package name** (same as your `applicationId` in `build.gradle`).
+
+Copy and paste this code:
+
+```kotlin
+package com.yourapp.package // Replace with your app's package name
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.media.RingtoneManager
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.modules.core.DeviceEventManagerModule
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+
+class NotificationApiFirebaseMessagingService : FirebaseMessagingService() {
+
+ override fun onMessageReceived(remoteMessage: RemoteMessage) {
+ super.onMessageReceived(remoteMessage)
+
+ // Show the notification
+ showNotification(remoteMessage)
+
+ // Emit event to React Native
+ sendNotificationReceivedEvent(remoteMessage)
+ }
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+ // Token refresh is handled by the SDK's sync mechanism
+ }
+
+ private fun showNotification(remoteMessage: RemoteMessage) {
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ // Create notification channel for Android 8.0+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ "notificationapi_channel",
+ "NotificationAPI",
+ NotificationManager.IMPORTANCE_HIGH
+ ).apply {
+ description = "NotificationAPI push notifications"
+ enableVibration(true)
+ enableLights(true)
+ }
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ // Extract title and body
+ val title = remoteMessage.data["title"] ?: remoteMessage.notification?.title ?: "Notification"
+ val body = remoteMessage.data["body"] ?: remoteMessage.notification?.body ?: ""
+
+ // Create intent for when notification is clicked
+ val intent = Intent(this, MainActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ remoteMessage.data.forEach { (key, value) ->
+ putExtra(key, value)
+ }
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ this, 0, intent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ // Get app icon
+ val iconResId = try {
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
+ appInfo.icon
+ } catch (e: Exception) {
+ android.R.drawable.ic_dialog_info
+ }
+
+ // Build and show notification
+ val notificationBuilder = NotificationCompat.Builder(this, "notificationapi_channel")
+ .setSmallIcon(iconResId)
+ .setContentTitle(title)
+ .setContentText(body)
+ .setAutoCancel(true)
+ .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
+ .setContentIntent(pendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setDefaults(NotificationCompat.DEFAULT_ALL)
+
+ val notificationId = remoteMessage.messageId?.hashCode() ?: System.currentTimeMillis().toInt()
+ notificationManager.notify(notificationId, notificationBuilder.build())
+ }
+
+ private fun sendNotificationReceivedEvent(remoteMessage: RemoteMessage) {
+ try {
+ // Get React context from SDK module
+ val moduleClass = Class.forName("com.notificationapi.rn.NotificationApiModule")
+ val getReactContextMethod = moduleClass.getMethod("getReactContext")
+ val reactContext = getReactContextMethod.invoke(null) as? com.facebook.react.bridge.ReactApplicationContext
+
+ reactContext?.let { context ->
+ val params = Arguments.createMap().apply {
+ putString("messageId", remoteMessage.messageId)
+ putString("senderId", remoteMessage.from)
+ putString("title", remoteMessage.data["title"] ?: remoteMessage.notification?.title ?: "")
+ putString("body", remoteMessage.data["body"] ?: remoteMessage.notification?.body ?: "")
+
+ val dataMap = Arguments.createMap()
+ remoteMessage.data.forEach { (key, value) ->
+ dataMap.putString(key, value)
+ }
+ putMap("data", dataMap)
+ }
+
+ context
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
+ ?.emit("notificationapi_notification_received", params)
+ }
+ } catch (e: Exception) {
+ // React context not available yet
+ }
+ }
+}
+```
+
+**Important**:
+
+- Replace `com.yourapp.package` with your actual app package name (found in `android/app/build.gradle` as `applicationId`)
+- Replace `MainActivity` with your main activity class name if different
+
+That's it! The service will automatically handle incoming notifications and display them.
+
+---
+
+#### 5. Install and initialize the NotificationAPI React Native SDK
+
+Our React Native SDK makes it easy to register the device for push notifications.
+
+**Install the SDK:**
+
+```bash
+npm install @notificationapi/react-native
+# or
+yarn add @notificationapi/react-native
+```
+
+**Then, initialize the SDK in your app** (e.g., in `App.tsx` or your main component):
+
+```typescript
+import NotificationAPI from '@notificationapi/react-native';
+
+// Initialize when your app starts
+await NotificationAPI.setup({
+ clientId: 'YOUR_CLIENT_ID', // from NotificationAPI dashboard
+ userId: 'user123', // your app's user ID
+ autoRequestPermission: true, // automatically request push permissions
+ region: 'us' // 'us' (default), 'eu', or 'ca'
+});
+```
+
+This will automatically handle requesting push permissions and registering the device token with NotificationAPI.
+
+### iOS (No Firebase Required!)
+
+1. **Install CocoaPods dependencies** (if using CocoaPods):
+
+ ```bash
+ cd ios && pod install && cd ..
+ ```
+
+2. **Enable Push Notifications capability** in Xcode:
+ - Open your project in Xcode
+ - Select your target
+ - Go to "Signing & Capabilities"
+ - Click "+ Capability" and add "Push Notifications"
+
+3. **Configure APN**:
+ - You need an Apple Developer account
+ - Create an APN key in Apple Developer Console
+ - **Important**: When uploading the APN key to NotificationAPI, ensure it includes the PEM headers:
+ ```
+ -----BEGIN PRIVATE KEY-----
+ [your key content here]
+ -----END PRIVATE KEY-----
+ ```
+ - Configure your NotificationAPI account with the APN key
+
+4. **Update AppDelegate** (if needed):
+ The SDK handles most of the setup, but you may need to ensure your `AppDelegate.m` or `AppDelegate.swift` properly handles APN token registration:
+
+ ```swift
+ // Swift
+ import UserNotifications
+
+ func application(_ application: UIApplication,
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
+ let token = deviceToken.map { String(format: "%02x", $0) }.joined()
+ UserDefaults.standard.set(token, forKey: "apns_token")
+ }
+ ```
+
+ ```objc
+ // Objective-C
+ - (void)application:(UIApplication *)application
+ didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
+ NSString *token = [[deviceToken description] stringByTrimmingCharactersInSet:
+ [NSCharacterSet characterSetWithCharactersInString:@"<>"]];
+ token = [token stringByReplacingOccurrencesOfString:@" " withString:@""];
+ [[NSUserDefaults standardUserDefaults] setObject:token forKey:@"apns_token"];
+ }
+ ```
+
+## 📞 Support
+
+- [Documentation](https://notificationapi.com/docs)
+- [Email Support](mailto:support@notificationapi.com)
+
+## 📄 License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
diff --git a/android/build.gradle b/android/build.gradle
index 5223abf..a51b548 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -32,4 +32,6 @@ repositories {
dependencies {
implementation 'com.facebook.react:react-native'
api 'com.github.notificationapi-com:notificationapi-android-sdk:381fc28'
+ implementation platform('com.google.firebase:firebase-bom:32.0.0')
+ implementation 'com.google.firebase:firebase-messaging'
}
\ No newline at end of file
diff --git a/android/src/main/java/com/notificationapi/rn/NotificationApiEvent.kt b/android/src/main/java/com/notificationapi/rn/NotificationApiEvent.kt
index d964edc..1e36c8f 100644
--- a/android/src/main/java/com/notificationapi/rn/NotificationApiEvent.kt
+++ b/android/src/main/java/com/notificationapi/rn/NotificationApiEvent.kt
@@ -2,5 +2,6 @@ package com.notificationapi.rn
enum class NotificationApiEvent(val eventName: String) {
NOTIFICATION_PERMISSIONS_REQUESTED("notificationapi_notification_permissions_requested"),
- NOTIFICATION_ON_CLICK("notificationapi_notification_on_click")
+ NOTIFICATION_ON_CLICK("notificationapi_notification_on_click"),
+ NOTIFICATION_RECEIVED("notificationapi_notification_received")
}
\ No newline at end of file
diff --git a/android/src/main/java/com/notificationapi/rn/NotificationApiFirebaseMessagingService.kt b/android/src/main/java/com/notificationapi/rn/NotificationApiFirebaseMessagingService.kt
new file mode 100644
index 0000000..ed266a5
--- /dev/null
+++ b/android/src/main/java/com/notificationapi/rn/NotificationApiFirebaseMessagingService.kt
@@ -0,0 +1,146 @@
+package com.notificationapi.rn
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.media.RingtoneManager
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.modules.core.DeviceEventManagerModule
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import com.notificationapi.notificationapi_android_sdk.NotificationApi
+
+class NotificationApiFirebaseMessagingService : FirebaseMessagingService() {
+
+ override fun onMessageReceived(remoteMessage: RemoteMessage) {
+ super.onMessageReceived(remoteMessage)
+
+ android.util.Log.d("NotificationAPI", "onMessageReceived called")
+ android.util.Log.d("NotificationAPI", "Message ID: ${remoteMessage.messageId}")
+ android.util.Log.d("NotificationAPI", "From: ${remoteMessage.from}")
+ android.util.Log.d("NotificationAPI", "Data: ${remoteMessage.data}")
+ android.util.Log.d("NotificationAPI", "Notification: ${remoteMessage.notification?.title} - ${remoteMessage.notification?.body}")
+
+ // Let the Android SDK handle the message (for tracking, etc.)
+ try {
+ NotificationApi.shared.onMessageReceived(remoteMessage)
+ } catch (e: Exception) {
+ android.util.Log.e("NotificationAPI", "Error in NotificationApi.shared.onMessageReceived", e)
+ }
+
+ // Show the notification
+ showNotification(remoteMessage)
+
+ // Emit event to React Native
+ sendNotificationReceivedEvent(remoteMessage)
+ }
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+ // Token refresh is handled by the SDK's sync mechanism
+ NotificationApi.shared.onNewToken(token)
+ }
+
+ private fun showNotification(remoteMessage: RemoteMessage) {
+ android.util.Log.d("NotificationAPI", "showNotification called")
+
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ // Create notification channel for Android 8.0+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ "notificationapi_channel",
+ "NotificationAPI",
+ NotificationManager.IMPORTANCE_HIGH
+ ).apply {
+ description = "NotificationAPI push notifications"
+ enableVibration(true)
+ enableLights(true)
+ }
+ notificationManager.createNotificationChannel(channel)
+ android.util.Log.d("NotificationAPI", "Notification channel created")
+ }
+
+ // Extract title and body
+ val title = remoteMessage.data["title"] ?: remoteMessage.notification?.title ?: "Notification"
+ val body = remoteMessage.data["body"] ?: remoteMessage.notification?.body ?: ""
+
+ // Create intent for when notification is clicked
+ // Use the launcher activity from the package
+ val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ // Add notification data to intent
+ remoteMessage.data.forEach { (key, value) ->
+ putExtra(key, value)
+ }
+ } ?: Intent().apply {
+ setPackage(packageName)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ remoteMessage.data.forEach { (key, value) ->
+ putExtra(key, value)
+ }
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ this, 0, intent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ // Get app icon - try to use the app's launcher icon
+ val iconResId = try {
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
+ appInfo.icon
+ } catch (e: Exception) {
+ android.R.drawable.ic_dialog_info
+ }
+
+ // Build notification
+ val notificationBuilder = NotificationCompat.Builder(this, "notificationapi_channel")
+ .setSmallIcon(iconResId)
+ .setContentTitle(title)
+ .setContentText(body)
+ .setAutoCancel(true)
+ .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
+ .setContentIntent(pendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setDefaults(NotificationCompat.DEFAULT_ALL)
+
+ // Show notification
+ val notificationId = remoteMessage.messageId?.hashCode() ?: System.currentTimeMillis().toInt()
+ notificationManager.notify(notificationId, notificationBuilder.build())
+ android.util.Log.d("NotificationAPI", "Notification shown with ID: $notificationId, Title: $title, Body: $body")
+ }
+
+ private fun sendNotificationReceivedEvent(remoteMessage: RemoteMessage) {
+ val reactContext = NotificationApiModule.getReactContext()
+ reactContext?.let { context ->
+ val params = Arguments.createMap().apply {
+ putString("messageId", remoteMessage.messageId)
+ putString("senderId", remoteMessage.from)
+
+ // Extract title and body from data or notification
+ val title = remoteMessage.data["title"] ?: remoteMessage.notification?.title
+ val body = remoteMessage.data["body"] ?: remoteMessage.notification?.body
+
+ putString("title", title ?: "")
+ putString("body", body ?: "")
+
+ // Add all data fields
+ val dataMap = Arguments.createMap()
+ remoteMessage.data.forEach { (key, value) ->
+ dataMap.putString(key, value)
+ }
+ putMap("data", dataMap)
+ }
+
+ context
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
+ ?.emit(NotificationApiEvent.NOTIFICATION_RECEIVED.eventName, params)
+ }
+ }
+}
+
diff --git a/android/src/main/java/com/notificationapi/rn/NotificationApiModule.kt b/android/src/main/java/com/notificationapi/rn/NotificationApiModule.kt
index e2ccc54..1b1503a 100644
--- a/android/src/main/java/com/notificationapi/rn/NotificationApiModule.kt
+++ b/android/src/main/java/com/notificationapi/rn/NotificationApiModule.kt
@@ -1,14 +1,35 @@
package com.notificationapi.rn
+import android.os.Build
+import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.WritableMap
+import com.facebook.react.bridge.Arguments
import com.notificationapi.rn.NativeNotificationApiSpec
import com.notificationapi.notificationapi_android_sdk.NotificationApi
import com.notificationapi.notificationapi_android_sdk.models.NotificationApiCredentials
+import com.google.firebase.messaging.FirebaseMessaging
+import android.provider.Settings
class NotificationApiModule(reactContext: ReactApplicationContext) : NativeNotificationApiSpec(reactContext) {
companion object {
const val NAME = "NotificationApiReactNativeSdk"
+ private var reactContext: ReactApplicationContext? = null
+
+ fun getReactContext(): ReactApplicationContext? = reactContext
+ }
+
+ init {
+ NotificationApiModule.reactContext = reactContext
+ // Also try to store in MainApplication if it exists (for FirebaseMessagingService access)
+ try {
+ val mainAppClass = Class.forName("com.millerm30.noti.MainApplication")
+ val setReactContextMethod = mainAppClass.getMethod("setReactContext", ReactApplicationContext::class.java)
+ setReactContextMethod.invoke(null, reactContext)
+ } catch (e: Exception) {
+ // MainApplication might not exist or might not have this method - that's okay
+ }
}
override fun getName() = NAME
@@ -17,7 +38,59 @@ class NotificationApiModule(reactContext: ReactApplicationContext) : NativeNotif
NotificationApi.shared.configure(NotificationApiCredentials(clientId, userId, hashedUserId))
}
- override fun requestNotificationPermission() {
- NotificationApi.shared.askNotificationPermissions()
+ override fun requestNotificationPermission(promise: Promise) {
+ try {
+ NotificationApi.shared.askNotificationPermissions()
+ // Note: The actual permission result will come via event emitter
+ // For now, we'll resolve immediately and let the event handle the actual result
+ promise.resolve(true)
+ } catch (e: Exception) {
+ promise.reject("PERMISSION_ERROR", e.message, e)
+ }
+ }
+
+ override fun getPushToken(promise: Promise) {
+ try {
+ FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
+ if (task.isSuccessful) {
+ val token = task.result
+ promise.resolve(token)
+ } else {
+ val exception = task.exception
+ promise.reject("TOKEN_ERROR", exception?.message ?: "Failed to get push token", exception)
+ }
+ }
+ } catch (e: Exception) {
+ promise.reject("TOKEN_ERROR", e.message, e)
+ }
+ }
+
+ override fun getDeviceInfo(promise: Promise) {
+ try {
+ val deviceInfo = Arguments.createMap()
+
+ // Get Android ID as device ID
+ val deviceId = Settings.Secure.getString(
+ reactApplicationContext.contentResolver,
+ Settings.Secure.ANDROID_ID
+ ) ?: "unknown_device"
+ deviceInfo.putString("deviceId", deviceId)
+
+ // Platform
+ deviceInfo.putString("platform", "android")
+
+ // Manufacturer
+ deviceInfo.putString("manufacturer", Build.MANUFACTURER)
+
+ // Model
+ deviceInfo.putString("model", Build.MODEL)
+
+ // App ID (package name)
+ deviceInfo.putString("appId", reactApplicationContext.packageName)
+
+ promise.resolve(deviceInfo)
+ } catch (e: Exception) {
+ promise.reject("DEVICE_INFO_ERROR", e.message, e)
+ }
}
}
\ No newline at end of file
diff --git a/android/src/main/java/com/notificationapi/rn/NotificationApiPackage.kt b/android/src/main/java/com/notificationapi/rn/NotificationApiPackage.kt
index c140c46..98a6bef 100644
--- a/android/src/main/java/com/notificationapi/rn/NotificationApiPackage.kt
+++ b/android/src/main/java/com/notificationapi/rn/NotificationApiPackage.kt
@@ -7,7 +7,7 @@ import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
class NotificationApiPackage : TurboReactPackage() {
-override fun getModule(name: String?, reactContext: ReactApplicationContext): NativeModule? =
+override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
if (name == NotificationApiModule.NAME) {
NotificationApiModule(reactContext)
} else {
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..706c5c5
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,27 @@
+// @ts-check
+import eslint from '@eslint/js';
+import tseslint from 'typescript-eslint';
+
+export default [
+ {
+ ignores: ['node_modules/**', 'dist/**', 'android/**', 'ios/**', '*.tgz']
+ },
+ {
+ files: ['js/**/*.{ts,tsx}']
+ },
+ eslint.configs.recommended,
+ ...tseslint.configs.recommended,
+ {
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'warn',
+ '@typescript-eslint/no-unused-vars': [
+ 'warn',
+ {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_'
+ }
+ ]
+ }
+ }
+];
+
diff --git a/ios/NotificationApiModule-Bridging-Header.h b/ios/NotificationApiModule-Bridging-Header.h
new file mode 100644
index 0000000..2eb3a47
--- /dev/null
+++ b/ios/NotificationApiModule-Bridging-Header.h
@@ -0,0 +1,7 @@
+//
+// Use this file to import your target's public headers that you would like to expose to Swift.
+//
+
+#import
+#import
+
diff --git a/ios/NotificationApiModule.h b/ios/NotificationApiModule.h
new file mode 100644
index 0000000..a6e66ed
--- /dev/null
+++ b/ios/NotificationApiModule.h
@@ -0,0 +1,6 @@
+#import
+
+@interface NotificationApiModule : RCTEventEmitter
+
+@end
+
diff --git a/ios/NotificationApiModule.m b/ios/NotificationApiModule.m
new file mode 100644
index 0000000..8587d6b
--- /dev/null
+++ b/ios/NotificationApiModule.m
@@ -0,0 +1,177 @@
+#import "NotificationApiModule.h"
+#import
+#import
+#import
+#import
+#import
+
+@import UserNotifications;
+
+@interface NotificationApiModule()
+@end
+
+@implementation NotificationApiModule {
+ BOOL _hasListeners;
+}
+
+RCT_EXPORT_MODULE(NotificationApiReactNativeSdk);
+
++ (BOOL)requiresMainQueueSetup {
+ return YES;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ center.delegate = self;
+ }
+ return self;
+}
+
+- (NSArray *)supportedEvents {
+ return @[
+ @"notificationapi_notification_permissions_requested",
+ @"notificationapi_notification_on_click",
+ @"notificationapi_push_token_received",
+ @"notificationapi_notification_received"
+ ];
+}
+
+- (void)startObserving {
+ _hasListeners = YES;
+}
+
+- (void)stopObserving {
+ _hasListeners = NO;
+}
+
+RCT_EXPORT_METHOD(configure:(NSString *)clientId
+ userId:(NSString *)userId
+ hashedUserId:(NSString *)hashedUserId
+ resolver:(RCTPromiseResolveBlock)resolve
+ rejecter:(RCTPromiseRejectBlock)reject) {
+ // Configuration is stored for later use
+ // The actual SDK configuration would happen here if there was an iOS SDK
+ resolve(@(YES));
+}
+
+RCT_EXPORT_METHOD(requestNotificationPermission:(RCTPromiseResolveBlock)resolve
+ rejecter:(RCTPromiseRejectBlock)reject) {
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+
+ [center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge)
+ completionHandler:^(BOOL granted, NSError * _Nullable error) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (error) {
+ reject(@"PERMISSION_ERROR", error.localizedDescription, error);
+ } else {
+ // Emit event
+ if (self->_hasListeners) {
+ [self sendEventWithName:@"notificationapi_notification_permissions_requested"
+ body:@{@"isGranted": @(granted)}];
+ }
+ resolve(@(granted));
+ }
+ });
+ }];
+}
+
+RCT_EXPORT_METHOD(getPushToken:(RCTPromiseResolveBlock)resolve
+ rejecter:(RCTPromiseRejectBlock)reject) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [[UIApplication sharedApplication] registerForRemoteNotifications];
+
+ // Get the APN token from UserDefaults (stored by AppDelegate)
+ NSString *apnsToken = [[NSUserDefaults standardUserDefaults] stringForKey:@"apns_token"];
+
+ if (apnsToken) {
+ resolve(apnsToken);
+ } else {
+ // If token not available yet, wait a bit and try again
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+ NSString *token = [[NSUserDefaults standardUserDefaults] stringForKey:@"apns_token"];
+ if (token) {
+ resolve(token);
+ } else {
+ reject(@"TOKEN_ERROR", @"APN token not available. Make sure push notifications are properly configured.", nil);
+ }
+ });
+ }
+ });
+}
+
+RCT_EXPORT_METHOD(getDeviceInfo:(RCTPromiseResolveBlock)resolve
+ rejecter:(RCTPromiseRejectBlock)reject) {
+ UIDevice *device = [UIDevice currentDevice];
+
+ NSMutableDictionary *deviceInfo = [NSMutableDictionary dictionary];
+
+ // Device ID (identifierForVendor)
+ NSString *deviceId = [[device identifierForVendor] UUIDString];
+ if (deviceId) {
+ deviceInfo[@"deviceId"] = deviceId;
+ } else {
+ deviceInfo[@"deviceId"] = @"unknown_ios_device";
+ }
+
+ // Platform
+ deviceInfo[@"platform"] = @"ios";
+
+ // Manufacturer
+ deviceInfo[@"manufacturer"] = @"Apple";
+
+ // Model
+ deviceInfo[@"model"] = [device model];
+
+ // App ID (bundle identifier)
+ deviceInfo[@"appId"] = [[NSBundle mainBundle] bundleIdentifier];
+
+ resolve(deviceInfo);
+}
+
+// UNUserNotificationCenterDelegate methods
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center
+ willPresentNotification:(UNNotification *)notification
+ withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
+ if (_hasListeners) {
+ NSDictionary *userInfo = notification.request.content.userInfo;
+ [self sendEventWithName:@"notificationapi_notification_received"
+ body:@{
+ @"messageId": userInfo[@"messageId"] ?: @"",
+ @"senderId": userInfo[@"senderId"] ?: @"",
+ @"title": notification.request.content.title ?: @"",
+ @"body": notification.request.content.body ?: @"",
+ @"data": userInfo[@"data"] ?: @{}
+ }];
+ }
+
+ // Show notification even when app is in foreground
+ if (@available(iOS 14.0, *)) {
+ completionHandler(UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBadge);
+ } else {
+ completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBadge);
+ }
+}
+
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center
+didReceiveNotificationResponse:(UNNotificationResponse *)response
+ withCompletionHandler:(void (^)(void))completionHandler {
+ if (_hasListeners) {
+ NSDictionary *userInfo = response.notification.request.content.userInfo;
+ [self sendEventWithName:@"notificationapi_notification_on_click"
+ body:@{
+ @"messageId": userInfo[@"messageId"] ?: @"",
+ @"senderId": userInfo[@"senderId"] ?: @"",
+ @"ttl": userInfo[@"ttl"] ?: @(0),
+ @"title": response.notification.request.content.title ?: @"",
+ @"body": response.notification.request.content.body ?: @"",
+ @"data": userInfo[@"data"] ?: @{}
+ }];
+ }
+
+ completionHandler();
+}
+
+@end
+
diff --git a/js/NativeNotificationApi.ts b/js/NativeNotificationApi.ts
index c682330..d412ad9 100644
--- a/js/NativeNotificationApi.ts
+++ b/js/NativeNotificationApi.ts
@@ -1,12 +1,19 @@
-import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
-import {TurboModuleRegistry} from 'react-native';
+import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport';
+import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
configure(clientId: string, userId: string, hashedUserId?: string): void;
-
- requestNotificationPermission(): void;
+ requestNotificationPermission(): Promise;
+ getPushToken(): Promise;
+ getDeviceInfo(): Promise<{
+ deviceId: string;
+ platform: string;
+ manufacturer: string | null;
+ model: string | null;
+ appId: string | null;
+ }>;
}
export default TurboModuleRegistry.get(
- 'NotificationApiReactNativeSdk',
-) as Spec | null;
\ No newline at end of file
+ 'NotificationApiReactNativeSdk'
+) as Spec | null;
diff --git a/js/NotificationAPI.ts b/js/NotificationAPI.ts
new file mode 100644
index 0000000..4a1aedc
--- /dev/null
+++ b/js/NotificationAPI.ts
@@ -0,0 +1,201 @@
+import { Platform } from 'react-native';
+import NativeNotificationApi from './NativeNotificationApi';
+import { NotificationAPIService } from './NotificationAPIService';
+import { getEventEmitter, Events, PushTokenReceivedEvent } from './events';
+import { NotificationAPIException } from './models';
+import type { Region, User, PushToken, Device } from './models';
+
+export interface SetupOptions {
+ clientId: string;
+ userId: string;
+ hashedUserId?: string;
+ region?: Region;
+ autoRequestPermission?: boolean;
+ baseUrl?: string;
+}
+
+export type { SetupOptions as SetupOptionsType };
+
+class NotificationAPI {
+ private static instance: NotificationAPI | null = null;
+ private service: NotificationAPIService | null = null;
+ private userId: string | null = null;
+ private clientId: string | null = null;
+ private hashedUserId: string | undefined;
+ private region: Region = 'us';
+ private baseUrl: string | undefined;
+ private isInitialized = false;
+
+ private constructor() {}
+
+ static getInstance(): NotificationAPI {
+ if (!NotificationAPI.instance) {
+ NotificationAPI.instance = new NotificationAPI();
+ }
+ return NotificationAPI.instance;
+ }
+
+ async setup(options: SetupOptions): Promise {
+ const {
+ clientId,
+ userId,
+ hashedUserId,
+ region = 'us',
+ autoRequestPermission = true,
+ baseUrl
+ } = options;
+
+ this.clientId = clientId;
+ this.userId = userId;
+ this.hashedUserId = hashedUserId;
+ this.region = region;
+ this.baseUrl = baseUrl;
+
+ // Initialize service
+ this.service = new NotificationAPIService(
+ clientId,
+ userId,
+ hashedUserId,
+ region,
+ baseUrl
+ );
+
+ // Configure native module
+ const nativeModule = NativeNotificationApi;
+ if (!nativeModule) {
+ throw new NotificationAPIException(
+ 'Native module not found. Make sure the native module is properly linked.'
+ );
+ }
+
+ nativeModule.configure(clientId, userId, hashedUserId);
+
+ // Request permission if auto-request is enabled
+ if (autoRequestPermission) {
+ await this.requestPermission();
+ }
+
+ // Get push token and sync with backend
+ await this.syncPushToken();
+
+ this.isInitialized = true;
+ }
+
+ async requestPermission(): Promise {
+ const nativeModule = NativeNotificationApi;
+ if (!nativeModule) {
+ throw new NotificationAPIException('Native module not found');
+ }
+
+ try {
+ const granted = await nativeModule.requestNotificationPermission();
+ return granted;
+ } catch (error) {
+ console.error('Error requesting notification permission:', error);
+ return false;
+ }
+ }
+
+ async getPushToken(): Promise {
+ const nativeModule = NativeNotificationApi;
+ if (!nativeModule) {
+ throw new NotificationAPIException('Native module not found');
+ }
+
+ try {
+ const token = await nativeModule.getPushToken();
+ return token;
+ } catch (error) {
+ console.error('Error getting push token:', error);
+ return null;
+ }
+ }
+
+ async getDeviceInfo(): Promise {
+ const nativeModule = NativeNotificationApi;
+ if (!nativeModule) {
+ throw new NotificationAPIException('Native module not found');
+ }
+
+ try {
+ const info = await nativeModule.getDeviceInfo();
+ return {
+ device_id: info.deviceId,
+ platform: info.platform || Platform.OS,
+ manufacturer: info.manufacturer || undefined,
+ model: info.model || undefined,
+ app_id: info.appId || undefined
+ };
+ } catch (error) {
+ console.error('Error getting device info:', error);
+ throw new NotificationAPIException(
+ `Failed to get device info: ${error instanceof Error ? error.message : 'Unknown error'}`
+ );
+ }
+ }
+
+ private async syncPushToken(): Promise {
+ if (!this.service || !this.userId || !this.clientId) {
+ return;
+ }
+
+ try {
+ const token = await this.getPushToken();
+ if (!token) {
+ console.warn('Push token not available yet');
+ return;
+ }
+
+ const deviceInfo = await this.getDeviceInfo();
+ // Determine environment for iOS (sandbox vs production)
+ // Use 'sandbox' for debug builds, 'production' for release builds
+ const pushToken: PushToken = {
+ type: Platform.OS === 'ios' ? 'APN' : 'FCM',
+ token,
+ device: deviceInfo,
+ // iOS environment: 'sandbox' for development, 'production' for release
+ environment:
+ Platform.OS === 'ios'
+ ? __DEV__
+ ? 'sandbox'
+ : 'production'
+ : undefined
+ };
+
+ const user: User = {
+ id: this.userId,
+ pushTokens: [pushToken]
+ };
+
+ await this.service.identify(user);
+
+ // Emit event
+ const eventEmitter = getEventEmitter();
+ eventEmitter.emit(Events.PUSH_TOKEN_RECEIVED, {
+ token,
+ type: pushToken.type
+ } as PushTokenReceivedEvent);
+ } catch (error) {
+ console.error('Error syncing push token:', error);
+ }
+ }
+
+ getService(): NotificationAPIService {
+ if (!this.service) {
+ throw new NotificationAPIException(
+ 'SDK not initialized. Call setup() first.'
+ );
+ }
+ return this.service;
+ }
+
+ get isReady(): boolean {
+ return this.isInitialized && this.service !== null;
+ }
+
+ get currentUser(): string | null {
+ return this.userId;
+ }
+}
+
+export default NotificationAPI.getInstance();
diff --git a/js/NotificationAPIService.ts b/js/NotificationAPIService.ts
new file mode 100644
index 0000000..5ee5f3e
--- /dev/null
+++ b/js/NotificationAPIService.ts
@@ -0,0 +1,255 @@
+import {
+ API_ENDPOINTS,
+ Region,
+ GetPreferencesResponse,
+ GetInAppNotificationsResult,
+ InAppNotification,
+ User,
+ Preference,
+ Channel,
+ DeliveryOption,
+ NotificationAPIException
+} from './models';
+
+export class NotificationAPIService {
+ private clientId: string;
+ private userId: string;
+ private hashedUserId?: string;
+ private baseUrl: string;
+
+ constructor(
+ clientId: string,
+ userId: string,
+ hashedUserId: string | undefined,
+ region: Region = 'us',
+ baseUrl?: string
+ ) {
+ this.clientId = clientId;
+ this.userId = userId;
+ this.hashedUserId = hashedUserId;
+ this.baseUrl = baseUrl || API_ENDPOINTS[region];
+ }
+
+ private generateBasicToken(): string {
+ const token = this.hashedUserId
+ ? `${this.clientId}:${this.userId}:${this.hashedUserId}`
+ : `${this.clientId}:${this.userId}`;
+ // Base64 encoding for React Native (btoa is not available)
+ // For Basic Auth, tokens are ASCII, so we can use charCodeAt directly
+ const chars =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+ let result = '';
+ let i = 0;
+
+ while (i < token.length) {
+ const byte1 = token.charCodeAt(i++);
+ const byte2 = i < token.length ? token.charCodeAt(i++) : undefined;
+ const byte3 = i < token.length ? token.charCodeAt(i++) : undefined;
+
+ const bitmap = (byte1 << 16) | ((byte2 ?? 0) << 8) | (byte3 ?? 0);
+
+ result += chars.charAt((bitmap >> 18) & 63);
+ result += chars.charAt((bitmap >> 12) & 63);
+
+ if (byte2 !== undefined) {
+ result += chars.charAt((bitmap >> 6) & 63);
+ } else {
+ result += '=';
+ }
+
+ if (byte3 !== undefined) {
+ result += chars.charAt(bitmap & 63);
+ } else {
+ result += '=';
+ }
+ }
+
+ return result;
+ }
+
+ private async apiRequest(
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
+ resource: string,
+ data?: unknown
+ ): Promise {
+ const url = `${this.baseUrl}/${this.clientId}/users/${encodeURIComponent(this.userId)}/${resource}`;
+
+ const headers: Record = {
+ Authorization: `Basic ${this.generateBasicToken()}`,
+ 'Content-Type': 'application/json'
+ };
+
+ const options: RequestInit = {
+ method,
+ headers
+ };
+
+ if (data && (method === 'POST' || method === 'PATCH')) {
+ options.body = JSON.stringify(data);
+ }
+
+ const response = await fetch(url, options);
+
+ if (response.status >= 200 && response.status < 300) {
+ const contentType = response.headers.get('content-type');
+ if (contentType && contentType.includes('application/json')) {
+ return (await response.json()) as T;
+ }
+ return {} as T;
+ } else {
+ throw new NotificationAPIException(
+ `API request failed: ${response.status}`,
+ response.status
+ );
+ }
+ }
+
+ async getNotifications(
+ before: string,
+ count: number
+ ): Promise<{
+ notifications: InAppNotification[];
+ hasMore: boolean;
+ oldestReceived: string;
+ }> {
+ return this.apiRequest(
+ 'GET',
+ `notifications/INAPP_WEB?count=${count}&before=${encodeURIComponent(before)}`
+ );
+ }
+
+ async patchNotifications(params: {
+ trackingIds: string[];
+ archived?: string | null;
+ clicked?: string | null;
+ opened?: string | null;
+ }): Promise {
+ return this.apiRequest('PATCH', 'notifications/INAPP_WEB', params);
+ }
+
+ async getPreferences(): Promise {
+ return this.apiRequest('GET', 'preferences');
+ }
+
+ async postPreferences(preferences: Preference[]): Promise {
+ await this.apiRequest('POST', 'preferences', { preferences });
+ }
+
+ async postUser(user: User): Promise {
+ await this.apiRequest('POST', '', user);
+ }
+
+ async getUserAccountMetadata(): Promise<{
+ userAccountMetadata: {
+ logo: string;
+ environmentVapidPublicKey: string;
+ hasWebPushEnabled: boolean;
+ };
+ }> {
+ return this.apiRequest('GET', 'account_metadata');
+ }
+
+ async getInAppNotifications(params: {
+ before: string;
+ maxCount?: number;
+ oldestNeeded?: string;
+ }): Promise {
+ const maxCount = params.maxCount || 100;
+ const oldestNeeded =
+ params.oldestNeeded ||
+ new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
+
+ const result: InAppNotification[] = [];
+ let oldestReceived = params.before;
+ let hasMore = true;
+ let shouldLoadMore = true;
+
+ while (shouldLoadMore) {
+ const response = await this.getNotifications(oldestReceived, maxCount);
+ const notifications = response.notifications || [];
+
+ // Remove duplicates
+ const notificationsWithoutDuplicates = notifications.filter(
+ (n) => !result.find((existing) => existing.id === n.id)
+ );
+
+ if (notificationsWithoutDuplicates.length > 0) {
+ oldestReceived = notificationsWithoutDuplicates.reduce(
+ (min, n) => (min < n.date ? min : n.date),
+ oldestReceived
+ );
+ }
+
+ result.push(...notificationsWithoutDuplicates);
+ hasMore = notificationsWithoutDuplicates.length > 0;
+ shouldLoadMore =
+ hasMore && result.length < maxCount && oldestReceived > oldestNeeded;
+ }
+
+ return {
+ items: result,
+ hasMore,
+ oldestReceived
+ };
+ }
+
+ async updateInAppNotifications(params: {
+ ids: string[];
+ archived?: boolean;
+ clicked?: boolean;
+ opened?: boolean;
+ }): Promise {
+ const body: {
+ trackingIds: string[];
+ archived?: string | null;
+ clicked?: string | null;
+ opened?: string | null;
+ } = {
+ trackingIds: params.ids
+ };
+
+ if (params.archived === true) {
+ body.archived = new Date().toISOString();
+ } else if (params.archived === false) {
+ body.archived = null;
+ }
+
+ if (params.clicked === true) {
+ body.clicked = new Date().toISOString();
+ } else if (params.clicked === false) {
+ body.clicked = null;
+ }
+
+ if (params.opened === true) {
+ body.opened = new Date().toISOString();
+ } else if (params.opened === false) {
+ body.opened = null;
+ }
+
+ await this.patchNotifications(body);
+ }
+
+ async updateDeliveryOption(params: {
+ notificationId: string;
+ subNotificationId?: string;
+ channel: Channel;
+ delivery: DeliveryOption;
+ }): Promise {
+ const preference: Preference = {
+ notificationId: params.notificationId,
+ channel: params.channel,
+ delivery: params.delivery,
+ subNotificationId: params.subNotificationId
+ };
+ await this.postPreferences([preference]);
+ }
+
+ async identify(user: User): Promise {
+ if (user.id !== this.userId) {
+ throw new NotificationAPIException(
+ 'The id in the parameters does not match the initialized userId.'
+ );
+ }
+ await this.postUser(user);
+ }
+}
diff --git a/js/NotificationApiEvents.ts b/js/NotificationApiEvents.ts
index 312c0ff..8d070cf 100644
--- a/js/NotificationApiEvents.ts
+++ b/js/NotificationApiEvents.ts
@@ -1,17 +1,19 @@
export enum Events {
- NOTIFICATION_PERMISSIONS_REQUESTED = 'notificationapi_notification_permissions_requested',
- NOTIFICATION_ON_CLICK = 'notificationapi_notification_on_click'
+ NOTIFICATION_PERMISSIONS_REQUESTED = 'notificationapi_notification_permissions_requested',
+ NOTIFICATION_ON_CLICK = 'notificationapi_notification_on_click'
}
export interface NotificationPermissionsRequestedEvent {
- isGranted: boolean;
+ isGranted: boolean;
}
-export interface NotificationOnClickEvent {
- messageId: string;
- senderId: string;
- ttl: number;
- title: string;
- body: string;
- data: T;
-}
\ No newline at end of file
+export interface NotificationOnClickEvent<
+ T extends Record = Record
+> {
+ messageId: string;
+ senderId: string;
+ ttl: number;
+ title: string;
+ body: string;
+ data: T;
+}
diff --git a/js/events.ts b/js/events.ts
new file mode 100644
index 0000000..5ebe21f
--- /dev/null
+++ b/js/events.ts
@@ -0,0 +1,47 @@
+import { NativeEventEmitter, NativeModules } from 'react-native';
+
+export enum Events {
+ NOTIFICATION_PERMISSIONS_REQUESTED = 'notificationapi_notification_permissions_requested',
+ NOTIFICATION_ON_CLICK = 'notificationapi_notification_on_click',
+ PUSH_TOKEN_RECEIVED = 'notificationapi_push_token_received',
+ NOTIFICATION_RECEIVED = 'notificationapi_notification_received'
+}
+
+export interface NotificationPermissionsRequestedEvent {
+ isGranted: boolean;
+}
+
+export interface NotificationOnClickEvent<
+ T extends Record = Record
+> {
+ messageId: string;
+ senderId: string;
+ ttl: number;
+ title: string;
+ body: string;
+ data: T;
+}
+
+export interface PushTokenReceivedEvent {
+ token: string;
+ type: 'FCM' | 'APN';
+}
+
+export interface NotificationReceivedEvent {
+ messageId: string;
+ senderId: string;
+ title: string;
+ body: string;
+ data?: Record;
+}
+
+// Event emitter instance
+let eventEmitter: NativeEventEmitter | null = null;
+
+export function getEventEmitter(): NativeEventEmitter {
+ if (!eventEmitter) {
+ const { NotificationApiReactNativeSdk } = NativeModules;
+ eventEmitter = new NativeEventEmitter(NotificationApiReactNativeSdk);
+ }
+ return eventEmitter;
+}
diff --git a/js/index.ts b/js/index.ts
new file mode 100644
index 0000000..fc0e9b6
--- /dev/null
+++ b/js/index.ts
@@ -0,0 +1,48 @@
+import NotificationAPI, { type SetupOptions } from './NotificationAPI';
+import { NotificationAPIService } from './NotificationAPIService';
+import { getEventEmitter, Events } from './events';
+import type {
+ Region,
+ User,
+ PushToken,
+ Device,
+ InAppNotification,
+ GetPreferencesResponse,
+ GetInAppNotificationsResult,
+ Preference,
+ Channel,
+ DeliveryOption,
+ NotificationAPIException
+} from './models';
+
+// Export main SDK instance
+export default NotificationAPI;
+
+// Export types
+export type {
+ Region,
+ User,
+ PushToken,
+ Device,
+ InAppNotification,
+ GetPreferencesResponse,
+ GetInAppNotificationsResult,
+ Preference,
+ Channel,
+ DeliveryOption,
+ SetupOptions
+};
+
+// Export classes
+export { NotificationAPI, NotificationAPIService, NotificationAPIException };
+
+// Export events
+export { Events, getEventEmitter };
+
+// Export event types
+export type {
+ NotificationPermissionsRequestedEvent,
+ NotificationOnClickEvent,
+ PushTokenReceivedEvent,
+ NotificationReceivedEvent
+} from './events';
diff --git a/js/models.ts b/js/models.ts
new file mode 100644
index 0000000..d27350a
--- /dev/null
+++ b/js/models.ts
@@ -0,0 +1,149 @@
+// API endpoints
+export const API_ENDPOINTS = {
+ us: 'https://api.notificationapi.com',
+ eu: 'https://api.eu.notificationapi.com',
+ ca: 'https://api.ca.notificationapi.com'
+} as const;
+
+export type Region = 'us' | 'eu' | 'ca';
+
+// Channel types
+export type Channel =
+ | 'EMAIL'
+ | 'INAPP_WEB'
+ | 'SMS'
+ | 'CALL'
+ | 'PUSH'
+ | 'WEB_PUSH'
+ | 'SLACK';
+
+// Delivery options
+export type DeliveryOption =
+ | 'off'
+ | 'instant'
+ | 'hourly'
+ | 'daily'
+ | 'weekly'
+ | 'monthly';
+
+// Push token providers
+export type PushProvider = 'FCM' | 'APN';
+
+// Device information
+export interface Device {
+ app_id?: string;
+ ad_id?: string;
+ device_id: string;
+ platform?: string;
+ manufacturer?: string;
+ model?: string;
+}
+
+// Push token
+export interface PushToken {
+ type: PushProvider;
+ token: string;
+ device: Device;
+ environment?: 'sandbox' | 'production';
+}
+
+// User information
+export interface User {
+ id: string;
+ email?: string;
+ number?: string;
+ pushTokens?: PushToken[];
+ webPushTokens?: WebPushToken[];
+ timezone?: string;
+}
+
+// Web push token
+export interface WebPushSubscription {
+ endpoint: string;
+ keys: {
+ p256dh: string;
+ auth: string;
+ };
+}
+
+export interface WebPushToken {
+ sub: WebPushSubscription;
+}
+
+// In-app notification
+export interface InAppNotification {
+ id: string;
+ notificationId: string;
+ subNotificationId?: string;
+ seen: boolean;
+ title: string;
+ redirectURL?: string;
+ imageURL?: string;
+ date: string;
+ parameters?: Record;
+ expDate?: number;
+ opened?: string;
+ clicked?: string;
+ archived?: string;
+ actioned1?: string;
+ actioned2?: string;
+ replies?: {
+ date: string;
+ message: string;
+ }[];
+}
+
+// Preference
+export interface Preference {
+ notificationId: string;
+ channel: Channel;
+ delivery: DeliveryOption;
+ subNotificationId?: string;
+}
+
+// Notification config
+export interface NotificationConfig {
+ notificationId: string;
+ title: string;
+ channels: Channel[];
+ options?: Record;
+}
+
+// Sub notification
+export interface SubNotification {
+ notificationId: string;
+ subNotificationId: string;
+ title: string;
+}
+
+// Preferences response
+export interface GetPreferencesResponse {
+ preferences: Preference[];
+ notifications: NotificationConfig[];
+ subNotifications: SubNotification[];
+}
+
+// User account metadata
+export interface UserAccountMetadata {
+ logo: string;
+ environmentVapidPublicKey: string;
+ hasWebPushEnabled: boolean;
+}
+
+// In-app notifications result
+export interface GetInAppNotificationsResult {
+ items: InAppNotification[];
+ hasMore: boolean;
+ oldestReceived: string;
+}
+
+// API error
+export class NotificationAPIException extends Error {
+ statusCode?: number;
+
+ constructor(message: string, statusCode?: number) {
+ super(message);
+ this.name = 'NotificationAPIException';
+ this.statusCode = statusCode;
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 98fd46e..e33a10c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,26 @@
{
- "name": "notificationapi-react-native-sdk",
- "version": "1.0.0",
+ "name": "@notificationapi/react-native",
+ "version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "notificationapi-react-native-sdk",
- "version": "1.0.0",
+ "name": "@notificationapi/react-native",
+ "version": "0.0.1",
+ "license": "MIT",
"devDependencies": {
- "react": "19.2.0",
- "react-native": "0.82.1"
+ "@eslint/js": "^9.39.1",
+ "@types/react": "^19.1.1",
+ "eslint": "^9.39.1",
+ "prettier": "^3.7.4",
+ "react": ">=18.0.0",
+ "react-native": ">=0.73.0",
+ "typescript": "^5.0.0",
+ "typescript-eslint": "^8.48.1"
},
"peerDependencies": {
- "react": "*",
- "react-native": "*"
+ "react": ">=18.0.0",
+ "react-native": ">=0.73.0"
}
},
"node_modules/@babel/code-frame": {
@@ -47,6 +54,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -523,6 +531,222 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
+ "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
"node_modules/@isaacs/ttlcache": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz",
@@ -936,6 +1160,13 @@
"@babel/types": "^7.28.2"
}
},
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -973,6 +1204,13 @@
"@types/istanbul-lib-report": "*"
}
},
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "24.10.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz",
@@ -983,6 +1221,17 @@
"undici-types": "~7.16.0"
}
},
+ "node_modules/@types/react": {
+ "version": "19.1.1",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz",
+ "integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -1007,6 +1256,264 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.48.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz",
+ "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.48.1",
+ "@typescript-eslint/type-utils": "8.48.1",
+ "@typescript-eslint/utils": "8.48.1",
+ "@typescript-eslint/visitor-keys": "8.48.1",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.48.1",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.48.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz",
+ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.48.1",
+ "@typescript-eslint/types": "8.48.1",
+ "@typescript-eslint/typescript-estree": "8.48.1",
+ "@typescript-eslint/visitor-keys": "8.48.1",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.48.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz",
+ "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.48.1",
+ "@typescript-eslint/types": "^8.48.1",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.48.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz",
+ "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.48.1",
+ "@typescript-eslint/visitor-keys": "8.48.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.48.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz",
+ "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.48.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz",
+ "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.48.1",
+ "@typescript-eslint/typescript-estree": "8.48.1",
+ "@typescript-eslint/utils": "8.48.1",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.48.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz",
+ "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.48.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz",
+ "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.48.1",
+ "@typescript-eslint/tsconfig-utils": "8.48.1",
+ "@typescript-eslint/types": "8.48.1",
+ "@typescript-eslint/visitor-keys": "8.48.1",
+ "debug": "^4.3.4",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.48.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz",
+ "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.48.1",
+ "@typescript-eslint/types": "8.48.1",
+ "@typescript-eslint/typescript-estree": "8.48.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.48.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz",
+ "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.48.1",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -1039,6 +1546,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1046,6 +1554,16 @@
"node": ">=0.4.0"
}
},
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -1056,6 +1574,23 @@
"node": ">= 14"
}
},
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/anser": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz",
@@ -1315,6 +1850,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -1346,6 +1882,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -1551,6 +2097,13 @@
"node": ">= 8"
}
},
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1569,6 +2122,13 @@
}
}
},
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -1661,6 +2221,180 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/eslint": {
+ "version": "9.39.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
+ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.1",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
@@ -1672,7 +2406,53 @@
"esvalidate": "bin/esvalidate.js"
},
"engines": {
- "node": ">=4"
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
}
},
"node_modules/etag": {
@@ -1701,6 +2481,13 @@
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -1708,6 +2495,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fb-dotslash": {
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz",
@@ -1731,6 +2525,19 @@
"bser": "2.1.1"
}
},
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -1794,6 +2601,27 @@
"node": ">=8"
}
},
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/flow-enums-runtime": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz",
@@ -1885,6 +2713,32 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1892,6 +2746,13 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -1967,6 +2828,16 @@
"node": ">= 14"
}
},
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/image-size": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
@@ -1983,6 +2854,33 @@
"node": ">=16.x"
}
},
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-fresh/node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -2038,6 +2936,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -2048,6 +2956,19 @@
"node": ">=8"
}
},
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -2336,6 +3257,27 @@
"node": ">=6"
}
},
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -2349,6 +3291,16 @@
"node": ">=6"
}
},
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -2359,6 +3311,20 @@
"node": ">=6"
}
},
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/lighthouse-logger": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz",
@@ -2400,6 +3366,13 @@
"node": ">=8"
}
},
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
@@ -2854,6 +3827,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -2948,6 +3928,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@@ -2987,6 +3985,19 @@
"node": ">=6"
}
},
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -3057,6 +4068,32 @@
"node": ">= 6"
}
},
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.7.4",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
+ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
@@ -3094,6 +4131,16 @@
"asap": "~2.0.6"
}
},
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
@@ -3120,6 +4167,7 @@
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3170,6 +4218,7 @@
"integrity": "sha512-tFAqcU7Z4g49xf/KnyCEzI4nRTu1Opcx05Ov2helr8ZTg1z7AJR/3sr2rZ+AAVlAs2IXk+B0WOxXGmdD3+4czA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.82.1",
@@ -3600,6 +4649,19 @@
"node": ">=8"
}
},
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -3661,6 +4723,55 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -3691,6 +4802,32 @@
"node": ">=0.6"
}
},
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
@@ -3710,6 +4847,45 @@
"node": ">=8"
}
},
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.48.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz",
+ "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.48.1",
+ "@typescript-eslint/parser": "8.48.1",
+ "@typescript-eslint/typescript-estree": "8.48.1",
+ "@typescript-eslint/utils": "8.48.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -3758,6 +4934,16 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -3807,6 +4993,16 @@
"node": ">= 8"
}
},
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -3913,6 +5109,19 @@
"engines": {
"node": ">=12"
}
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
}
}
}
diff --git a/package.json b/package.json
index c2f044e..8305f1b 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,26 @@
{
- "name": "notificationapi-react-native-sdk",
- "version": "1.0.0",
- "description": "",
+ "name": "@notificationapi/react-native",
+ "version": "0.0.1",
+ "description": "React Native SDK for NotificationAPI - Push notifications for Android and iOS",
+ "main": "js/index.ts",
+ "types": "js/index.ts",
"react-native": "js/index",
"source": "js/index",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/notificationapi-com/notificationapi-react-native-sdk.git"
+ },
+ "keywords": [
+ "react-native",
+ "notificationapi",
+ "push-notifications",
+ "notifications",
+ "fcm",
+ "apn"
+ ],
+ "author": "NotificationAPI",
+ "license": "MIT",
+ "homepage": "https://www.notificationapi.com",
"files": [
"js",
"android",
@@ -16,18 +33,25 @@
"!**/__mocks__"
],
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "lint": "eslint js --ext .ts,.tsx",
+ "type-check": "tsc --noEmit",
+ "prettier": "prettier --config .prettierrc './js/**/*.ts' --write",
+ "prettier-check": "prettier --config .prettierrc './js/**/*.ts' --check"
},
"devDependencies": {
- "react": "19.2.0",
- "react-native": "0.82.1"
+ "@eslint/js": "^9.39.1",
+ "@types/react": "^19.1.1",
+ "eslint": "^9.39.1",
+ "prettier": "^3.7.4",
+ "react": ">=18.0.0",
+ "react-native": ">=0.73.0",
+ "typescript": "^5.0.0",
+ "typescript-eslint": "^8.48.1"
},
"peerDependencies": {
- "react": "*",
- "react-native": "*"
- },
- "resolutions": {
- "ws": "^7.5.10"
+ "react": ">=18.0.0",
+ "react-native": ">=0.73.0"
},
"codegenConfig": {
"name": "NotificationApiReactNativeSdkSpec",
diff --git a/rtn-notificationapi.podspec b/rtn-notificationapi.podspec
index 9e875d4..6770b54 100644
--- a/rtn-notificationapi.podspec
+++ b/rtn-notificationapi.podspec
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
s.license = package["license"]
s.platforms = { :ios => "13.0" }
s.author = package["author"]
- s.source = { :git => package["repository"], :tag => "#{s.version}" }
+ s.source = { :git => package["repository"]["url"], :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,swift}"
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e2f300f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "commonjs",
+ "lib": ["ES2020"],
+ "jsx": "react-native",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "outDir": "./dist",
+ "rootDir": "./js",
+ "types": ["react-native"]
+ },
+ "include": [
+ "js/**/*"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ "**/__tests__",
+ "**/__mocks__"
+ ]
+}
+