From eb08ca324104aa6040549238eaec9024a4e702de Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Sun, 24 Aug 2025 19:11:11 +0900 Subject: [PATCH] testing onesignal --- .env.example | 9 ++ src/lib/onesignalPublic.ts | 7 ++ src/lib/server/OneSignalService.ts | 84 +++++++++++++++++ src/lib/services/OneSignalService.ts | 98 ++++++++++++++++++++ src/routes/+layout.svelte | 24 +++++ src/routes/api/notifications/test/+server.ts | 36 +++++++ 6 files changed, 258 insertions(+) create mode 100644 .env.example create mode 100644 src/lib/onesignalPublic.ts create mode 100644 src/lib/server/OneSignalService.ts create mode 100644 src/lib/services/OneSignalService.ts create mode 100644 src/routes/api/notifications/test/+server.ts diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..5b82e36e --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# OneSignal configuration (SvelteKit style) +# Public values (exposed to client build): +PUBLIC_ONESIGNAL_APP_ID=00000000-0000-0000-0000-000000000000 +PUBLIC_ONESIGNAL_SAFARI_WEB_ID=web.onesignal.auto.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +# Private (server only): +PRIVATE_ONESIGNAL_REST_API_KEY=OS_REST_API_KEY_HERE + +# Existing variables (append your current ones below if you have a separate example file) +PUBLIC_SITE_URL=https://cropwatch.io diff --git a/src/lib/onesignalPublic.ts b/src/lib/onesignalPublic.ts new file mode 100644 index 00000000..5482bf96 --- /dev/null +++ b/src/lib/onesignalPublic.ts @@ -0,0 +1,7 @@ +import { env as publicEnv } from '$env/dynamic/public'; + +// Public-only config for client web push setup. +export const ONE_SIGNAL_PUBLIC_CONFIG = { + appId: publicEnv.PUBLIC_ONESIGNAL_APP_ID, + safari_web_id: publicEnv.PUBLIC_ONESIGNAL_SAFARI_WEB_ID +}; diff --git a/src/lib/server/OneSignalService.ts b/src/lib/server/OneSignalService.ts new file mode 100644 index 00000000..d4e2e0a8 --- /dev/null +++ b/src/lib/server/OneSignalService.ts @@ -0,0 +1,84 @@ +import { ErrorHandlingService } from '$lib/errors/ErrorHandlingService'; +import { env as publicEnv } from '$env/dynamic/public'; +import { env as privateEnv } from '$env/dynamic/private'; + +/** + * Server-side OneSignal push helper (2025 docs: Create message API) + * Uses SvelteKit dynamic env so build won't fail if vars absent at build-time. + * Required at runtime: + * PUBLIC_ONESIGNAL_APP_ID + * PRIVATE_ONESIGNAL_REST_API_KEY + */ +export class OneSignalService { + constructor(private errorHandler: ErrorHandlingService) {} + + private assertEnv() { + if (!publicEnv.PUBLIC_ONESIGNAL_APP_ID) + throw new Error('PUBLIC_ONESIGNAL_APP_ID not configured'); + if (!privateEnv.PRIVATE_ONESIGNAL_REST_API_KEY) + throw new Error('PRIVATE_ONESIGNAL_REST_API_KEY not configured'); + } + + private baseHeaders() { + this.assertEnv(); + return { + 'content-type': 'application/json; charset=utf-8', + authorization: `Key ${privateEnv.PRIVATE_ONESIGNAL_REST_API_KEY}` + }; + } + + async sendPush(options: { + headings?: Record; + contents: Record; + externalUserIds?: string[]; + includedSegments?: string[]; + data?: Record; + iosAttachments?: Record; + bigPicture?: string; + name?: string; + }) { + try { + this.assertEnv(); + const { + headings, + contents, + externalUserIds, + includedSegments, + data, + iosAttachments, + bigPicture, + name + } = options; + if (!contents || Object.keys(contents).length === 0) throw new Error('contents is required'); + if (!externalUserIds?.length && !includedSegments?.length) + throw new Error('Must supply externalUserIds or includedSegments'); + + const body: any = { + app_id: publicEnv.PUBLIC_ONESIGNAL_APP_ID, + target_channel: 'push', + contents, + headings, + data, + name + }; + if (externalUserIds?.length) body.include_external_user_ids = externalUserIds; + if (includedSegments?.length) body.included_segments = includedSegments; + if (iosAttachments) body.ios_attachments = iosAttachments; + if (bigPicture) body.big_picture = bigPicture; + + const res = await fetch('https://api.onesignal.com/notifications', { + method: 'POST', + headers: this.baseHeaders(), + body: JSON.stringify(body) + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`OneSignal error ${res.status}: ${text}`); + } + return await res.json(); + } catch (err) { + this.errorHandler.logError(err as Error); + throw err; + } + } +} diff --git a/src/lib/services/OneSignalService.ts b/src/lib/services/OneSignalService.ts new file mode 100644 index 00000000..458c469e --- /dev/null +++ b/src/lib/services/OneSignalService.ts @@ -0,0 +1,98 @@ +import { ErrorHandlingService } from '../errors/ErrorHandlingService'; +// SvelteKit env imports. Public App ID may be used on client; REST key must stay private. +import { PUBLIC_ONESIGNAL_APP_ID, PUBLIC_ONESIGNAL_SAFARI_WEB_ID } from '$env/static/public'; +import { PRIVATE_ONESIGNAL_REST_API_KEY } from '$env/static/private'; + +/** + * OneSignal push helper (2025 docs: Create message API) + * Env vars (SvelteKit style): + * PUBLIC_ONESIGNAL_APP_ID (public) + * PUBLIC_ONESIGNAL_SAFARI_WEB_ID (public, optional for Safari web push) + * PRIVATE_ONESIGNAL_REST_API_KEY (server only) + */ +export class OneSignalService { + constructor(private errorHandler: ErrorHandlingService) {} + + private assertEnv() { + if (!PUBLIC_ONESIGNAL_APP_ID) throw new Error('PUBLIC_ONESIGNAL_APP_ID not configured'); + if (!PRIVATE_ONESIGNAL_REST_API_KEY) + throw new Error('PRIVATE_ONESIGNAL_REST_API_KEY not configured'); + } + + private baseHeaders() { + this.assertEnv(); + return { + 'content-type': 'application/json; charset=utf-8', + authorization: `Key ${PRIVATE_ONESIGNAL_REST_API_KEY}` + }; + } + + /** + * Send a push notification. + * Provide either externalUserIds (preferred) or includedSegments. + */ + async sendPush(options: { + headings?: Record; + contents: Record; + externalUserIds?: string[]; // OneSignal External IDs + includedSegments?: string[]; // e.g. ['Test Users'] + data?: Record; + iosAttachments?: Record; + bigPicture?: string; // Android image + name?: string; // internal name in dashboard + }): Promise { + try { + this.assertEnv(); + const { + headings, + contents, + externalUserIds, + includedSegments, + data, + iosAttachments, + bigPicture, + name + } = options; + + if (!contents || Object.keys(contents).length === 0) { + throw new Error('contents is required'); + } + if (!externalUserIds?.length && !includedSegments?.length) { + throw new Error('Must supply externalUserIds or includedSegments'); + } + + const body: any = { + app_id: PUBLIC_ONESIGNAL_APP_ID, + target_channel: 'push', + contents, + headings, + data, + name + }; + if (externalUserIds?.length) body.include_external_user_ids = externalUserIds; + if (includedSegments?.length) body.included_segments = includedSegments; + if (iosAttachments) body.ios_attachments = iosAttachments; + if (bigPicture) body.big_picture = bigPicture; + + const res = await fetch('https://api.onesignal.com/notifications', { + method: 'POST', + headers: this.baseHeaders(), + body: JSON.stringify(body) + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`OneSignal error ${res.status}: ${text}`); + } + return await res.json(); + } catch (err) { + this.errorHandler.logError(err as Error); + throw err; + } + } +} + +// Optional helper for web push initialization (Safari ID is exposed for completeness) +export const ONE_SIGNAL_PUBLIC_CONFIG = { + appId: PUBLIC_ONESIGNAL_APP_ID, + safari_web_id: PUBLIC_ONESIGNAL_SAFARI_WEB_ID +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 54b85905..c477b7e5 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -13,6 +13,7 @@ import { onMount } from 'svelte'; import '../app.css'; import { info, warning } from '$lib/stores/toast.svelte'; + import { ONE_SIGNAL_PUBLIC_CONFIG } from '$lib/onesignalPublic'; // No preloading needed - dashboard will load its data when navigated to @@ -75,6 +76,29 @@ i18n.initialize(); }); + // OneSignal Web Push (2025 docs style) - only loads if appId present + onMount(() => { + if (typeof window === 'undefined') return; + if (!ONE_SIGNAL_PUBLIC_CONFIG.appId) return; + // Inject script once + if (!document.querySelector('script[data-onesignal-sdk]')) { + const s = document.createElement('script'); + s.src = 'https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js'; + s.defer = true; + s.setAttribute('data-onesignal-sdk', 'true'); + document.head.appendChild(s); + } + // Queue init + (window as any).OneSignalDeferred = (window as any).OneSignalDeferred || []; + (window as any).OneSignalDeferred.push(async function (OneSignal: any) { + await OneSignal.init({ + appId: ONE_SIGNAL_PUBLIC_CONFIG.appId, + safari_web_id: ONE_SIGNAL_PUBLIC_CONFIG.safari_web_id, + notifyButton: { enable: true } + }); + }); + }); + // Handle navigation loading states with a small delay to avoid flash on fast transitions let navTimer: ReturnType | null = null; beforeNavigate(() => { diff --git a/src/routes/api/notifications/test/+server.ts b/src/routes/api/notifications/test/+server.ts new file mode 100644 index 00000000..f1dda438 --- /dev/null +++ b/src/routes/api/notifications/test/+server.ts @@ -0,0 +1,36 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import { ErrorHandlingService } from '$lib/errors/ErrorHandlingService'; +import { OneSignalService } from '$lib/server/OneSignalService'; + +/** + * Test endpoint to send a push to either provided externalUserIds or the Test Users segment. + * Secure this route (add auth / role checks) before using outside local dev. + * Uses env vars: PUBLIC_ONESIGNAL_APP_ID / PRIVATE_ONESIGNAL_REST_API_KEY + * POST body JSON: { contents: { en: "Hello" }, externalUserIds?: ["user123"], includedSegments?: ["Test Users"] } + */ +export const POST: RequestHandler = async ({ request }) => { + const errorHandler = new ErrorHandlingService(); + const oneSignal = new OneSignalService(errorHandler); + try { + const body = await request.json(); + // Basic validation + if (!body.contents) { + return new Response(JSON.stringify({ error: 'contents required' }), { status: 400 }); + } + + const result = await oneSignal.sendPush({ + contents: body.contents, + headings: body.headings, + externalUserIds: body.externalUserIds, + includedSegments: body.includedSegments || (!body.externalUserIds && ['Test Users']), + data: body.data, + iosAttachments: body.iosAttachments, + bigPicture: body.bigPicture, + name: body.name || 'Test API Push' + }); + + return new Response(JSON.stringify({ success: true, result }), { status: 200 }); + } catch (err) { + return new Response(JSON.stringify({ error: (err as Error).message }), { status: 500 }); + } +};