feat: add and refactor fcm script, update readme
- Add xsendReady.ts, a production-ready script for sending FCM notifications. - Refactor script to be modular, use external config, and handle token validation efficiently. - Update README.md with more detailed project information, setup instructions, and scripts.
This commit is contained in:
89
README.md
89
README.md
@@ -1,38 +1,83 @@
|
|||||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
# Sistem Desa Mandiri
|
||||||
|
|
||||||
## Getting Started
|
Sistem Desa Mandiri adalah aplikasi web yang dirancang untuk membantu pengelolaan administrasi dan informasi di tingkat desa. Dibangun dengan Next.js, aplikasi ini menyediakan berbagai fitur untuk mendukung kegiatan desa, mulai dari pengumuman, diskusi, manajemen proyek, hingga administrasi kependudukan.
|
||||||
|
|
||||||
First, run the development server:
|
## Fitur Utama
|
||||||
|
|
||||||
|
* **Manajemen Pengguna:** Mengelola data anggota dan hak akses.
|
||||||
|
* **Pengumuman:** Menyebarkan informasi penting kepada seluruh warga desa.
|
||||||
|
* **Diskusi:** Forum untuk berdiskusi antar warga atau perangkat desa.
|
||||||
|
* **Manajemen Proyek & Tugas:** Melacak kemajuan proyek dan tugas yang sedang berjalan di desa.
|
||||||
|
* **Dokumentasi:** Tempat terpusat untuk menyimpan dan mengelola dokumen-dokumen penting.
|
||||||
|
* **Notifikasi Push:** Mengirimkan notifikasi real-time ke perangkat pengguna.
|
||||||
|
|
||||||
|
## Teknologi yang Digunakan
|
||||||
|
|
||||||
|
* **Framework:** [Next.js](https://nextjs.org/)
|
||||||
|
* **UI Framework:** [Mantine](https://mantine.dev/)
|
||||||
|
* **Database ORM:** [Prisma](https://www.prisma.io/)
|
||||||
|
* **Styling:** [Tailwind CSS](https://tailwindcss.com/)
|
||||||
|
* **State Management:** [Hookstate](https://hookstate.js.org/)
|
||||||
|
* **Push Notifications:** [Web Push](https://www.npmjs.com/package/web-push)
|
||||||
|
|
||||||
|
## Memulai
|
||||||
|
|
||||||
|
### Persyaratan
|
||||||
|
|
||||||
|
* [Node.js](https://nodejs.org/) (versi 20.x atau lebih tinggi)
|
||||||
|
* [Bun](https://bun.sh/) (direkomendasikan) atau package manager lain seperti npm/yarn/pnpm.
|
||||||
|
* Database (misalnya PostgreSQL, MySQL, atau SQLite).
|
||||||
|
|
||||||
|
### Instalasi
|
||||||
|
|
||||||
|
1. **Clone repositori ini:**
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
git clone https://github.com/username/sistem-desa-mandiri.git
|
||||||
# or
|
cd sistem-desa-mandiri
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
2. **Install dependensi:**
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
3. **Setup Variabel Lingkungan:**
|
||||||
|
Salin file `.env.test` menjadi `.env` dan sesuaikan nilainya, terutama untuk koneksi database.
|
||||||
|
```bash
|
||||||
|
cp .env.test .env
|
||||||
|
```
|
||||||
|
Buka file `.env` dan isi variabel yang diperlukan, seperti `DATABASE_URL`.
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
4. **Migrasi Database:**
|
||||||
|
Jalankan migrasi Prisma untuk membuat skema database.
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev
|
||||||
|
```
|
||||||
|
|
||||||
## Learn More
|
5. **Seed Database (Opsional):**
|
||||||
|
Jika Anda ingin mengisi database dengan data awal, jalankan perintah seed.
|
||||||
|
```bash
|
||||||
|
npx prisma db seed
|
||||||
|
```
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
6. **Jalankan Server Development:**
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
Aplikasi akan berjalan di [https://localhost:3000](https://localhost:3000).
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
## Skrip yang Tersedia
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
* `dev`: Menjalankan server development dengan HTTPS.
|
||||||
|
* `build`: Membuat build produksi dari aplikasi.
|
||||||
|
* `start`: Menjalankan server produksi.
|
||||||
|
* `lint`: Menjalankan linter untuk memeriksa kualitas kode.
|
||||||
|
* `prisma:seed`: Menjalankan skrip seed database.
|
||||||
|
|
||||||
## Deploy on Vercel
|
## Kontribusi
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
Kontribusi dalam bentuk apapun sangat kami hargai. Jika Anda menemukan bug atau memiliki ide untuk fitur baru, silakan buat *issue* baru. Jika Anda ingin berkontribusi dalam kode, silakan buat *pull request*.
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
## Lisensi
|
||||||
|
|
||||||
v011
|
Proyek ini dilisensikan di bawah [Lisensi ISC](LICENSE).
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import elysia from "elysia";
|
import elysia from "elysia";
|
||||||
import { sendFCMNotification } from "../../../../../../xsend";
|
import { sendFCMNotification } from "../../../../../../xsend";
|
||||||
|
import { sendMultiple } from "../../../../../../xsendReady";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ const ApiV2 = new elysia({
|
|||||||
'cRz96GHKTRaQaRJ35e8Hxa:APA91bEUSxE0VPbqKSzseQ_zGhbYsDofMexKykRw7o_3z2aPM9YFmZbeA2enrmb3qjdZ2g4-QQtiNHAyaZqAT1ITOrwo9jVJlShTeABmEFYP5GLEUZ3dlLc'
|
'cRz96GHKTRaQaRJ35e8Hxa:APA91bEUSxE0VPbqKSzseQ_zGhbYsDofMexKykRw7o_3z2aPM9YFmZbeA2enrmb3qjdZ2g4-QQtiNHAyaZqAT1ITOrwo9jVJlShTeABmEFYP5GLEUZ3dlLc'
|
||||||
]
|
]
|
||||||
|
|
||||||
await sendFCMNotification('c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo')
|
// await sendFCMNotification('c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo')
|
||||||
|
await sendMultiple()
|
||||||
return {
|
return {
|
||||||
data: "success elysia api"
|
data: "success elysia api"
|
||||||
};
|
};
|
||||||
|
|||||||
217
xsendReady.ts
Normal file
217
xsendReady.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
|
||||||
|
import { initializeApp, cert, App } from "firebase-admin/app";
|
||||||
|
import { getMessaging, Message } from "firebase-admin/messaging";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const key = {
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "mobile-darmasaba",
|
||||||
|
"private_key_id": "764e1207d5acf4db2eac539256c8f1bf397c7d8f",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwCU9PBpAbXsOl\ntb1syvWrmH3FSDRyI4oOVWZJRqYX+j44UTNfzTjYySpNy7x1lr91uOC2GGHJeFvT\nLg5er6uvzFvzwg42A8Rz4+aqxlUhvhNXYRyfaaP7tbui5X9GEmhKYzvYd6T/6z1u\njo7LE1tBaiB8eB69tSJidGcr90yXOsbvKFgaPkpvlrseNR/t0PYDUaXHsxdKvCHI\ntK13KxhJCJrU9+/W1Wwr+45WGfK9m+jLVuOEZT9dd3FUgDn/0CFzykZLA0iHRLjx\neczahlrlvLVCtUIJjHbmsjG8vLZyl6/puh1l2OkEZyADb6m7OOxFVTo5ADZvj4nD\nVCCirdMVAgMBAAECggEAMF0mbnJBpltnSkA/vkOWsmHPcCOx0QgFloGM/CXOXTkR\n3hwlDrWN4DWIi14ltXLIwFmeVzkkqJsKM19scEQ4WbC+NJ7Ek79+Ok7LYXDjE8Wq\nf6+9EukNtgqMdikySfilsYZI+2SHrw4czyKYhZ+YS0USjs/btkgtHbqYW+JyJvv4\nlXAGp3129kbOHTc6+DBq6tn4XiRMKUdBNtcRHe9k+zAIuwbeAdsl4bock1ADnMIv\n/Q4FfOua+nJl8MUpPCZDvz14az+3j/rUVkR/wgDqQirFNRfFfpEPNM2oXVSjp0oK\nTC8NEy5mN4aj0DYS8U2x8barsAFDr5N4L9JxTtdlgwKBgQDkXK9iieIe1/yJFDw6\ntHbQu/bl+t82DESapss62++6ckh2mo+IScvVg/rCwXIag7IRQO40BHWwYTrOwTbj\nD1VUamn6UaqJHpIjDj/SK+As3DumuOTcb+kbJq9TpjLGeR2hj0aKcFXAjL5+B+yr\nBt7fVsB2uhouS9aD68HV8azsxwKBgQDFV2yRKgSf11vNRsxtJekpZ7ruF4h8OZPA\nHcq1kMDPRJcuVD9XwG7RAEgxcErKKS6NrrT/2Iaq5r+P3owgxZ6yB5pabGGvsgcg\nqrvsVEjzETsrrDbp5IevwE/MTwplakr6vJBnfAyjqMbDQSGSZPp+6S8M5JtZhJDL\n9Pqy6yxNQwKBgEE9ZXGuWKZdKC11VXukAOnDOVcco9ZKDPNtwVPQb52BdshDgcv6\n4Tvfl606HMIMa7vYI/VCbOj17hoRQv/9anBScnJsEF9aF3/iW0NM+591T6li2ydK\n5Xq3Q5GPQqRHB7sXNpzoWOdIjkdtNiTqMpP1sch5hG9DhUZs/RSFFdUTAoGBALyV\nyD2NXu/1WVh5cQBZe1FDPMMtIBQ+3bB5h+8tDuTEEomGnyXX0s7OKy97tS0uX7us\nGnJo1IDblHMDZPwofnh5hYsmCdBiHCeeoYm+HhyS+e3JXIz2BKjy6g8/9ZpnEpI8\nwu7yAA4iSxfq1Q9Win/fjUQP71mDsvAGA9IZpbOLAoGBAK57RjNemVh3oNB5ZaQs\n45WzfmPPjKoDQdMYLtohHz9HhPxYFLuvlDc/9OcWFCz3tZHtyDrUtXvv+vX+rG4Y\nemxXkqdg3lYo7nayw772myJb2w6QIfGyuSRx/C1/phmPhp+UkHk7B+KdvWhpPmCC\nBufws2LSn5VZzivO6LrwSCfR\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "firebase-adminsdk-fbsvc@mobile-darmasaba.iam.gserviceaccount.com",
|
||||||
|
"client_id": "105653213329235865762",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40mobile-darmasaba.iam.gserviceaccount.com",
|
||||||
|
"universe_domain": "googleapis.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- KONFIGURASI ---
|
||||||
|
const CONFIG = {
|
||||||
|
/**
|
||||||
|
* Path ke file service account key JSON Anda.
|
||||||
|
* Pastikan file ini ada dan dapat diakses.
|
||||||
|
*/
|
||||||
|
firebaseServiceAccountPath: path.resolve(process.cwd(), "key.json"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konten notifikasi default yang akan dikirim.
|
||||||
|
*/
|
||||||
|
notificationPayload: {
|
||||||
|
title: "Sistem Desa Mandiri",
|
||||||
|
body: "Ada informasi baru untuk Anda, silakan periksa aplikasi.",
|
||||||
|
// Anda bisa menambahkan URL gambar ikon di sini
|
||||||
|
// imageUrl: "https://example.com/icon.png",
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pengaturan untuk mekanisme coba lagi (retry) jika terjadi kegagalan jaringan.
|
||||||
|
*/
|
||||||
|
retry: {
|
||||||
|
maxRetries: 3, // Jumlah maksimal percobaan ulang
|
||||||
|
delay: 2000, // Waktu tunda antar percobaan dalam milidetik (ms)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// --- AKHIR KONFIGURASI ---
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inisialisasi Firebase Admin SDK.
|
||||||
|
* Hanya akan menginisialisasi satu kali.
|
||||||
|
*/
|
||||||
|
let firebaseApp: App | null = null;
|
||||||
|
function initializeFirebase(): App {
|
||||||
|
if (firebaseApp) {
|
||||||
|
return firebaseApp;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const serviceAccount = key;
|
||||||
|
firebaseApp = initializeApp({
|
||||||
|
credential: cert(serviceAccount as any),
|
||||||
|
});
|
||||||
|
console.log("Firebase Admin SDK berhasil diinisialisasi.");
|
||||||
|
return firebaseApp;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("KRITIS: Gagal inisialisasi Firebase. Pastikan path service account key benar.");
|
||||||
|
console.error(`Detail Error: ${error.message}`);
|
||||||
|
process.exit(1); // Keluar dari proses jika Firebase gagal diinisialisasi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengambil daftar token perangkat dari database.
|
||||||
|
* TODO: Ganti fungsi ini dengan logika untuk mengambil token dari database Anda.
|
||||||
|
* @returns {Promise<string[]>} Daftar token FCM.
|
||||||
|
*/
|
||||||
|
async function getDeviceTokens(): Promise<string[]> {
|
||||||
|
console.log("Mengambil token perangkat dari sumber data...");
|
||||||
|
// CONTOH: Ini adalah data dummy. Seharusnya Anda mengambilnya dari database.
|
||||||
|
// Misalnya: const users = await prisma.user.findMany({ where: { fcmToken: { not: null } } });
|
||||||
|
// return users.map(user => user.fcmToken);
|
||||||
|
const exampleTokens: string[] = [
|
||||||
|
"c89yuexsS_uc1tOErVPu5a:APA91bEb6tEKXAfReZjFVJ2mMyOzoW_RXryLSnSJTpbIVV3G0L_DCNkLuRvJ02Ip-Erz88QCQBAt-C2SN8eCRxu3-v1sBzXzKPtDv-huXpkjXsyrkifqvUo", // Valid
|
||||||
|
"cRz96GHKTRaQaRJ35e8Hxa:APA91bEUSxE0VPbqKSzseQ_zGhbYsDofMexKykRw7o_3z2aPM9YFmZbeA2enrmb3qjdZ2g4-QQtiNHAyaZqAT1ITOrwo9jVJlShTeABmEFYP5GLEUZ3dlLc", // Valid
|
||||||
|
"token_tidak_valid_ini_pasti_gagal_12345", // Contoh token tidak valid
|
||||||
|
];
|
||||||
|
console.log(`Berhasil mendapatkan ${exampleTokens.length} token.`);
|
||||||
|
return exampleTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Membuat array pesan yang akan dikirim ke setiap token.
|
||||||
|
* @param {string[]} tokens - Daftar token FCM.
|
||||||
|
* @returns {Message[]} Array objek pesan untuk `sendEach`.
|
||||||
|
*/
|
||||||
|
function constructMessages(tokens: string[]): Message[] {
|
||||||
|
return tokens.map((token) => ({
|
||||||
|
token,
|
||||||
|
notification: {
|
||||||
|
title: CONFIG.notificationPayload.title,
|
||||||
|
body: CONFIG.notificationPayload.body,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
// Anda bisa menambahkan data tambahan di sini
|
||||||
|
// Contoh: click_action: "FLUTTER_NOTIFICATION_CLICK"
|
||||||
|
timestamp: String(Date.now()),
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
priority: "high",
|
||||||
|
notification: {
|
||||||
|
sound: "default",
|
||||||
|
channelId: "default_channel", // Pastikan channel ini ada di aplikasi Android Anda
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apns: {
|
||||||
|
payload: {
|
||||||
|
aps: {
|
||||||
|
sound: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menangani respons dari `sendEach` untuk mencatat keberhasilan dan kegagalan.
|
||||||
|
* @param response - Objek respons dari `sendEach`.
|
||||||
|
* @param originalTokens - Daftar token asli yang dikirimi pesan.
|
||||||
|
*/
|
||||||
|
function handleSendResponse(response: any, originalTokens: string[]) {
|
||||||
|
console.log("Total notifikasi berhasil dikirim:", response.successCount);
|
||||||
|
console.log("Total notifikasi gagal:", response.failureCount);
|
||||||
|
|
||||||
|
const tokensToRemove: string[] = [];
|
||||||
|
|
||||||
|
response.responses.forEach((resp: any, index: number) => {
|
||||||
|
const token = originalTokens[index];
|
||||||
|
if (resp.success) {
|
||||||
|
// console.log(`Pesan ke token ...${token.slice(-6)} berhasil:`, resp.messageId);
|
||||||
|
} else {
|
||||||
|
console.error(`Pesan ke token ...${token.slice(-6)} GAGAL:`, resp.error.code);
|
||||||
|
// Jika token tidak lagi terdaftar, tandai untuk dihapus
|
||||||
|
if (
|
||||||
|
resp.error.code === "messaging/registration-token-not-registered" ||
|
||||||
|
resp.error.code === "messaging/invalid-registration-token"
|
||||||
|
) {
|
||||||
|
tokensToRemove.push(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tokensToRemove.length > 0) {
|
||||||
|
console.warn("Token berikut tidak valid dan harus dihapus dari database:");
|
||||||
|
tokensToRemove.forEach(token => console.log(`- ${token}`));
|
||||||
|
// TODO: Implementasikan logika untuk menghapus token-token di atas dari database Anda.
|
||||||
|
// Misalnya: await prisma.user.updateMany({ where: { fcmToken: { in: tokensToRemove } }, data: { fcmToken: null } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fungsi utama untuk mengirim notifikasi ke banyak perangkat dengan mekanisme retry.
|
||||||
|
* @param {Message[]} messages - Array pesan yang akan dikirim.
|
||||||
|
* @param {string[]} tokens - Daftar token asli untuk logging.
|
||||||
|
*/
|
||||||
|
async function sendNotifications(messages: Message[], tokens: string[]) {
|
||||||
|
let lastError: any;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= CONFIG.retry.maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await getMessaging().sendEach(messages);
|
||||||
|
handleSendResponse(response, tokens);
|
||||||
|
return; // Berhasil, keluar dari fungsi
|
||||||
|
} catch (error: any) {
|
||||||
|
lastError = error;
|
||||||
|
console.error(`Percobaan pengiriman ke-${attempt} gagal:`, error.message);
|
||||||
|
|
||||||
|
// Hanya coba lagi jika error berhubungan dengan jaringan
|
||||||
|
const isNetworkError = error.code === "app/network-error" || error.code?.includes("network");
|
||||||
|
if (isNetworkError && attempt < CONFIG.retry.maxRetries) {
|
||||||
|
console.log(`Menunggu ${CONFIG.retry.delay}ms sebelum mencoba lagi...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, CONFIG.retry.delay));
|
||||||
|
} else {
|
||||||
|
// Jika bukan error jaringan atau sudah mencapai batas retry, lempar error
|
||||||
|
throw new Error(`Gagal mengirim notifikasi setelah ${attempt} percobaan: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fungsi orchestrator untuk menjalankan seluruh proses.
|
||||||
|
*/
|
||||||
|
export async function sendMultiple() {
|
||||||
|
try {
|
||||||
|
initializeFirebase();
|
||||||
|
|
||||||
|
const tokens = await getDeviceTokens();
|
||||||
|
if (tokens.length === 0) {
|
||||||
|
console.log("Tidak ada token perangkat yang ditemukan. Proses dihentikan.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = constructMessages(tokens);
|
||||||
|
|
||||||
|
console.log(`
|
||||||
|
Mempersiapkan pengiriman ${messages.length} notifikasi...`);
|
||||||
|
await sendNotifications(messages, tokens);
|
||||||
|
|
||||||
|
console.log("Proses pengiriman notifikasi selesai.");
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Terjadi error fatal dalam proses pengiriman:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user